사용자 추가

Aug 8, 2015 • Node.js서버강좌 - 4


들어가는 말

사용자가 처음 게임에 접속했을 때 아이디를 입력받아 서버에 기록하게 된다.

이때 서버가 어떻게 데이터베이스에 데이터를 기록하고 가입 요청 처리를 완료하는지 살펴보게된다.

ORM인 Sequelize.js를 사용해보고 Promise 패턴에 대해서도 배워본다.


라우터(Router) 살펴보기

라우터는 특정 URL로 들어오는 요청(HTTP Request)를 처리하는 패턴이다. 예를 들면 Sequelize.js를 다룬 이전 포스트 URL을 부분별로 나눠보면 아래 표와 같다.

프로토콜 호스트 패스(path)
http:// totuworld.github.io /2015/08/07/sequelize

위 표는 호스트에 패스에 해당하는 데이터를 요청하게 되고 이를 라우터가 해석하여 필요한 데이터를 전송하게 된다(여기서는 HTML과 CSS같은 웹 브라우저에 표시할 데이터이다).

express의 라우터는 패스와 콜백 메서드로 이뤄진다.

router.METHOD( /* 패스(path) */ , /* 콜백 메서드(callback method) */ );

앞의 METHOD는 HTTP 메서드(get, post, put 등)를 제공한다. 패스는 URL의 패스를 입력하면 된다. 콜백 메서드가 실제로 모든 일을 처리하게 된다.


가입 요청 처리

게임을 실행했을 때 사용자 아이디의 유무를 확인힌다. 이때 아이디가 없는 경우 게스트 로그인이나 소셜 미디어를 통한 가입을 권유해야 한다.

위와 같은 과정이 클라이언트에서 실행된 후 실제적인 가입 요청이 왔다고 가정하자.

웹 어플리케이션에서는 어떤 일을 수행해야할까?

크게 아이디 중복 여부 확인과 기본 정보 등록으로 나눌 수 있다. 이런 내용을 routes/users.js 파일에 추가하자.

먼저 스크립트 앞에 데이터베이스 모델을 사용할 수 있도록 아래 코드를 추가한다.

var models = require('../models');

이제 models._tableName_형식으로 각 테이블에 데이터를 읽고 쓸 수 있다.

그리고 가입 요청 처리하는 로직을 작성해 넣는다.

/* POST 사용자 아이디를 등록할 때 사용한다. */
router.post('/add/:userID', function(req, res) {

  //아이디 중복 확인
  models.usercore.findOne({where:{id:req.params.userID}})
  .then(function(findUserCoreData) {
    if( (findUserCoreData === null || findUserCoreData === undefined) === false ) {
      //err:아이디가 중복되는 경우.
      res.send('exist');
      return;
    }
    
    //아이디를 등록한다.
    models.usercore
    .create({id:req.parmas.userID, gems:20, coins:1000, hearts:5})
    .then(function(createdUserCore) {
      //완료 결과 전송.
      res.send('done0'+createduserCore.no);
    });
  });
  
});

