티스토리 뷰

Node.js 사용자가 만드는 웹게임 만들기 이야기 - 2

주의사항

이 글은 제 능력부족으로 누구에게 정보를 알려주거나 가르치는 목적의 글이 아님을 알려드립니다. 그냥 일기라 생각하고 봐주시면 될 거 같습니다.

  • 중간중간에 뭔가 넣는 이유는 그래도 다른 분들이 보시는거니까 그냥 좀 쓰는겁니다.

어제 어디까지 했더라?

1편 링크
어제 프로젝트 만들기 끝났습니다. 그렇습니다. 날로 먹었습니다. 이제 조금씩 살을 붙여보려고 합니다.

이번에 할 일

  1. config 셋팅
  2. DB셋팅 + 데이터 모델링(일부)
  3. router 분리
  4. 사용자 인증 도입
    얘는 제가 지치지 않았다면 도전해보겠습니다.

시-작

1. config 셋팅

제가 공개적인 프로그래밍을 하는 건 이번이 처음입니다. 이전까지는 그냥 설정파일도 막 커밋같은 곳에 올리고 했습니다. 그런데 이거 문제가 있습니다. 실제 github에 오픈으로 자신의 s3 계정이 들어있는 config 파일을 올렸다가 월 수백만원 요금이 나오지를 않나 등등 다양한 사건들이 벌어졌었습니다. 그래서 설정파일을 살짝 덮어씌워보려고 합니다.

node-config

node-config 라는 라이브러리가 있습니다. 이 라이브러리의 특징은 설정파일을 각 배포별로 분리할 수 있다는 것입니다.

사용해봅시다. 먼저 설치해주는 건 기본입니다.
yarn add config
이름 겁나 비범합니다. 아무튼 설치했으면 config 를 만들고 기본값 셋팅을 해줍시다. config 파일을 만들고 그 안에 default.json 을 만들어줍시다. 이 라이브러리의 강점은 yaml, plain js 다 제공해주지만 전 json이 익숙하니까요.
이 라이브러리는 NODE_ENV에 따라 어떤 녀석을 default 에 덮어쓸 지 결정합니다. 따라서 실행할 때 NODE_ENV=blahblah node ~~ 이렇게 실행해야 한다는 것을 의미합니다.
실제로 덮어씌워지는 지 확인해보기 위해 config/localpc.json 도 만들어보겠습니다. 그리고 실행할 때 NODE_ENV=localpc 라고 해주면 되겠지요. 이제 이 설정은 DB 셋팅할 때 유용하게 쓰일 것입니다.

주의사항이 있습니다. 이 프로젝트의 경로에서 앱을 실행하지 않다면 config 디렉터리의 경로를 NODE_CONFIG_DIR 에 잡아주셔야 합니다. 기본값은 ./, ../ 입니다.

2. DB셋팅

2.1. mongoDB란? 를 설치해보자

mongoDB라는 NoSQL DSBMS가 있습니다. 특이하게도 이 DBMS의 타겟은 mySQL 사용자입니다. 물론 CAP 이론에서 두 DB는 다르게 취급되니 엄밀한 의미의 경쟁은 안됩니다만 mongoDB는 입문하기 쉽고 node.js와 상성이 좋아서 인기가 많습니다. DB-ENGINES에서 PostgreSQL과 엎치락 뒤치락하고 있습니다.

2.2. mongoDB를 써보자

SQL 만큼은 아니지만 mongoDB로 이곳 저곳에서 지원해주고 있습니다. mongoLab이라거나 Atlas라거나 등등이요. 물론 DB는 네트워크가 먼 곳에 있다면 성능이 치명적이니 로컬에 설치해서 확인해보겠습니다. 아, 물론 전 이미 깔아놨습니다. 현재 mongoDB의 최신 버전은 3.4.2 입니다. 물론 이건 일기라서 설치법은 적지 않겠습니다.

2.3. mongoose 설정

