이번에도, 글을 시작하기 전에 이전 글을 한 번쯤 읽거나 훑어볼 것을 강력히 권한다. 또한, 이 프로젝트가 어떻게 진행되었는지 궁금하거나 과정에 대해 관심이 있다면, 여기에 글들을 정리해 두었으니 가볍게 읽어보는 것도 좋다. 이 글은 기초적 웹 기술(HTML+CSS, Node.js, Express.js)에 대한 지식을 요구하고, 그에 대한 예제 코드를 포함하고 있어 프로그래밍에 관심이 없거나 웹 기술을 경험해보지 못한 사람에게는 적절하지 않은 내용일 수 있다.
저번 글에서 엔드포인트 개발을 완료했기에, 사용자에게 페이지를 표시하고 엔드포인트에 요청을 전송, 사용자가 원하는 명령을 처리하는 것이 주 목표이다. 로그인, 메일 목록, 메일 상세 정보, 메일 전송 등을 담당하는 페이지를 각각 만들 계획이다. 차례대로 천천히 개발해 본다.
Step 9.
프론트엔드: 라우터 셋
이제, 엔드포인트 라우터가 아닌 프론트엔드를 위한 라우터를 설정해 줄 차례이다. 로그인 페이지를 제외한 모든 페이지는 전부 사용자의 ..session.username의 null 여부를 판단하는 미들웨어를 요한다는 것에 유의하며 코드를 작성한다. 일단, 현재로서 예상 가능한 모든 경우의 라우트를 추가한다.
// route/public.route.js
const express = require('express');
const router = express.Router();
function checkUserPermission(req, res, next, opposite = false) {
if (req.session.username == null) {
if (opposite)
next();
else
res.redirect("/login");
return;
}
if (!opposite)
next();
else
res.redirect("/");
}
router.use("/login", (req, res, n) => checkUserPermission(req, res, n, true));
router.get("/login", (req, res) => {
// Login
});
router.use("/", checkUserPermission);
router.get("/", (req, res) => {
// Dashboard
res.send("");
});
router.use("/mails", checkUserPermission);
router.get("/mails", (req, res) => {
// Mailbox
});
router.use("/mails/:mail_idx", checkUserPermission);
router.get("/mails/:mail_idx", (req, res) => {
// Mail detail
});
router.use("/create", checkUserPermission);
router.get("/create", (req, res) => {
// Create a new email
});
router.use("/logout", checkUserPermission);
router.get("/logout", (req, res) => {
// Logout?
});
module.exports = router;
일단 로그인, 대시보드, 메일함, 메일 상세, 메일 전송, 로그아웃에 대한 라우트들을 라우터에 할당한다. 추후 페이지를 만들고 라우트에서 res.render(...) 등을 통해 렌더링하는 방향으로 코드를 작성하면 된다. 로그인 페이지에 대한 라우트에만 checkUserPermission 함수의 opposite 파라미터를 참으로 전송하는 것으로, 예외로서 반대로 동작하도록 설정할 수 있다.
Step 10.
프론트엔드: 로그인
checkUserPermission(req, res, next, opposite?) 함수 정의 부분을 보면, 사용자의 세션 값에 username이 없고 opposite가 거짓이라면, res.redirect(uri) 메서드를 호출하여 강제로 로그인 페이지로 이동시키는 것을 볼 수 있다.
function checkUserPermission(req, res, next, opposite = false) {
if (req.session.username == null) {
if (opposite)
next();
else
res.redirect("/login");
return;
}
if (!opposite)
next();
else
res.redirect("/");
}
이렇게 설정하고 상술한 것과 같이 로그인 페이지 라우트에 적용되는 미들웨어만 opposite 파라미터를 참으로 넘기는 방식을 사용하여 Guest 상태로 페이지에 접근 시 로그인 페이지를 표시하게끔 만든다.
ejs 파일을 HTML 페이지로서 표시하기 위해 ejs 라이브러리를 설치하고, Express 앱에 포함시킬 필요가 있다. 코드를 적용한 이후에는 res.render(filename) 메서드를 통해 쉽게 ejs 파일을 보여줄 수 있다.
// main.js
// ...
app.set("view engine", "ejs");
app.set("views", "./view");
// ...
이제 ejs를 사용할 수 있게 되었으므로 view 폴더에 login.ejs 파일을 만들고, 템플릿의 기초 내용을 채운다. ejs 템플릿에 대한 문법은 여기에서 확인할 수 있다.
<!-- view/login.ejs -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Login</title>
</head>
<body>
<div class="wrap">
<h1>Login</h1>
<form onsubmit="return false;" action="">
<input type="text" id="login-id" name="login-id" required>
<input type="password" id="login-pass" name="login-pass" required>
<button id="login-btn">Login</button>
</form>
</div>
</body>
</html>
이제, res.render 메서드를 활용하여 로그인 라우트에 접근하면 view/login.ejs 파일을 보여주도록 설정한다.
// route/public.route.js
// ...
router.use("/login", (req, res, n) => checkUserPermission(req, res, n, true));
router.get("/login", (req, res) => {
res.render("login");
});
// ...
상단에서 적용한 미들웨어에 따라, 기본 라우트에 접근할 경우(예제의 경우에는 http://localhost/) 로그인 페이지로 자동으로 리다이렉트되는 것도 확인할 수 있다.
이제, 로그인 페이지를 엔드포인트와 연결하는 코드를 작성한다. /assets 디렉터리를 만들고, Express.js에 정적 디렉토리임을 명시함으로써 웹 페이지에서 JavaScript 파일을 불러올 수 있도록 만들면 된다.
// main.js
// ...
app.use("/assets", express.static(__dirname + "/assets"));
// ...
정적 어셋 디렉터리에 JS 파일을 위한 디렉터리를 생성하고, 그 안에 클라이언트를 담당하는 JS 파일(예제에서는 everyday.client.js)을 생성한다. 이후, 로그인 버튼인 '#login-btn'에 대한 클릭 이벤트 리스너를 작성해줌으로써 로그인 요청을 보낼 수 있도록 만든다.
이벤트 리스너를 작성하기 전에 요청을 전송하는 함수 sendRequest(method, uri, body?)를 작성한다.
// assets/scripts/everyday.client.js
const sendRequest = (method, uri, body = {}) => {
return new Promise((res, rej) => {
const xhr = new XMLHttpRequest();
xhr.onreadystatechange = () => {
const { status } = xhr;
if (xhr.readyState === XMLHttpRequest.DONE) {
if (status < 400)
res(JSON.parse(xhr.responseText));
else
rej(JSON.parse(xhr.responseText));
}
};
xhr.open(method, uri);
xhr.setRequestHeader("Content-Type", "application/json");
xhr.send(JSON.stringify(body));
});
};
그리고 개체와 이벤트 리스너들을 저장하는 객체를 하나 만들어, 이벤트 리스너의 등록, 개체 관리 등을 더욱 쉽게 할 수 있도록 만든다. 이렇게 하면, document.querySelector(selector) 메서드가 여러 번 동작하지 않게 되어 효율적이다.
// assets/scripts/everyday.client.js
const $ELEMENTS = {};
// ...
아직은 아무 내용도 없다. Document가 완전히 로드되고 난 후 속성들을 설정해줄 것이다.
이제 아직 존재하지 않는 메서드인 Client.loginPage.submit() 메서드를 작성해야 한다. 이 메서드가 실질적으로 로그인 요청을 보내는 메서드가 된다.
먼저, Client 객체를 작성한다. 이 객체는 개체 테이블 업데이트, 개체에 이벤트 리스너 추가(할당), 이벤트 리스너들의 컨테이너 등을 담는 역할을 하게 된다.
// assets/scripts/everyday.client.js
// ...
const Client = {
init() {
this.listen();
},
initTable() {
Object.assign($ELEMENTS, {
"login.username": {
element: document.querySelector("#login-id")
},
"login.password": {
element: document.querySelector("#login-pass")
},
"login.submit_button": {
element: document.querySelector("#login-btn"),
eventListeners: [
{
event: "click",
eventListener: Client.loginPage.submit
}
]
}
});
},
listen() {
for (const v of Object.values($ELEMENTS)) {
for (const listener of (v.eventListeners ?? [])) {
v.element.addEventListener(listener.event, listener.eventListener);
}
}
}
};
Client.init() 메서드는 Client 객체의 초기화 메서드로, 외부에서 Client 내부의 메서드들을 한꺼번에 호출하는 것은 보기에 좋지 않으므로 추가하였다.
이제 Client.loginPage 객체와 Client.loginPage.submit() 메서드를 작성하여 코드를 완성한다. 로그인 요청을 전송하고, 성공 시(Resolve 함수 호출 시) location.href 속성 수정을 통해 대시보드 페이지로 리다이렉트한다. 그렇지 않으면 오류 메시지를 표시하면 된다.
// assets/scripts/everyday.client.js
// ...
const Client = {
loginPage: {
async submit() {
const username = $ELEMENTS["login.username"].value;
const password = $ELEMENTS["login.password"].value;
try {
await sendRequest(GET, "/endpoints/login", { username, password });
location.href = "/";
} catch (e) {
alert("Incorrect ID or password");
}
}
},
init() {
this.initTable();
this.listen();
},
initTable() {
Object.assign($ELEMENTS, {
"login.username": {
element: document.querySelector("#login-id")
},
"login.password": {
element: document.querySelector("#login-pass")
},
"login.submit_button": {
element: document.querySelector("#login-btn"),
eventListeners: [
{
event: "click",
eventListener: Client.loginPage.submit
}
]
}
});
},
listen() {
for (const v of Object.values($ELEMENTS)) {
for (const listener of (v.eventListeners ?? [])) {
v.element.addEventListener(listener.event, listener.eventListener);
}
}
}
};
이제, Client의 초기화 메서드를 자동으로 호출해주는 코드를 작성한다. 페이지 컨텐츠가 로드되면 실행할 것이므로 document에 'DOMContentLoaded' 이벤트 리스너를 추가하면 된다.
// assets/scripts/everyday.client.js
// ...
document.addEventListener("DOMContentLoaded", () => {
Client.init();
});
다시 ejs 파일로 들어와서 작성한 JS 파일을 script 태그로 불러옴으로써 페이지에 스크립트를 적용한다.
<!-- view/login.ejs -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Login</title>
<script src="/assets/scripts/everyday.client.js"></script>
</head>
<body>
<div class="wrap">
<h1>Login</h1>
<form onsubmit="return false;" action="">
<input type="text" id="login-id" name="login-id" required>
<input type="password" id="login-pass" name="login-pass" required>
<button id="login-btn">Login</button>
</form>
</div>
</body>
</html>
Step 11.
프론트엔드: 대시보드
대시보드라는 말은 거창하지만, 사실 별 거 없이 메일함과 메일 작성 페이지로 향하는 메뉴만을 포함하는 페이지이다. 적어도 지금 만드는 프로토타입에서는 그렇다. 따라서, view 디렉터리에 dashboard.ejs 파일을 만들고 내용만 채워 둔다.
<!-- view/dashboard.ejs -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Login</title>
<script src="/assets/scripts/everyday.client.js"></script>
</head>
<body>
<div class="wrap">
<h1>Dashboard</h1>
<a href="/logout">Logout</a>
<a href="/mails">Mail list</a>
<a href="/create">New mail</a>
</div>
</body>
</html>
물론, ejs 파일을 res.render를 통해 보여주는 코드도 작성해야 한다.
// route/public.route.js
// ...
router.use("/", checkUserPermission);
router.get("/", (req, res) => {
// Dashboard
res.render("dashboard");
});
// ...
마치며
ejs 파일을 표시하고, 표시된 ejs 파일에 클라이언트용 JS 파일을 불러와서 엔드포인트와 통신하는 데에 성공했다. 이번에는 Client 객체, sendRequest(method, uri, body?) 함수 등을 설정하기 위해 목표까지 조금 돌아갔지만, 앞으로 구현하는 기능들은 모두 ejs 작성과 이벤트 리스너 추가로 구현되므로 더욱 명확하리라 생각한다.
지금 작성하는 프로젝트 개발일지가 좋은 컨텐츠일지 필자로서는 알 수 없지만, 그래도 당신의 도움이 필요하다. 만약 이 개발일지에 관심이 있거나, 이 개발일지가 유익하거나 좋은, 양질의 컨텐츠라고 생각된다면 아래의 공감 버튼을 한 번 클릭해 주시길 바란다. 이 프로젝트의 마지막 글은 프로젝트에서 사용한 다양한 핵심 개념만을 정리한 내용으로 작성할 생각이기에, 프로젝트에는 관심이 없지만 Nodemailer, mailpop3, Express.js 등의 용례 등에 관심이 있을 경우에도 더욱 도움이 되는 자료를 만들 수 있게끔 도와주시기를 부탁드린다.
본 글에서 오류가 있거나, 지나치게 편향적이거나 주관적인 서술이 있어 이해를 방해하는 등 문제가 있다면 언제든지 댓글로 피드백을 해주어 오류 수정에 도움을 주실 수 있다. 긴 글 읽어 주시어 진심으로 감사드린다.
'개발일지 > 웹' 카테고리의 다른 글
텍스트 에디터로 메일 전송 페이지 만들기: 웹메일 클라이언트 개발하기 6 (0) | 2024.07.28 |
---|---|
웹메일들은 어떻게 메일을 보여주는가: 웹메일 클라이언트 개발하기 5 (0) | 2024.07.23 |
웹 우체국으로 편지 보내는 방법: 웹메일 클라이언트 개발하기 3 (0) | 2024.07.19 |
메일은 어떻게 받아오는가: 웹메일 클라이언트 개발하기 2 (0) | 2024.07.16 |
내 도메인 주소에서 보내는 메일: 웹메일 클라이언트 개발하기 1 (0) | 2024.07.14 |