잊지 않겠습니다.

nodejs (express) + mongodb in Heroku

전부터 알고 있던 cloud base 개발 환경인 heroku를 한번 사용해보기로 했습니다. 일단 nodejs로 만들어진 application 이고, express와 mongodb를 사용하고 있는데. 이와 같은 환경을 무료로 제공해주는 곳 중에서 가장 유명하고, 한번 써보고자 하는 욕구가 강한 서비스여서 접근해보기로 했습니다.

먼저, 배포할 application의 구성입니다.

  • nodejs
  • express
  • mongodb
  • google OAuth 이용

nodejs + mongodb application deploy

Heroku service 가입

email을 등록시켜주면, 가입신청 확인 mail이 날라옵니다. 클릭후, password 설정만 하면 완료됩니다.
완료 후, https://dashboard-next.heroku.com/ 에서 Management Account -> Billing에서 신용카드 정보를 넣어두는 것이 좋습니다. 추후 설치할 addon에서 신용카드 정보가 없으면 진행할 수 없습니다.

Heroku toolbelt 설치

https://toolbelt.heroku.com/ 에서 환경에 맞는 toolbelt를 설치합니다. 저는 지금 linux를 설치하고 있기 때문에, linux 용 toolbelt를 설치한 기준으로 아래 글을 계속 이어가도록 하겠습니다.

설치후, heroku cmd를 사용할 수 있습니다.

Heroku toolbelt 인증 정보 + ssh public key 전송

heroku toolbelt를 설치하면 heroku cmd를 사용해서 인증정보를 입력합니다. heroku service에 가입한 email과 password를 넣어주면 됩니다. 그리고 ssh public key를 heroku에 등록시켜 이제는 heroku email/password가 아닌 ssh key인증을 이용해서 heroku cmd를 사용할 수 있습니다.

$> heroku login
Enter your Heroku credentials.
Email: 
Password:
Could not find an existing public key.
Would you like to generate one? [Yn]
Generating new SSH public key.
Uploading ssh public key /Users/adam/.ssh/id_rsa.pub

Application의 준비

기본적으로 heroku toolbelt를 이용한 application의 배포는 git를 이용합니다. local 또는 http://github.com 에서 제공되는 git repository에 project가 등록되어 있어야지 됩니다.

java의 pom.xml이나 build.gradle과 같은 기능을 하는 것이 package.json 입니다. 여기에 모든 dependency가 기록되어있어야지 됩니다.

그리고, scripts 항목의 start에 반드시 application의 실행 node command를 넣어줘야지 됩니다. 다음은 scripts 항목의 예시입니다.

  "scripts": {
    "start": "node app.js",
    "test": "grunt jasmine"
  }

개인적으로 이부분에서 실수를 한것이, 제 개발환경이 express가 global로 설치가 되어있습니다. 그래서 express가 package.json에 등록되어있지 않았습니다. 이렇게 되는 경우, dependency 문제로 인하여 application이 정상적으로 동작하지 않습니다. 반드시 package.json 정보만으로 application이 구동될 수 있어야지 됩니다.

git repository와 heroku repository 간의 연결

git repository directory안에서 다음 command를 실행합니다.

$>  heroku create --http-git
Creating sharp-rain-871... done, stack is cedar-14
http://sharp-rain-871.herokuapp.com/ | https://git.heroku.com/sharp-rain-871.git
Git remote heroku added

위 command의 결과, git는 remote repository를 하나 더 만들게 됩니다. 이 remote repository에 우리가 appliation을 배포하면 heroku에서 running되는 application을 구성하게 되는 것입니다.

위 console 창의 결과는 sharp-rain-871이라는 application의 이름을 갖게 됩니다. 이 이름은 unique 한 결과이며, 변경을 원하는 원하는 경우, heroku apps:rename 명령어를 통해 변경할 수 있습니다. 모든 action은 위 git repository directory 안에서 실행되어야지 됩니다.

git repository에서 heroku repository로 deploy

git repository directory안에서 다음 command를 실행합니다.

$> git push heroku master
Counting objects: 7, done.
Delta compression using up to 4 threads.
Compressing objects: 100% (3/3), done.
Writing objects: 100% (3/3), 386 bytes | 0 bytes/s, done.
Total 3 (delta 2), reused 0 (delta 0)
remote: Compressing source files... done.
remote: Building source:
remote: 
remote: -----> Node.js app detected
remote: -----> Requested node range:  0.10.33
remote: -----> Resolved node version: 0.10.33
remote: -----> Downloading and installing node
remote: -----> Restoring node_modules directory from cache
remote: -----> Pruning cached dependencies not specified in package.json
remote: -----> Exporting config vars to environment
remote: -----> Installing dependencies
remote:        
remote:        > sadari-app@1.0.0 postinstall /tmp/build_efb71b073a7c6b8520ad4613bd4ca2a3