mongodb에서 node.js 드라이버를 제공해주는데 쓸 사람은 잘 쓰는데 전 영 안되네요. 그래서 mongoose를 사용하기로 했습니다. 얘는 ODM(Object Document Mapper)이라고 불리는데요. ORM이랑 같은 의미라고 생각하시면 됩니다. 닼큐멘트냐 릴뤠이션이냐의 차이입니다. 그 밖에 mongoose는 다양한 기능을 제공해주므로 강추합니다.
mongoose는 똑같이 yarn add mongoose 해주면 됩니다.

2.4. mongoose와 DB 모델링

이제 사용자에 대한 모델링을 해보겠습니다. document형 DB는 모든 데이터를 document로 관리하고 그 document를 불러오는 방법입니다. mongodb는 json을 개조한 bson을 씁니다. 네, json 비스무리한 걸 쓰다보니 javascript와의 궁합이 예술입니다.
이 document를 모으는 곳을 collection이라고 부르는데 얘를 일종의 table이라고 생각하시면 편합니다.
쉽게 말하면 이렇게 됩니다.
MongoDB <-> RDBMS
database <-> database
collection <-> table
document <-> record(or row)
key <-> column
느낌입니다.

이제 적당히 스키마를 설계해보겠습니다. 스키마는 이러한 DB 내의 항목들에 대해 정의한 명세입니다.

MongoDB의 장점 중 하나가 Schemaless 입니다. 하지만 이 이야기는 스키마의 변경이 쉽다는 이야기이지 스키마를 개나 줘버리라는 이야기가 아닙니다!

mongoose의 강점은 mongodb의 schemaless 속성에 의해 관리하기 힘든 DB명세를 코드로 쉽게 관리할 수 있게 도와줍니다. 그래서 바로 mongoose 라이브러리를 이용한 model을 만들겠습니다.

이제 직접 로그인해서 들어올 수 있는 사용자를 모델링해보겠습니다. model/users.js 라는 파일을 생성하였습니다.

'use strict';

const mongoose  = require('mongoose');

const schema = mongoose.Schema({
  //_id에 대한 명세는 따로 하지 않습니다. 알아서 만들어줍니다.
  loginId: String,
  password : String,
  nickname : String,
  created : {
    type : Date, "default" : Date.now
  },
  updated : {
    type : Date, "default" : Date.now
  },
  removed : {
    type : Boolean, "default" : false
  }
});

const Model = mongoose.model('app_users', schema);

module.exports = Model;

_id는 이 문서의 고유한 ID입니다. 이건 DB 레벨에서 알아서 고유한 UUID(ObjectId)로 만들어주니 신경쓰지 않으셔도 됩니다.
여기서 mongoose.model 함수의 첫 파라미터는 실제 mongodb에 기록될 컬렉션명입니다. 단수로 써도 mongoose가 알아서 복수로 바꿔버리니 그냥 만들 때부터 복수로 만드세요.
가입할 때 써야하니 loginId와 password를 준비하였습니다. facebook 로그인 등은 어느정도 진도 나간 뒤에 다시 작성하겠습니다.
그리고 상대방에게 보여주는 이름은 nickname, 생성시각 created, 갱신 시각은 updated로 했고 JS의 Date 함수를 기본 값으로 넣도록 하였습니다.
마지막으로 removed는 문서가 지워진 것인지를 알리는 flag입니다. 실수로 delete로 날려먹는 상황을 막기 위함입니다.
실제 이 녀석이 어떻게 쓰이는지는 4. 사용자 인증 도입에서 알아보겠습니다.

이제 DB에 실제 접속을 해보겠습니다. app.js의 앞부분에 다음과 같이 넣었습니다.

process.env.NODE_CONFIG_DIR = process.env.NODE_CONFIG_DIR || __dirname + '/config';
const config = require('config');
const mongoose = require('mongoose');
mongoose.connect(config.mongoURL);

근데 mongoURL이 어디서 튀어나왔냐고요? config/localpc.json 파일을 건드려줍시다.

{
  "mode" : "development",
  "mongoURL" : "mongodb://localhost/{DB명}"
}

{DB명}에는 사용하고 싶은 DB의 이름을 넣으시면 됩니다. 실제 mongodb는 mongoose.connect 를 하는 시점에서 접속을 시도합니다. mongoose에서 내부적으로 명령을 큐잉하기 때문에 연결이 실제로 이뤄지기 전에 오는 요청도 처리를 합니다. 불안하다면 지연을 주시기 바랍니다. 아마 callback이 있을겁니다.

