본문 바로가기

Development/Web

Express + MVC pattern (1) - 프로젝트 초기 설정, View 기능 개발

들어가며

아키텍처 패턴(Architecture patterns)에는 MVC(Model-View-Controller), MVP(Model-View-Presenter), MTV(Model-Template-view) 등 여러 패턴이 존재한다. 이 중에서 MVC 패턴은 주로 Spring 프레임워크에서 사용되는 것으로 알고 있으나, 필자의 경우 Spring은 고사하고 Java를 다뤄볼 기회가 없었기 때문에 MVC 패턴이 무엇인지에 대해서 항상 궁금했었다. 따라서, MVC 패턴을 적용한 express로 로그인과 회원가입 기능을 간단하게 구현하면서 MVC 패턴이 어떻게 동작하는지 정리해보았다.

🔗 MDN - MVC pattern

🔗 MVC 패턴(written by. bp.chys)

🔗 YouTube - Node.js 백엔드 맛보기(with 우리밋_woorimIT)

초기 설정

🔗 소스코드 : v1.0.1-init

프로젝트 진행 시 Node.js는 14.17.3 버전으로 하였다.

폴더 구성

.gitignore
/app
    /node_modules
    /bin
    /src
        /database
        /public
        /routes
        /views
        /models
    app.js
    package-lock.json
    package.json

루트 경로에는 app 폴더가 있고, app 폴더 안에 모든 node 프로젝트의 모든 파일이 담겨있다. app 폴더에는 크게 bin, src 폴더로 나뉘는데, bin 폴더는 express의 서버를 실행시키는 소스코드 파일이 담겨있고, src 폴더에는 웹 서버의 동작을 실행시키는 여러 폴더가 담겨있다. src 폴더에 담긴 여러 폴더를 기능별로 설명하면 아래와 같다.

  • database : 본 프로젝트를 진행할 때 json 파일을 file system으로 접근하는 방식의 데이터베이스를 적용하였으며, 해당 파일이 보관되어 있는 폴더이다.
  • public : 웹 페이지를 구성하는데 필요한 정적파일(js, css)이 보관되어 있는 폴더이며, express의 static 미들웨어를 적용하여 해당 폴더의 경로를 지정하였다.
  • routes : 클라이언트로부터 받은 요청을 나누어줄 라우팅 파일과 라우팅 기능별로 Controller 역할을 하는 파일이 함께 보관되어 있는 폴더이다.
  • views : View 역할을 하는 파일(.ejs)이 보관되어 있는 폴더이다.
  • models : 데이터베이스의 처리 기능을 담당하는 클래스와 저장소 기능을 담당하는 클래스를 보관하는 폴더이다.

의존성 설치

본 프로젝트에 필요한 패키지는 ejs, express이며, 터미널에 아래와 같이 입력하여 해당 패키지를 설치한다.

$ npm i express ejs -s

또한, 소스코드 변경 후 서버를 재실행해야 하는 번거로움을 줄이기 위해 nodemon을 전역으로 설치한다.

$ npm i nodemon -g

npm 초기화

bin 폴더에 www.js 파일을 생성하고, 터미널의 실행 경로를 app 폴더로 변경한 후 아래 명령어를 사용하여 npm으로 초기화한다.

npm init -y

이때, package.json의 scripts에 nodemon으로 서버를 실행시키는 명령어를 추가하고, 나머지는 본인의 기호에 맞게 수정한다(package.json의 자세한 내용은 아래 링크에 자세하게 기재되어 있다).

🔗 goorm.io - npm 활용하기(package.json)

추가적으로 프로젝트의 소스코드를 저장하기 위해 git을 연결하고, repo 설정을 해주도록 하자.

express 서버 구현

🔗 소스코드 : v1.0.2-server

express를 사용하면 아래 코드와 같이 간단하게 서버를 구현할 수 있다.

/* ./app/app.js */

"use strict";

const express = require('express');
const app = express();

app.get('/', (req, res) => {
    return res.send("메인 페이지");
});

app.get('/login', (req, res) => {
    return res.send("로그인 페이지");
});

app.get('/signup', (req, res) => {
    return res.send("회원가입 페이지");
});

const port = 3000;
app.listen(port, () => {
    console.log(`express server running on port ${port}`);
});

서버 실행 코드 분리

위의 코드에서 서버는 아래 부분을 통해 실행된다.

/* ...생략... */

const port = 3000;
app.listen(port, () => {
    console.log(`express server running on port ${port}`);
});