deploy가 마치면 heroku open command를 통해서 browser를 이용해서 application이 구동되고 있는 것을 확인할 수 있습니다.

heroku는 cloud platform입니다. instance의 갯수를 제어할 수 있습니다. instance를 1개 이상 사용하는 경우, 비용이 발생하기 때문에 주의가 필요합니다.

$> heroku ps:scale web=1

bower를 이용한 javascript module dependency 처리

bower를 이용하는 경우, javascript를 각각의 repository에서 다운 받아 처리하게 됩니다. 따라서 bower components의 경우, repository에 올라가지 않는 것이 원칙입니다. 따라서, heroku에 배포된 application은 bower components가 하나도 없게 됩니다.

개발환경에서 bower install을 통해서 bower components들을 얻어주는 것과 같이 heroku에서도 이와 같은 action이 필요하게 됩니다. heroku의 nodejs 환경에는 아무런 환경이 없다고 생각해야지 됩니다. 이를 해결하기 위해서 bower 자체를 dependency에 추가해야지 됩니다.

npm install bower --save

bower를 추가후, package.json 파일의 scripts 항목에 다음을 추가한 후, heroku에 배포를 다시 진행합니다.

  "scripts": {
    "start": "node app.js",
    "test": "grunt jasmine",
    "postinstall": "bower install"
  },

이제 application을 정상적으로 설치하는 과정이 모두 마쳐졌습니다.

mongodb 설치

제가 만든 application은 mongodb를 사용하고 있습니다. mongodb는 heroku에서 addon 형태로 제공하고 있습니다. 500M 까지는 무료로 사용할 수 있으니, 아주 작은 application의 경우에는 언제나 무료로 사용할 수 있습니다. compose mongoDb에 대한 소개는 다음 URL에서 확인 가능합니다. https://devcenter.heroku.com/articles/mongohq

mongodb addon의 설치는 다음과 같습니다.

$> heroku addons:add mongohq

여기서, billing information이 설정되어 있지 않으면 addon의 설치는 진행되지 않습니다. 주의가 필요합니다.

mongodb 연결

compose mongodb를 설치하면, ENV에서 db connection url을 확인할 수 있습니다. config 값의 확인은 다음과 같습니다.

$> heroku config
=== my-sadari Config Vars
MONGOHQ_URL: mongodb://heroku:Xucs9HmJrBKBr6bPkO6CM044_G6oVv5gV0bN0dxcXS-LYHWmKdWXaPnqcYwa7MQn1iVyOmbrP4BRT8QkdJcxlQ@dogen.mongohq.com:10058/app31973614

이제 이 값을 이용하는 nodejs code는 다음과 같습니다.

db: process.env.MONGOHQ_URL

mondodb data import/export

개발환경에서의 mongodb의 값을 heroku에 옮겨야지 되는 경우는 자주 발생합니다. 기본적인 key값이라던지, 초기값들을 넣어주는 작업들은 반드시 필요한 작업들입니다. 이러한 작업을 application에서 해주는 것도 좋지만, 개발중인 mongodb의 값을 eport 시킨 후, import 시켜주는 것이 보다 더 편합니다.

이를 위해서는 먼저 원 데이터의 export가 필요합니다.
기본적인 export command는 다음과 같습니다.

mongoexport --collection <collection> --out <collection.json>

각 collection 당 json 파일을 만들어야지 되는 것을 까먹으면 안됩니다.
이제 heroku mongodb로 값을 export 시켜야지 됩니다. heroku deshboard로 가면 설치된 addon들을 모두 볼 수 있습니다. (아래의 무료 Plan은 이제 없습니다. 기존에 사용하고 있던 사람들은 계속 사용가능합니다.)

click 하면 Compose MongoDB configuration 화면을 볼 수 있습니다.

여기서 Admin으로 들어가서 새로운 사용자를 하나 만들어줍니다.

이제 만들어진 사용자로, import를 진행할겁니다. 이 관리자 console에서 remote url을 확인 할 수 있습니다.

위 정보를 이용해서 mongodb import는 다음 command를 이용해서 처리가 가능합니다.

mongoimport --host dogen.mongohq.com --port 10058 --username <user> --password <pass> --collection <collection> --db app31973614 --file <file.json>

Summary