3. router 분리

앞으로 할 일이 태산인데 이 모든 일을 app.js에서 하려고 하면 양에 질려버릴겁니다. 그래서 API의 리스트 및 구현을 다른 파일로 옮기려고 합니다.
먼저 routes/index.js 파일을 만들고 다음과 같이 넣었습니다.

'use strict';

const router = require('koa-router')();

router.get('/', async function(ctx, next) {
  ctx.body = 'hello, world!';
});

module.exports = router;

어제 썼던 app.js의 appRoutings 함수에 있던 녀석 그냥 복사한겁니다. 이제 app.js도 고쳤습니다.

...
const router = require('./routes');
...
function appRoutings(app) {
  app.use(router.routes()).use(router.allowedMethods());
  return app;
}
...

이제 app.js를 실행해보겠습니다. 아까 node-config 셋팅을 했기 때문에 NODE_ENV를 잡아야 하는 점 참고하시기 바랍니다.
물론 이건 일기라 실행 결과니 뭐니 이런 거 적지 않겠습니다(퍽)

4. 사용자 인증 도입

이제 드디어 코딩을 하게 되겠네요. 사용자의 인증을 도입할겁니다. 그 전에 먼저 회원가입부터 해야하지 않겠습니까? routes/users.js를 만들겠습니다. 여기에 사용자와 관계된 API들을 몰아넣을겁니다. 일단 이렇게 써보겠습니다.

'use strict';

const router = require('koa-router')();

/*
 * @api {post} 회원가입
 * @apiParam {String} loginId 사용자가 로그인할 때 사용하는 ID입니다
 * @apiParam {String} password 사용자가 사용할 비밀번호입니다.
 * @apiParam {String} passwordRepeat 비밀번호를 2번 입력받습니다.
 * @apiParam {String} nickname 사용자의 닉네임을 정합니다.
 */
router.post('/', async function(ctx) {
  let body = ctx.request.body;

  console.log(body);
  ctx.body = body;
});
module.exports = router;

아직 제대로 만드는 건 아니기 때문에 일단 대충 틀만 만들었습니다. 지금 상황은 그냥 body에 들어오는 값을 그대로 리턴하는 겁니다. routers/index.js 파일을 수정해줘서 이 라우터를 등록을 합시다.

...
const users = require('./users');
...
router.use('/users', users.routes()).use(users.allowedMethods());
...

koa-router가 이게 좋습니다. express 처럼 라우터가 다른 라우터를 불러오고 이를 연결할 수 있도록 하여 모듈화를 도와줍니다.

실제 테스트는… 음… postman 으로 적당히 하고 있습니다. 나중에 이것도 코드 짜서 자동화하도록 노력해야죠 ㅠㅠ cURL로 하긴 귀찮잖아요 ㅋㅋ

다시 routes/users.js 를 수정하겠습니다.

'use strict';

const router = require('koa-router')();
const User = require('../model/users');

const utils = require('../lib/utils');
const error = require('../lib/error');

/*
 * @api {post} 회원가입
 * @apiParam {String} loginId 사용자가 로그인할 때 사용하는 ID입니다
 * @apiParam {String} password 사용자가 사용할 비밀번호입니다.
 * @apiParam {String} passwordRepeat 비밀번호를 2번 입력받습니다.
 * @apiParam {String} nickname 사용자의 닉네임을 정합니다.
 */
router.post('/', async function(ctx) {
  let body = ctx.request.body;

  //파라미터 검사 (loginId, password, passwordRepeat, nickname)
  if(!utils.isIncludeAll(body, ['loginId', 'password', 'passwordRepeat', 'nickname'])) {
    return ctx.body = error.create('INVALID_PARAMS');
  }

  //비밀번호 유효성 검사
  if(!utils.isPassword(body.password)) {
    return ctx.body = error.create('INVALID_PARAMS', '비밀번호 양식이 맞나 확인해주세요');
  }

  //password == passwordRepeat ?
  if(body.password != body.passwordRepeat) {
    return ctx.body = error.create('INVALID_PARAMS', '비밀번호가 일치하지 않습니다');
  }

  try {
    //loginId 중복 검사
    let isDuplicated = await User.findOne({loginId : body.loginId});
    if(isDuplicated) {
      return ctx.body = error.create('INVALID_PARAMS', '중복된 사용자가 있습니다');
    }

    //create user
    let user = new User({
      loginId : body.loginId,
      password : body.password,
      nickname : body.nickname
    });
    await user.encryptPassword();
    console.log(user);
    await user.save();

    //return OK
    return ctx.body = error.success({
      ok : true
    });
  } catch(e) {
    console.error(e);
    return ctx.body = error.create('ERROR');
  }
});
module.exports = router;