5번째 줄: Sequelize.js가 제공하는 findOne 메서드1를 사용했다. {where:{/* 조건 */} 형태로 옵션을 넣으면 콜백으로 입력한 조건과 맞는 usercore 테이블의 행(row)를 리턴한다.

7번째 줄: 리턴된 행이 null이거나 undefined일 때를 확인하여 false이면 아이디가 중복인 것으로 판단한다.

14번째 줄: 아이디가 중복되지 않으므로 create 메서드2로 아이디를 등록한다. 필요한 컬럼을 {/* 초기값 */}형태로 입력하면 새 행을 만들고 콜백으로 만들어진 행을 리턴한다.

위 과정을 순서도로 나타내면 아래와 같다.

사용자 추가 순서도1

Promise 패턴 적용

20줄 정도되는 코드에 벌써부터 Node.js의 단점인 콜백 지옥(callback hell)나타났다. 해석도 어렵고 다시 코드를 수정할 때도 길을 잃기 좋다.

node.js는 기본적으로 모든 I/O를 비동기로 처리하고 있고 많은 기능들도 비동기적으로 동작하고 있습니다.

덕분에 콜백 안에서 다시 콜백을 호출하는 일이 많다.

이렇게 작성하면 에러에 대한 처리와 개발 코드에 대한 관리가 매우 어려워진다.

이를 완화하기 위해서 Promise 패턴Async.js 모듈을 활용할 필요가 있다.

여기서는 Promise 패턴을 사용한다.3

주의 : Node.js >= 4.x 이상 사용

Promise 패턴을 활용하여 코드를 수정해보자.

/* POST 사용자 아이디를 등록할 때 사용한다. */
router.post('/add/:userID', function (req, res) {
    
    //중복을 체크
    function CheckExistID(findUserCoreData) {
        return new Promise(function (resolve, reject) {
                //중복인가?
                if (!(findUserCoreData == null || findUserCoreData == undefined)) {
                    //중복이 아니다.
                    resolve();
                }
                else {
                    //중복이다.
                    reject();
                }
            });
    }
    
    //usercore에 추가된 신규 row를 기준으로 결과 생성 
    function MakeCreateResult(createdUserCore) {
        return new Promise(function (resolve, reject) {
            resolve(`done0${createdUserCore.no}`);
        });
    }
    
    //usercore 테이블을 id로 검색
    models.usercore.findOne({ where: { id: req.params.userID } })
        //중복 사용중인 체크
        .then(CheckExistID)
        //중복 아니므로 아이디 등록.
        .then(function () {
            return models.usercore
                .create({ id: req.parmas.userID, gems: 20, coins: 1000, hearts: 5 });
        })
        //생성에 관한 결과 전달
        .then(MakeCreateResult)
        //중복 일 때 예외처리
        .catch(function (err) {
            return new Promise(function (resolve, reject) {
                resolve('exist');
            });
        })
        //최종적으로 결과를 전송한다.
        .then(function (result) {
            res.send(result);
        });
});

문법이 생소해서 그렇지 보기 편해졌다.

5 번 줄 : CheckExistID 메서드는 usercore테이블에서 id로 검색된 데이터를 바탕으로 중복 여부를 확인한다.

20 번 줄 : MakeCreateResult 메서드는 신규 생성된 usercore테이블의 row에서 id 컬럼값을 읽어서 결과에 전달한다.

27 번 줄 : CheckExistID, MakeCreateResult메서드를 제외하면 전체 흐름이 직선화 되어서 보기 편하다.

Promise 패턴 설명

promise는 단 한번 성공만 하거나 실패만 합니다. 두번 성공하거나 실패할 수 없으며 성공에서 실패로 혹은 반대로 변경될 수도 없습니다. - 자바스크립트 Promise 중 발췌

Promise는 성공하거나 실패한다.

그런 사건이 발생하게 되면 프로미스의 then 메서드에 의해 대기중이던 관련 핸들러가 호출됩니다. - Promise 중 발췌

정확히는 성공했을 때 then이 호출되고 실패하면 catch가 호출된다.

그리고 아래와 같은 특징 덕분에 체이닝(연결해서 사용)할 수 있다.

Promise.prototype.then과 Promise.prototype.catch 메서드는 프로미스를 반환하기 때문에 이를 체이닝 시킬 수 있습니다. - Promise 중 발췌

또한 catch는 Promise의 체이닝 중 실패(rejected)하게 되면 그 아래에 많은 then 메서드가 있어도 건너뛰고 catch 메서드를 실행한다.

이를 잘 설명한 이미지가 있으니 참조바란다.

promise-flow

녹색선을 따라가면 처리된(fulfill) promise들이고 빨간선을 따라가면 거부(reject)된 promise입니다.

앞서 살펴본 코드가 Promise를 통해서 어떻게 흘러가는지 나타내면 아래와 같다.

promise-flow

위 그림에서 붉은 점선은 sequelize에서 모델을 통해 테이블에 쿼리를 실행하다가 실패(rejected)했을 때 catch로 이동하는 것을 표현한 것이다. CheckExistID 메서드만을 생각하고 작성된 catch이므로 이런 오류까지 대응하려면 중간 중간 catch를 배치시켜야한다.


업그레이드 기록 테이블 추가

간단한 가입 요청 처리가 완료되었다.

이제 사용자의 공격, 방어 레벨등을 기록하는 userupgrade 테이블에도 데이터를 추가하도록 해보자.

먼저 userupgrade테이블은 총 4개의 컬럼으로 구성된다.

Name Type Default Attributes Index AutoIncrease
no INTEGER - UNSIGNED PRIMARY -
attLv INTEGER 1 UNSIGNED - -
defLv INTEGER 1 UNSIGNED - -
moneyLv INTEGER 1 UNSIGNED - -

no는 usercore의 no와 같은 값이 할당된다.4 나머지 컬럼은 공격, 방어, 코인 획득 레벨을 나타내게 된다.

모델 추가

models/userupgrade.js파일을 생성하고 아래 링크와 같은 내용으로 입력한다.

userupgrade.js

users.js 수정

그리고 users.js파일의 MakeCreateResult 메서드 아래쪽에 CreateUserUpgradeRow메서드를 추가한다.

//userupgrade테이블에 업그레이드 기록할 행을 추가한다.
    function CreateUserUpgradeRow(createdUserCore) {
        return new Promise(function (resolve, reject) {
            models.userupgrade
            .create({ no: createdUserCore.no })
            .then(function(createUserUpgrade){
                resolve(createdUserCore);
            });
        });
    }

CreateUserUpgradeRow 메서드는 usercore 테이블에 생성된 정보 중 no컬럼 값을 이용해서 새로운 row를 생성하는 일을 한다.

주의 : 위 코드에서는 MakeCreateResult 메서드를 수정하지 않으려고 create후 resolve로 값을 전달했으나 사실 아래처럼 직접 전달되도 무방하긴 하다.

return models.userupgrade.create({ no: createdUserCore.no }); 

CreateUserUpgradeRow 메서드를 Promise 체인 중 usercore테이블에 데이터를 생성하던 부분 아래에 위치시키면 된다.

//앞부분 생략
        
        //중복 아니므로 아이디 등록.
        .then(function () {
            return models.usercore
                .create({ id: req.parmas.userID, gems: 20, coins: 1000, hearts: 5 });
        })
        //업그레이드 정보를 기록할 row를 생성한다.
        .then(CreateUserUpgradeRow)
        //생성에 관한 결과 전달
        .then(MakeCreateResult)
        
        //뒷부분 생략

9 번째 줄 : usercore테이블에 아이디(userID)가 등록되고 then 메서드를 통해 CreateUserUpgradeRow 메서드르 바로 호출되게 된다.


맺음말

아주 간단하지만 사용자 아이디를 등록하는 과정을 살펴봤다. 핵심은 데이터베이스를 확인하고 기록하는 것이다.

이런 과정을 반복하다보면 더 효과적이고 재사용성이 높은 코드를 작성하게 될 것이라고 본다.


  1. findOne 메서드는 데이터베이스에서 조건에 맞는 하나의 데이터를 찾을 때 사용한다. 

  2. create 메서드는 테이블에 행을 추가할 때 사용한다. 

  3. Promise 패턴을 기존에는 사용하기 어려웠으나 Node.js 4.x 버전 이상에서 ES2015의 몇몇 기능을 구현하게 되었다. 그중 Promise가 포함되어 별도의 모듈 설치없이 진행이 가능하다. 

  4. 책에서는 user란 컬럼명을 사용했지만 여기서는 no로 통일했다. 


Buy me a latteBuy me a latte