heroku에 nodejs를 이용한 appliation 배포에 대해서 알아봤습니다. 제가 속한 팀에서의 점심후의 사다리타기 결과를 모아둔 heroku app을 공개해뒀습니다. ㅋㅋ (http://my-sadari.herokuapp.com) heroku는 300M까지는 무료입니다. 이 점에서 nodejs의 가벼운 code의 강점이 보이는 것 같습니다. 아무리 가벼운 웹이라도 은근히 용량이 되는 경우가 많으니까요.

그럼 모두 Happy Coding!


Posted by Y2K
,

nodejs + express를 이용한 Google OAuth 연동

OAuth는 근간에 거의 모든 WebSite에서 사용되고 있는 인증방법입니다. 회원 가입절차를 빠르게 할 수 있으며, 개인정보에 대한 관리 이슈를 피할 수 있어, 많이들 사용되는 방법입니다. nodejs를 이용한 OAuth 인증 방법에 대해서 알아보도록 하겠습니다.

npm package 설치

npm install passport
npm install passport-google-oauth
npm install cookie-session
  • passport: OAuth를 지원하기 위한 base package 입니다. twitter, facebook, google에서 지원하고 있는 OAuth를 모두 지원합니다.
  • passport-google-oauth: passport를 기반으로 하는 google-oauth 지원 package입니다.
  • cookie-session: express에서 session을 지원하기 위한 package입니다. express-session으로 대체해서 사용 가능합니다.

google auth api 설정

https://console.developers.google.com 에서 Google에서 제공하는 API를 이용하는 project를 생성할 수 있습니다. 새로운 project를 생성한 후, API 및 인증 > 사용자인증정보에서 새클라이언트 ID 만들기를 통해서 새로운 API를 만들어줍니다. 여기서 중요한 설정은 다음과 같습니다.

  • 승인된 javascript 원본 : 인증을 요청할 url을 넣습니다. 개발자 환경인 http://localhost:3000 과 같은 표현 역시 가능합니다.
  • 승인된 redirection URL : 인증이 완료된 후, redirect될 url을 넣어줍니다.

만들어진 웹어플리케이션용 Client ID에서 이제 3개의 정보는 우리가 작성할 application에서 사용해야지 됩니다.

  • 클라이언트 ID
  • 클라이언트 보안 비밀
  • URI 리디렉션

Login uri handling

사용자가 OAuth를 통해서 인증될 URL을 설정합니다. express controller code에 해당되는 내용입니다.

module.exports = OAuthController;

function OAuthController(app) {
  var passport = require('passport');
  app.use(passport.initialize());
  app.use(passport.session());
  // Google OAuth를 사용하는데 필요한 객체를 선언합니다.
  var GoogleStrategy = require('passport-google-oauth').OAuth2Strategy;

  passport.serializeUser(function(user, done) {
    done(null, user);
  });
  passport.deserializeUser(function(obj, done) {
    done(null, obj);
  });

  passport.use(new GoogleStrategy({
    clientID: '클라이언트 ID',
    clientSecret: '클라이언트 보안 비밀',
    callbackURL: 'URI 리다렉션'
  },
  function(accessToken, refreshToken, profile, done) {
    //
    // !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
    // req.session.passport 정보를 저장
    // done 메소드에 전달된 정보가 세션에 저장된다.
    // profile을 이용해서 사용자 정보를 DB에 넣는 등의 작업에 활용할 수 있다.
    // !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
    //
    return done(null, profile);
  }));

  // 인증 URI : uri link를 이곳에 넣어주면 됩니다.
  app.get('/auth/google', passport.authenticate('google', {
    scope: [
      'https://www.googleapis.com/auth/userinfo.profile',
      'https://www.googleapis.com/auth/userinfo.email'
    ]
  }));

  app.get('/oauth2callback', passport.authenticate('google', {
    successRedirect: '/auth/successed',
    failureRedirect: '/auth/failure'
  }));

  app.get('/logout', function(req, res) {
    //
    // passport 에서 지원하는 logout 메소드이다.
    // req.session.passport 의 정보를 삭제한다.
    //
    req.logout();
    req.session = null;
    res.redirect('/');
  });
};

Summary

Passport를 통해서 Google OAuth에 접근하는 방법은 많은 예시 code들이 있고, 거의 유사한 형태를 갖습니다. 위 controller code를 그대로 copy & paste로만도 간단히 Google OAuth를 구현 가능합니다.


Posted by Y2K
,

jasmine-node를 이용한 nodejs test

설치

다음 npm package를 설치해서 처리한다.

sudo npm install -g jasmine-node

Application의 구성

기본적으로 yo에서 제공되는 application 구조를 그대로 가지고 가는 것이 좋다. (express application을 기준)

.
├── Gruntfile.js
├── app
│   └── lib
├── app.js
├── npm-debug.log
├── package.json
└── test
    └── calculator-spec.js

test code의 실행

일반적으로 root directory에서 다음 명령어를 이용해서 처리하면 된다.

jasmine-node --test-dir test --autotest --watch app test --color

simple test code

target이 되는 file의 위치가 app/lib/testservice.js 라고 할때, export되는 객체는 다음과 같이 구성될 수 있다.

exports.method1 = function() {
  return true;
};

exports.method2 = function() {
  return false;
};

javascript는 method 단위의 구성을 주목할 필요가 있다.

이때, 이 method1, method2를 test하는 code는 다음과 같이 작성가능하다.

var testservice = require('../app/lib/testservice.js');
describe('test testservice', function() {
  it('method1 called', function() {
    expect(testservice.method1()).toBe(true);
  });

  it('method2 called', function() {
    expect(testservice.method2()).toBe(false);
  });
});

simple test code 2

만약에 db를 접속해서 만들어지는 BL이 존재한다면 다음과 같이 구성한다. yo express-generator에서는 db connection이 app.js에서 구성되는데, 이를 따로 뽑아서 사용하면 db에 대한 service code test가 쉽다.
mongoDb에 접근하는 코드는 다음과 같다.

var mongoose = require('mongoose'),
    glob = require('glob'),
    path = require('path'),
    rootPath = path.normalize(__dirname + '/..');

var init = function() {
  mongoose.connect('mongodb://localhost/sadari');
  var db = mongoose.connection;
  db.on('error', function() {
    throw new Error('unable connect to database');
  });

  //glob.sync는 absolute directory를 기준으로 검색하기 때문에 반드시 path에 대한 정보가 필요하다.
  var models = glob.sync(rootPath + '/models/*.js');
  models.forEach(function(model) {
    require(model);
  });
};

exports.init = init;

위와 같은 db connection을 이용하는 간단한 CRUD service를 작성할 때, code는 다음과 같이 구성될 수 있다.

var mongoose = require('mongoose');
var Player = mongoose.model('Player');
var ObjectId = mongoose.Types.ObjectId;

var findAll = function(func) {
  return Player.find(function(err, players) {
    if(func) {
      func(players);
    }
  });
};

var save = function(name, defaultAmount) {
  var player = new Player({
    name: name,
    defaultAmount: defaultAmount
  });
  return player.save(function(err) {
    throw new Exception(err);
  });
};

var findOne = function(id, func) {
  var objectId = mongoose.Types.ObjectId(id);
  Player.findById(objectId, func);
};

var find = function(query, func) {
  Player.find(query, func);
};

var update = function(id, name, defaultAmount, func) {
  var objectId = mongoose.Types.ObjectId(id);
  Player.update(
    {_id: objectId},
    {
      $set: {
        name: name,
        defaultAmount: defaultAmount
      }
    }, { upsert: false, multi: true }, func);
};

exports.update = update;
exports.findOne = findOne;
exports.findAll = findAll;
exports.save = save;
exports.find = find;

이에 대한 test code는 다음과 같다.

var dbConnection = require('../app/lib/db-connect.js');
dbConnection.init();
var playerService = require('../app/lib/playerservice.js');

describe('mongoDb test', function() {
  it('playerService.findAll', function(done) {
    var conn = playerService.findAll(function(players) {
      expect(players.length).not.toBe(0);
      players.forEach(function(player) {
        expect(player.defaultAmount).not.toBe(0);
      });
      done();
    });
    expect(!!conn).toBe(true);
  });

  it('playerService.save', function() {
    var result = playerService.save('playerName', 10);
    expect(result).not.toBe(null);
  });

  it('playerService.find', function(done) {
    playerService.find({}, function(err, players) {
      expect(players.length).toBe(1);
      players.forEach(function(player) {
        console.log(player.name);
      });
      done();
    });
  });

  it('playerService.findOne', function(done) {
    var id = "5469751de83eaf6e34434c5e";
    playerService.findOne(id, function(err, player) {
      console.log(err);
      expect(!!err).toBe(false);
      expect(player).not.toBe(null);
      console.log(player);
      console.log(player.name);

      done();
    });
  });

  it('playerService.update', function(done) {
    var id = "5469751ae83eaf6e34434c55";
    playerService.update(id, "changedName2", 100, function(err, model) {
      expect(!!err).toBe(false);
      done();
    });
  });

  it('players list', function(done) {
    var mongoose = require('mongoose');
    var Player = mongoose.model('Player');
    var playersResult = null;
    Player.find(function(err, players) {
      expect(players.length).not.toBe(0);
      players.forEach(function(p) {
        expect(p.defaultAmount).not.toBe(0);
      });
      done();
    });
  });
});


Posted by Y2K
,