전 API 틀을 만들어놓고 내용을 채우는 식으로 작업합니다. 아직 저 API는 작동하지 않을겁니다. 아마 실행하면 바로 에러를 뱉지 않을까요? 그래서 이제 하나둘씩 에러를 잡도록 하겠습니다.

솔직히 이 블로그의 코드를 좀 표절했습니다 ㅠㅠ 살려주세요

lib/utils.js

'use strict';

exports.isIncludeAll = function(obj, keys) {
  for(let i=0,key;key=keys[i];++i) {
    if(obj[key] != null) {
      return false;
    }
  }
  return true;
};

//http://stackoverflow.com/questions/19605150/regex-for-password-must-be-contain-at-least-8-characters-least-1-number-and-bot
exports.isPassword = function(pwd) {
  //8자 이상에 최소 1글자의 영문자, 1글자의 숫자가 들어가야 함
  return /^(?=.*[A-Za-z])(?=.*\d)[A-Za-z\d]{8,}$/.test(pwd);
};

여기서 좀 독특한 for문을 사용했는데요. i=0,item;item=array[i];++i 입니다. 이는 변수도 표현식이라는 JS의 철학 덕택에 가능한 트릭입니다. 성능도 가장 빠릅니다만 이 경우에 가장 큰 문제는 배열 안에 false로 인식할 수 있는 데이터(0, false, ‘’, null, undefined 등)이 들어갈 경우 중간에 break가 걸려버립니다. 그러므로 무조건 배열 내에 값이 있다고 보장될 경우에만 사용하시기 바랍니다.
그리고 비교문에서 value != null 만 썼는데 이는 != undefiend 도 포함합니다. 사실 !value 가 더 짧고 간결하지만 이는 위의 경우처럼 0, false, ‘’ 가 와도 문제가 생길 수 있으니 사용하지 않았습니다.
비밀번호 체크 루틴은 그냥 스택 오버플로우에서 쓱 가져왔습니다. 답변해주신 분께 정말 감사드립니다.

lib/error.js

'use strict';

const CODE_STR_MAP = {
  'SUCCESS' : {no : 20000, msg : null},
  'FAILURE' : {no : 40000, msg : '정해지지 않은 실패입니다'},
  'ERROR' : {no : 50000, msg : '서버 내부 오류가 발생하였습니다'},
  'INVALID_PARAMS' : {no : 40001, msg : '파라미터가 올바르지 않습니다'}
};

exports.success = function(data) {
  const SUCCESS = CODE_STR_MAP['SUCCESS'];
  return {
    code : 'SUCCESS',
    no : SUCCESS.no,
    msg : null,
    data : data
  };
};

exports.create = function(codeStr, msg) {
  const DATA = CODE_STR_MAP[codeStr] || CODE_STR_MAP['FAILURE'];
  return {
    code : codeStr,
    no : DATA.no,
    msg : msg || DATA.msg,
    data : null
  };
};

그리고 line 47에 있는 user.encryptPassword() 는 mongoose 에서 제공해주는 함수가 아닙니다. 즉, 제가 모델 함수로 구현해야 한다는 의미입니다. 그리고 loginId는 유니크해야 합니다. unique 인덱스까지 추가하겠습니다.