가독성을 위해 이 부분을 ./app/bin/www.js로 옮겨주도록 하자. 먼저, ./app/bin/www.js에서 ./app/app.js의 app을 불러올 수 있도록 아래와 같이 수정하여 app을 모듈로 내보내도록 한다.

/* ./app/app.js */

"use strict";

const express = require('express');
const app = express();

app.get('/', (req, res) => {
    return res.send("홈 페이지");
});

app.get('/login', (req, res) => {
    return res.send("로그인 페이지");
});

app.get('/signup', (req, res) => {
    return res.send("회원가입 페이지");
});

module.exports = app;

이어서 ./app/bin/www.js의 코드를 아래와 같이 작성한다.

/* ./app/bin/www.js */

"use strict";

const app = require("../app");
const port = 3000;

app.listen(port, () => {
    console.log(`express server running on port ${port}`);
});

그리고 터미널을 ./app 경로로 이동시킨 후 아래 명령어를 입력하여 서버를 실행한다.

$ npm start

라우팅 분리

routes 폴더에 ./app/home/index.js 폴더와 파일을 생성한 후 아래와 같이 코드를 입력하여 /, /login, /signup 페이지로 접속하는 라우터를 분리해주도록 하자.

/* ./app/src/routes/home/index.js */

"use strict";

const express = require('express');
const router = express.Router();

router.get('/', (req, res) => {
    return res.send("홈 페이지");
});

router.get('/login', (req, res) => {
    return res.send("로그인 페이지");
});

router.get('/signup', (req, res) => {
    return res.send("회원가입 페이지");
});

module.exports = router;

그리고 위에서 작성한 라우터를 ./app/app.js에 불러온 후 아래와 같이 미들웨어에 등록해준다.

/* ./app/app.js */

"use strict";

const express = require('express');
const app = express();
const home = require('./src/routes/home');

app.use('/', home);

module.exports = app;

Views 구현

🔗 소스코드 : v1.0.3-views

위에서는 웹 서버의 경로를 요청할 때 문자열을 응답하도록 구현하였는데, ejs로 웹 페이지를 응답하는 방식으로 바꾸어보자.

홈, 로그인, 회원가입 화면 꾸미기

./app/src/views/home에 index.ejs, login.ejs, signup.ejs 파일을 만들고 홈 화면, 로그인 화면, 회원가입 화면을 구현한다. 실습 편의성을 위해 code pen에 업로드된 템플릿을 그대로 사용하였는데, 이와 같이 누군가의 소스코드를 사용할 때에는 저작권을 최상단에 주석으로 남겨주어야 한다.

<!-- ./app/src/views/home/index.ejs -->

<!DOCTYPE html>
<html lang="ko">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>메인 페이지</title>
</head>
<body>
    <h1>메인 페이지</h1>
    <a href="/login">로그인하기</a>
</body>
</html>
<!-- ./app/src/views/home/login.ejs -->

