본문 바로가기

Development/Web

Express + MVC pattern (2) - Contoller 기능 구현, DB 구축, fetch, Model로 구현한 로그인 처리

이어가며

지난 글에서는 프로젝트 초기 설정부터 View 기능 개발까지 정리해보았다. 이번에는 컨트롤러를 구현하고, 모델로 로그인 기능을 구현하는 부분까지 정리해보도록 하겠다.

Controller 구현

🔗 소스코드 : v1.0.4-controller

이전에는 라우터에서 직접 웹 페이지를 렌더링 할 수 있도록 코드를 작성하였는데, 이러한 동작을 컨트롤러를 통해 할 수 있도록 코드를 수정해보겠다. 먼저, 컨트롤러는 두 개의 object로 구성할 건데, 첫 번째는 웹 페이지를 렌더링시켜주는 render, 두 번째는 HTTP 요청에 맞는 로직을 처리하는 process로 구성하겠다. 이를 ./app/src/routes/home/index.controller.js에 작성하면 아래와 같다.

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

"use strict";

const render = {
    index: (req, res) => {
        res.render('home/index.ejs');
    },
    login: (req, res) => {
        res.render('home/login.ejs');
    },
    signup: (req, res) => {
        res.render('home/signup.ejs');
    }
};

const process = {

};

module.exports = { render, process };

이어서 ./app/src/routes/home/index.js를 아래 코드와 같이 수정한다.

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

"use strict";

const express = require('express');
const router = express.Router();
const controller = require('./index.controller');

router.get('/', controller.render.index);
router.get('/login', controller.render.login);
router.get('/signup', controller.render.signup);

module.exports = router;

이와 같이 코드를 작성하면, 가독성이 높아졌을 뿐만 아니라 각각의 기능이 잘 분리되어 더욱 편리하게 코드를 관리할 수 있다.

클라이언트 측 데이터 전달

🔗 소스코드 : v1.0.5-fetch

전체 프로젝트 폴더에서 클라이언트 측을 담당하는 부분은 public 폴더에 존재하는 static 파일이고, 나머지는 서버를 구성하는 코드라고 할 수 있다.

fetch를 사용한 HTTP 요청 구현

로그인, 회원가입 버튼을 클릭하면, 클라이언트는 해당 데이터를 요청 body에 포함시켜 json 형식의 문자열로 넘겨준다. 이를 fetch로 구현한 코드는 다음과 같다.

/* ./app/src/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
    }
    fetch("/login", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify(req)
    })
        .then(res => res.json())
        .then(res => {
            console.log(res);
        });
};

위 코드를 간단하게 설명하자면, 서버의 '/login'으로 POST 요청을 보내고, 그 응답을 console.log로 출력하도록 하였다. fetch나 axios로 받은 서버의 응답은 Promise로 넘어오는데, then을 통해 비동기로 처리하였다.

ECMA Script의 동기/비동기(Promise, then, async, await) 처리는 이 프로젝트의 주된 주제가 아니므로 자세한 설명은 생략하고, 나중에 따로 정리해서 포스팅하도록 하겠다.

./app/src/public/js/home/signup.js에서도 서버의 '/signup'으로 POST 요청을 보낼 수 있도록 위와 같이 수정해주도록 하자.

서버 측 컨트롤러 및 라우팅 수정

fetch로 클라이언트 측에서 서버 측에 요청을 보냈지만, 아직 서버 측에서는 해당 요청을 처리하는 라우팅을 해주지 않았다. 서버에서 POST 요청을 받으면, 그 요청 body에 포함된 데이터를 console.log로 출력하는 컨트롤러를 아래와 같이 작성해보자.

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

"use strict";

const render = {
    index: (req, res) => {
        res.render('home/index.ejs');
    },
    login: (req, res) => {
        res.render('home/login.ejs');
    },
    signup: (req, res) => {
        res.render('home/signup.ejs');
    }
};

const process = {
    login: (req, res) => {
        console.log(req.body);
    },
    signup: (req, res) => {
        console.log(req.body);
    }
};

module.exports = { render, process };

이어서 서버의 '/login', '/signup'으로 POST 요청으로 들어왔을 때 이를 처리하기 위한 라우터를 컨트롤러와 연결해주도록 아래와 같이 코드를 수정한다.

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

"use strict";

const express = require('express');
const router = express.Router();
const controller = require('./index.controller');

// RENDER
router.get('/', controller.render.index);
router.get('/login', controller.render.login);
router.get('/signup', controller.render.signup);

// PROCESS
router.post('/login', controller.process.login);
router.post('/signup', controller.process.signup);

module.exports = router;

body-parser 미들웨어 적용

localhost:3000/login으로 접속 후 로그인 버튼을 클릭하여 그 결과를 서버측 터미널에서 확인해보면 undefined가 출력되는 것을 확인할 수 있다. 이는 요청 body에 포함시킨 데이터를 서버 측에서 파싱하지 못하기 때문에 발생한다. 이를 해결하기 위해서는 요청 body에 포함된 데이터의 형식을 서버가 알 수 있도록 지정해주어야 한다.

/* ./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(express.json());
app.use(express.urlencoded({ extended: true }));

app.use('/', home);

module.exports = app;

위의 코드에서 아래와 같이 적용한 미들웨어는 요청 데이터를 json 형태로 받도록 설정한 코드이다. 기존에는 body-parser라는 별도의 라이브러리를 통해 미들웨어를 설정하여야 했는데, express의 4.16 버전 이후부터는 해당 모듈이 포함되어 있기 때문에 아래와 같이 입력하면 된다.

app.use(express.json());
app.use(express.urlencoded({ extended: true }));

다시 localhost:3000/login으로 접속 후 로그인 버튼을 클릭하고, 서버측 터미널에서 결과를 확인하면 아래와 같이 object 형태로 출력되는 것을 볼 수 있다.

[nodemon] restarting due to changes...
[nodemon] starting `node ./bin/www.js`
express server running on port 3000
{ id: '', passwd: '' }

Model로 구현한 로그인 기능

🔗 소스코드 : v1.0.6-model1

위에서 클라이언트, 서버 간 데이터 전송 기능까지 구현하였으니, 이번에는 로그인, 회원가입 기능을 구현하기 위한 모델을 OOP(Object-Oriented-Programming, 객체지향 프로그래밍)로 구현해보도록 하겠다.

데이터베이스 생성

로그인, 회원가입 시 데이터를 서버에만 저장한다면, 서버가 재실행되는 경우 모든 데이터가 사라지게 된다. 따라서, 데이터베이스가 필요한데, 본 프로젝트에서는 json 파일을 데이터베이스로 사용하였다.

/* ./app/src/database/users.json */
[
    {
        "id": "choewy",
        "name": "최원영",
        "passwd": "choewy123"
    },
    {
        "id": "test",
        "name": "테스트",
        "passwd": "test123"
    },
    {
        "id": "admin",
        "name": "관리자",
        "passwd": "admin123"
    }
];