```javascript
...
schema.index({loginId : 1}, {unique : true});
...
schema.methods.encryptPassword = function() {
  this.password = 'ENCRYPTED_PASSWORD';
};
...

아직 암호화 라이브러리를 뭘 쓸지는 안정해서 그냥 임의로 밀어넣었습니다. 일단 여기까지 했더니 실행은 되네요. 현재까지 약점이 몇 개 존재하지만(비밀번호를 평문으로 보내다니!) 일단 구색은 갖췄습니다.

4.1. 암호화는 뭐로할까요?

비밀번호는 역방향으로 돌아갈 수 없는 해시함수를 사용해야 합니다. 보통 sha2(sha256, sha512) 를 많이 씁니다. md5와 sha1은 해독이 가능하기 때문입니다. 물론, 더 좋은 방법이 하나 있습니다. bcrypt라는 놈인데요. 얘를 사용하면 좀 더 안전해집니다.(암호화, 해시 할 때마다 값이 바뀝니다. 무서운 녀석이죠) 써봅시다.
yarn add bcrypt
이 녀석은 compile이 필요합니다. 컴파일할 환경이 준비되지 않았다면 에러가 발생합니다. 그 경우엔 다른 라이브러리를 사용하시면 됩니다. 똑같이 생겼는데 native가 아닌 순수 Javascript를 사용합니다. 물론 속도는 좀 포기해야죠. bcryptjs 입니다. require('bcryptjs') 이렇게 불러오는 거 빼면 사용법은 동일합니다. 이제 이렇게 써보겠습니다.

...
const bcrypt = require('bcrypt');
...
schema.methods.encryptPassword = function() {
  return bcrypt.hash(this.password, 10).then((encrypted) => {
    this.password = encrypted;
    return Promise.resolve(encrypted);
  });
};
...

bcrypt는 Promise를 지원하기 때문에 그냥 return 시켜버리고 await user.encryptPassword(); 처럼 await로 때워버릴 수 있습니다. 그리고 여기서 보통 함수의 콜백에는 this가 먹히지 않는데 얘는 this로 때워버렸습니다. ES6에는 arrow function 이라는 개념이 있는데 얘는 간단하게 말하면

function(...) { ... }.bind(this);

와 같은 의미라고 보시면 됩니다.

일단 전 중간에 콘솔 로그로 내부값 변화를 좀 볼겁니다. debug모듈을 사용하면 debug 상태일때만 보게 할 수 있는데 그냥 하겠습니다.
비밀번호가 $2a$10$3xKPlEDybl9gjUEpmxn6i.s80ZVlH77Y1s4Z40g7DvAzRA4Wtk40C 이딴 식으로 바뀐 것을 볼 수 있습니다. 물론 그냥 평문을 그대로 넣으면 털려버릴 우려가 있으므로 salt(문자열의 앞뒤에 특수한 문자열을 더 추가하는 것)를 추가해주면 더 안전하게 할 수 있습니다. 물론 전 패스하겠습니다.

5. 로그인

로그인까진 하고 끝내겠습니다. 안하면 응아 하고 뒤 안닦은 느낌이 들 거라는 생각이 드네요. API를 하나 더 추가해보겠습니다. routes/users.js 파일에 다음과 같이 추가하겠습니다.

/*
 * @api {post} 토큰 발급 - 로그인 같은 놈임
 * @apiParam {String} loginId 계정입니다
 * @apiParam {String} password 비밀번호입니다.
 */
router.post('/_token', async function(ctx, next) {
  let body = ctx.request.body;

  //파라미터 검사
  if(!utils.isIncludeAll(body, ['loginId', 'password'])) {
    return ctx.body = error.create('INVALID_PARAMS');
  }

  try {
    //loginId로 계정 찾아오기
    let user = await User.findOne({loginId : body.loginId});
    if(!user) {
      return ctx.body = error.create('SIGNIN_FAILURE');
    }

    //입력받은 비밀번호와 대조한다.
    let isEqualPassword = await user.validatePassword(body.password);
    if(!isEqualPassword) {
      return ctx.body = error.create('SIGNIN_FAILURE');
    }

    //사용자의 권한을 갖는 인증 토큰을 생성한다.
    let token = await user.createAuthToken();
    return ctx.body = error.success({
      token : token
    });
  } catch(e) {
    console.error(e);
    return ctx.body = error.create('ERROR');
  }
});

아마 그대로 실행하면 오류가 발생할겁니다. 잡아줍시다. node.js의 가장 큰 문제가 실행을 해보기 전까진 무슨 에러가 날 지 모른다는겁니다. 그래서 좋은 IDE를 써야합니다!(살려주세요) 일단 추측으로 잡아봅시다. user.validatePassworduser.createAuthToken 함수를 만들겠습니다.
model/users.js

schema.methods.validatePassword = function(input) {
  return bcrypt.compare(input, this.password).then((isEqual) => {
    return isEqual;
  });
};

schema.methods.createAuthToken = function() {
  return 'AUTH_TOKEN';
};

아직 어떻게 토큰을 만들고 관리할 지에 대한 걸 안정해놔서 그냥 임의로 문자열을 리턴하게 만들었습니다.
그리고 에러 메시지도 하나 더 등록하겠습니다.
lib/error.js

'SIGNIN_FAILURE' : {no : 40002, msg : '아이디 혹은 비밀번호가 올바르지 않습니다'}

이제 돌려봅시다. 일단 작동은 합니다. 그리고 token은 그냥 AUTH_TOKEN 이네요. 사실 여기까지 이 API에는 크나큰 보안 결함이 있습니다만(또 평문으로 보냈어!!!) 이는 HTTPS를 이용하거나 별도로 RSA 알고리즘을 이용하여 클라이언트와 합의를 거쳐 구현하면 해결할 수 있습니다.

그래서 토큰은요? 보통은 세션을 발급하여 세션 ID를 주거나 하는 방법들을 썼는데 요즘은 JWT 라는 녀석을 사용합니다. 이 녀석은 암호화된 토큰에 값을 직접 집어넣는 방법인데 그로 인해 불편한 점도 있지만 편한 점도 있으니 얘를 써보겠습니다.
jwt 라이브러리를 받겠습니다.
yarn add jsonwebtoken
이름 참 화끈하군요. 얘를 이용해서 토큰을 만들어보겠습니다. 토큰의 포맷은 다음과 같이 할 생각입니다.

{
  "userId" : String    //사용자의 ID입니다. user._id 입니다.
}

사실 아무 옵션도 주지 않고 쓰면 이 라이브러리가 멋대로 값을 추가시킵니다. 물론 나중에 상세히 다루도록 하겠습니다.
model/users.js

schema.methods.createAuthToken = function() {
  return new Promise((resolve, reject) => {
    jwt.sign({
      userId : this._id.toString()
    }, config.tokenSecret, {}, (err, token) => {
      if(err) return reject(err);
      return resolve(token);
    });
  });
};

먼저 async/await는 promise, generator에 쓰이는 thunk 등에서 작동합니다. 그런데 이 라이브러리는 promise를 지원하지 않으므로 직접 promise 객체를 생성하여 처리했습니다. 패턴을 이용하여 promisify 모듈을 만들거나 bluebird의 도움을 빌리면 됩니다.
그리고 config라는 놈이 갑툭튀했는데 얘를 불러오는 방법은 2가지가 있습니다.

  1. config를 처음 불러오는 곳에서 global.config = config; 를 코딩해서 global 객체로 등록시킨다.
  2. 그냥 모듈 한 번 더 불러온다.

정답은 딱히 없습니다. 어차피 require로 불러오는 모듈은 싱글턴 패턴으로 취급이 되기 때문에 걔가 객체를 마구 생성하거나 하지 않으면 메모리에 저장이 됩니다. 전 그냥 global에 등록시키겠습니다.
app.js

const config = require('config');
global.config = config;

이 경우에는 딱히 변수 선언을 하지 않으면 global에서 먼저 찾아서 가져오게 됩니다. 브라우저에서 window에 값을 넣는 거와 같은 의미이죠. 물론 변수의 오염이 싫다면 다른 방법을 고안해보시길 바랍니다. 일단 이렇게 하고 실행해보겠습니다. 그리고 각 동작 환경에서 다르게 작동하게 하기 위해 각 환경 설정파일에 값을 넣겠습니다.

과연 어떻게 값이 나올까요? 두근두근!
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiI1OGM2YTc5ZjkyNTU5ZTRmOTA3NmNjMTQiLCJpYXQiOjE0ODk0MTc0MjF9.Qstv4cRPDqVt0wYsJEGnp4s4AW3fYrBBQJobh-JCLLQ 이런 식으로 나옵니다.
이제 이 녀석을 어디다 써먹을 것이냐? 얘를 Authorization 이라는 헤더에 집어넣을 것입니다. 그리고 중간에 인증을 담당하는 미들웨어를 만들 것입니다.
routes/middleware.js

'use strict';

const User = require('../model/users');
const error = require('../lib/error');

async function tokenAuthenticator(ctx, next) {
  const authorization = ctx.request.headers.authorization;

  if(!authorization) {
    return ctx.body = error.create('INVALID_PARAMS', '인증 토큰을 입력하지 않았습니다');
  }

  try {
    const decoded = await User.decodeToken(authorization);
    if(!decoded) {
      return ctx.body = error.create('INVALID_PARAMS', '인증 토큰이 올바르지 않습니다');
    }
    let user = await User.findOne({_id : decoded.userId});
    if(!user) {
      return ctx.body = error.create('NOT_FOUND_USER');
    }
    ctx.user = user;
    next();
  } catch(e) {
    console.error(e);
    return ctx.body = error.create('ERROR');
  }
}
exports.tokenAuthenticator = tokenAuthenticator;

여기서 추가해야 할 건 error.jsNOT_FOUND_USER, model/users.jsUser.decodeToken입니다. model/users.js 를 보겠습니다.

Model.decodeToken = function(token) {
  return new Promise((resolve, reject) => {
    jwt.verify(token, config.tokenSecret, (err, decoded) => {
      if(err) {
        if(err.name == 'JsonWebTokenError') {
          //jwt parse error
          return resolve(null);
        } else {
          return reject(err);
        }
      }
      return resolve(decoded);
    });
  });
}

특이하게도 얘는 Model 만든 뒤에 그 Model에 넣었습니다. static 함수여서 그런겁니다. 그리고 에러 중 JWT 파싱 에러는 사용자 에러로 보여지기에 핸들링을 하나 더 추가하였습니다.
이제 이 middleware를 어떻게 썼는 지 보겠습니다. 아주 간단한 형태의 API인 사용자 정보 보기를 보겠습니다.
routes/users.js

/*
 * @api {get} 사용자 정보 보기
 */
router.get('/', middleware.tokenAuthenticator, async function(ctx) {
  const user = ctx.user;

  return ctx.body = error.success(user);
});

허무하죠… 근데 여기서 주의해야 할 사항이 있습니다. 사용자에게 보여주지 말아야 할 정보도 있다는 의미이죠. password나 __v 같은 정보는 줄 이유가 없죠.

__v는 mongoose 객체에 들어가는 정보입니다. 일종의 스키마 버전같은 녀석입니다. 얘는 다음에 알아보겠습니다
여기서 선택은 2가지입니다.

  1. mongodb query에 projection을 넣는다.
  2. 일단 plain object로 바꾼 뒤 필드를 지운다.

선택은 자유입니다. 전 그때그때 다르게 가도록 하겠습니다.

마무리

일단 여기까지 이번에 하기로 한 모든 작업을 마쳤습니다. 급하게 대충 만든거라 헛점이 많지만 이렇게 급하게 로그인까지 해봤습니다. 마지막으로 지금까지 한 코드를 커밋하겠습니다. 아, config/localpc.json은 제외하겠습니다. .gitignore에 추가하면 됩니다.

다음에 할 일

로그인까지 완료하였으니 이제 본격적으로 게임의 뼈대를 작성하도록 하겠습니다. 그리고 이 코드를 github에 올리도록 하겠습니다. 볼 사람이 있을진 모르겠습니다 ㅋㅋ
TODO.

  1. 코드를 github에 올리기 전 자잘한 작업
    부족한 주석 추가, README.md 추가 등
  2. 코드를 github에 올리기
  3. 게임의 대략적인 플로우 잡기
  4. 3에 따라 다르게 될 거 같습니다.

여기까지 이 글을 질리지 않고 봐주셔서 감사합니다. 제발 이 프로젝트를 터트릴 일이 없기를 바라겠습니다 ㅠㅠ

댓글
댓글쓰기 폼