<!-- Copyright (c) 2022 by Aigars Silkalns (https://codepen.io/colorlib/pen/rxddKy) -->

<!DOCTYPE html>
<html lang="ko">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>로그인 페이지</title>
</head>
<body>
    <div class="container">
        <div class="form">
            <form class="login-form">
                <input id="id" type="text" placeholder="아이디"/>
                <input id="passwd" type="password" placeholder="비밀번호"/>
                <p id="btn">login</p>
                <p class="message">계정이 없으신가요? <a href="/signup">회원가입</a></p>
            </form>
        </div>
    </div>
</body>
</html>
<!-- ./app/src/views/home/signup.ejs -->

<!-- Copyright (c) 2022 by Aigars Silkalns (https://codepen.io/colorlib/pen/rxddKy) -->

<!DOCTYPE html>
<html lang="ko">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>회원가입 페이지</title>
</head>
<body>
    <div class="container">
        <div class="form">
            <form class="signup-form">
                <input id="id" type="text" placeholder="아이디"/>
                <input id="name" type="text" placeholder="이름"/>
                <input id="passwd" type="password" placeholder="비밀번호"/>
                <input id="passwd-confirm" type="password" placeholder="비밀번호 확인"/>
                <p id="btn">회원가입</p>
                <p class="message">이미 계정이 있으신가요? <a href="/login">로그인</a></p>
            </form>
        </div>
    </div>
</body>
</html>

스타일 시트 작성

아래 링크를 참고하여 ./app/src/public/css/home/style.css에 스타일 시트를 작성한다.

🔗 소스코드 : v1.0.3-views의 style.css

로그인, 회원가입 페이지 이벤트 구현

로그인, 회원가입 페이지에서 DOM 객체에 접근하여 로그인, 회웝가입 버튼 클릭 시의 이벤트를 연결해주도록 하자. ./app/src/public/js/home 폴더에 각각 login.js와 signup.js를 생성한 후 아래 코드와 같이 입력한다.

/* ./app/public/js/home/login.js */

"use strict";

const id = document.querySelector('#id'),
    passwd = document.querySelector('#passwd'),
    btn = document.querySelector('#btn');

btn.addEventListener('click', login);

function login() {
    const req = {
        id: id.value,
        passwd: passwd.value
    }
    console.log(req);
};
/* ./app/public/js/home/signup.js */

"use strict";

const id = document.querySelector('#id'),
    name = document.querySelector('#name'),
    passwd = document.querySelector('#passwd'),
    confirmPasswd = document.querySelector('#passwd-confirm'),
    btn = document.querySelector('#btn');

btn.addEventListener('click', signup);

function signup() {
    const req = {
        id: id.value,
        name: id.value,
        passwd: passwd.value,
        confirmPasswd: confirmPasswd.value
    }
    console.log(req);
};

views 파일 경로와 engine 설정

이제 서버가 view로 인식할 대상을 설정해주어야 한다. ./app/app.js에 아래와 같이 코드를 추가하여 수정해준다.

/* ./app/app.js */

"use strict";

const express = require('express');
const app = express();
const home = require('./src/routes/home');

app.set('views', "./src/views");
app.set('view engine', 'ejs');

app.use('/', home);

module.exports = app;

정적(static) 파일 경로 설정

위에서 작성한 public 폴더의 css파일과 js 파일은 각각의 ejs 파일에서 불러온 후 해당 웹 페이지에 적용된다. 이와 같이 내용이 고정되어 응답을 할 때 별도의 처리 없이 파일 내용 그대로 보내지는 파일을 정적(static) 파일이라고 한다. express에서 정적 파일을 웹 페이지에 적용시키려면, 정적 파일의 경로를 설정해주어야 한다. 따라서, 아래와 같이 ./app/app.js에 아래와 같이 코드를 추가하여 수정해준다.

/* ./app/app.js */

"use strict";

const express = require('express');
const app = express();
const home = require('./src/routes/home');

app.set('views', "./src/views");
app.set('view engine', 'ejs');
app.use(express.static(`${__dirname}/src/public`));

app.use('/', home);

module.exports = app;

이어서 ./app/src/views/home 폴더의 login.ejs와 signup.ejs에서 정적 파일을 불러올 수 있도록 <head>에 각각 아래의 코드를 추가한다.

<!-- ./app/src/views/home/login.ejs -->

<link rel="stylesheet" href="/css/home/style.css">
<script src="/js/home/login.js" defer></script>
<!-- ./app/src/views/home/signup.ejs -->

<link rel="stylesheet" href="/css/home/style.css">
<script src="/js/home/signup.js" defer></script>

ejs 파일로 응답 처리

지금까지는 서버로 요청이 들어오면 그 응답으로 문자열을 보내주었다. 이번에는 위에서 작성한 ejs 파일을 렌더링해주도록 하기 위해 ./app/src/routes/home/index.js를 아래와 같이 수정한다.

/* ./app/src/routes/home/index.js */

"use strict";

const express = require('express');
const router = express.Router();

router.get('/', (req, res) => {
    return res.render('home/index.ejs');
});

router.get('/login', (req, res) => {
    return res.render("home/login.ejs");
});

router.get('/signup', (req, res) => {
    return res.render("home/login.ejs");
});

module.exports = router;

ejs 파일의 경로를 위와 같이 나타내는 이유는 위에서 views의 폴더의 경로를 설정(app.set('views', "./src/views"))해주었기 때문이다. 코드 수정을 거친 파일을 모두 저장한 후 localhost:3000/login과 localhost:3000/signup에 접속해보면 웹 페이지가 정상적으로 보이고, 각각 로그인, 회원가입 버튼을 클릭하면 콘솔창에 object가 출력되는 것을 확인할 수 있다.

쉬어가며

프로젝트 초기 설정부터 MVC의 VIew까지 구현해보았다. 글을 작성하다보니, 너무 길어졌기 때문에 잠시 여기서 쉬어가도록 하겠다.