이를 관계형 DB로 비유하자면, 위의 파일이 보관된 database는 데이터베이스를 의미하고, users.json은 테이블, users.json의 배열 안에 있는 각각의 object를 행, object의 key를 field, object의 value를 데이터라고 할 수 있다.

UserStorage 구현

데이터베이스의 users.json 파일과 연결하여 데이터 입/출력을 담당하는 객체를 class로 구현하였다. 먼저, users.json에서 사용자의 id와 일치하는 사용자의 정보를 출력하는 메소드를 다음과 같이 작성하였다.

/* ./app/src/models/UserStorage.js */

"use strict";

const fileSystem = require('fs').promises;
const filePath = './src/database/users.json';

class UserStorage {
    static #getUserInfo(data) {
        const users = JSON.parse(data);
        const userInfo = users.find(user => user.id === id);
        if (!userInfo) return {};
        return userInfo;
    };

    static async getUserInfo(id) {
        return fileSystem.readFile(filePath)
            .then(data => {
                return this.#getUserInfo(data, id);
            })
            .catch(console.error);
    };
};

module.exports = UserStorage;

getUserInfo 메소드를 통해 입력받은 id를 은닉 함수인 #getUsersInfo로 넘겨주어 id에 해당하는 object만 찾아오도록 하였든데, 코드의 가독성을 위해 getUserInfo 메소드를 은닉 함수 #getUserInfo로 분리시켰다. fileSystem으로 데이터를 처리하는 경우 Promise 객체를 반환하므로, 비동기 처리를 위해 async 메소드로 적용하였다.

User 모델 구현

이번에는 위에서 구현한 UserStorage 클래스의 getUserInfo로 동작하는 User 클래스를 구현해보도록 하겠다. User 클래스는 외부에서 인스턴스로 사용하도록 할 것이므로, 생성자를 통해 User의 데이터를 전달받고, 이를 통해 로그인 시 검사를 수행하도록 구현하였다.

/* ./app/src/models/User.js */

"use strict";

const UserStorage = require("./UserStorage");

class User {
    constructor(body) {
        this.body = body;
    };

    async login() {
        const body = this.body;
        const data = await UserStorage.getUserInfo(body.id);

        if (Object.keys(data).length) {
            if (data.id === body.id && data.passwd === body.passwd) {
                return { success: true };
            }
            return { success: false, message: "비밀번호가 일치하지 않습니다." };
        }
        return { success: false, message: "존재하지 않는 아이디입니다." };
    };
};

module.exports = User;

코드를 보면 알 수 있듯이, 로그인 검사 결과 인증에 성공하면 { success : true }를 반환하지만, 실패하면 { success : false }와 함께 그에 맞는 메시지 문자열까지 함께 반환하도록 하였다.

Controller와 연결

위에서 구현한 모델을 컨트롤러와 연결하여 로그인 기능이 잘 동작하는지 테스트하기 위해 아래와 같이 코드를 수정해주자.

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

// ... 생략 ...

const process = {
    login: async (req, res) => {
        const user = new User(req.body);
        const response = await user.login();
        return res.json(response);
    },
    signup: (req, res) => {
        console.log(req.body);
    }
};

module.exports = { render, process };

클라이언트 측 응답 결과 처리

위의 코드에서 보았듯이 서버는 클라이언트의 요청에 대한 처리가 완료되면 object 형태로 응답 결과를 클라이언트에게 보내준다. 클라이언트 측에서 응답 결과에 따른 동작 처리를 하기 위해 아래와 같이 코드를 수정한다.

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

// ... 생략 ...

function login() {
    const req = {
        id: id.value,
        passwd: passwd.value
    }
    fetch("/login", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify(req)
    })
        .then(res => res.json())
        .then(res => {
            const { success, message } = res;
            if (success) return location.href = '/';
            alert(message);
        })
        .catch(console.error);
};

서버로부터 받은 응답 중 success가 true인 경우 '/'으로 이동하고, 그렇지 않으면 message를 알림창에 출력하도록 하였다. 또한, 로그인 과정에서 예기치 못한 오류가 발생하는 경우를 대비하여 catch로 예외처리를 해주었다.

쉬어가며

OOP로 모델을 구현하였고, 해당 모델로 로그인 기능을 처리할 수 있도록 코드를 작성해보았다. 또 댜시 글이 너무 길어졌기 때문에 나머지 부분은 다음 글에 작성하겠다.