클라우드 컴퓨팅 & NoSQL/Vert.x & Node.js

빠르게 훝어 보는 node.js - #4 웹개발 프레임웍 Express 1/2

Terry Cho 2014. 4. 1. 19:36

빠르게 훝어보는 node.js

#3 - Express 1/2

조대협 (http://bcho.tistory.com

Express

node.js는 여러 종류의 웹 개발 프레임웍을 제공한다.얼마전에 Paypal이 내부 시스템을 대규모로 node.js로 전환하면서 오픈소스화한 KarkenJS Meteo 등 여러가지 프레임웍이 있는데그중에서 가장 많이 사용되는 프레임웍 중하나인 Express에 대해서 설명하고자 한다.

Express는 웹 페이지 개발 및 REST API 개발에 최적화된 프레임웍으로 매우 사용하기가 쉽다.

프로젝트 생성

먼저 express module npm을 이용해서 설치한 다음 express 프로젝트를 생성해보자.

% express --session --ejs --css stylus myapp

명령을 이용하면 아래와 같이 express 프로젝트가 생성되고, 기본 디렉토리 및 파일이 생성된다.



Ÿ   --session HTTP session을 사용하겠다는 것이다.

Ÿ   --ejs는 템플릿 렌더링 엔진으로 ejb를 사용하겠다는 것이다. (자세한 내용은 나중에 template 엔진에서 설명한다.)

Ÿ   그리고 --css CSS 엔진을 stylus로 사용한다는 것이다.

위와 같이 프로젝트가 생성되었으면, 의존성이 있는 모듈을 설치하기 위해서

%cd myapp

%npm install

을 실행해보자. 앞에서 정의한 옵션에 따라서, ejb,stylus 등의 모듈과 기타 필요한 모듈들이 서리되는 것을 확인할 수 있을 것이다.

디렉토리 구조



그러면 생성된 디렉토리 구조를 살펴보도록 하자

Ÿ   ./node_module : express앱에 필요한 module을 저장한다.

Ÿ   ./public : html, image, javascript,css와 같은 정적 파일들을 저장한다.

Ÿ   ./routes : URL 별로 수행되는 로직을 저장한다.

Ÿ   ./views : HTML view 템플릿을 저장한다. (여기서는 ejs로 지정하였기 때문에, ejs 템플릿들이 여기 저장된다.)

Ÿ   app.js : 이 웹프로젝트의 메인 소스 코드

Ÿ   package.json : module package.json 파일로, 의존성 및 npm script 명령어를 정의한다.

예제 코드

다음은 자동으로 생성된 app.js 코드의 전문이다. 주요 부분에 대한 내용을 살펴보도록 하자.

var express = require('express');

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

var user = require('./routes/user');

var http = require('http');

var path = require('path');

 

var app = express();

 

// all environments

app.set('port', process.env.PORT || 3000);

app.set('views', path.join(__dirname, 'views'));

app.set('view engine', 'ejs');

app.use(express.favicon());

app.use(express.logger('dev'));

app.use(express.json());

app.use(express.urlencoded());

app.use(express.methodOverride());

app.use(app.router);

app.use(require('stylus').middleware(path.join(__dirname, 'public')));

app.use(express.static(path.join(__dirname, 'public')));

 

// development only

if ('development' == app.get('env')) {

  app.use(express.errorHandler());

}

 

app.get('/', routes.index);

app.get('/users', user.list);

 

http.createServer(app).listen(app.get('port'), function(){

  console.log('Express server listening on port ' + app.get('port'));

});

먼저 require 부분에서 필요한 module 들을 로드한다.


환경 설정

다음으로, var app = express(); express 객체를 설정한 후에, express 애플리케이션에 대한 환경을 설정한다. 몇몇 주요 내용만 살펴보면,

Ÿ   app.set('port', process.env.PORT || 3000);

를 이용해서, node.js의 포트를 설정한다. 여기서는 default 3000 포트로 사용하고, 환경변수에 PORT라는 이름으로 포트명을 지정했을 경우에는 그 포트명을 사용하도록 한다.(Linux의 경우 export PORT=80 이런식으로 환경 변수를 지정한다.)

Ÿ   app.set('views', path.join(__dirname, 'views'));

Ÿ   app.set('view engine', 'ejs');

다음으로, 템플릿 엔진의 종류와 템플릿 파일의 위치를 정의한다 템플릿 파일의 위치를 path.join(__dirname, 'views') 로 정의 하였는데, __dirname은 현재 수행중인 파일의 위치 즉, app.js가 위치한 위치를 의미하며, path.join을 이용하면, ${현재디렉토리}/views 라는 경로로 지정한 것이다.

Ÿ   app.use(require('stylus').middleware(path.join(__dirname, 'public')));

Ÿ   app.use(express.static(path.join(__dirname, 'public')));

css 엔진의 종류를 stylus로 지정하고, 엔진이 렌더링할 static 파일들의 위치를 지정한다. 그리고, express static 파일의 위치를 지정한다. “./public” 디렉토리로 지정

Ÿ   app.use(express.json());

는 들어오는 http request body json 일때도 파싱할 수 있도록 지원한다.

이 밖에도, urlencoded request multipart request(파일 업로드)를 지원하려면 아래 부분을 추가하면 된다

Ÿ   app.use(express.urlencoded());

Ÿ   app.use(express.multipart());

 

이밖에도 다양한 지원 설정들이 있는데, 자세한 사항은 http://expressjs.com/3x/api.html#middleware를 참고하기 바란다.


router 처리

다음으로 특정 URL로 들어오는 http request에 대한 handler (node에서는 router라고 한다.)를 지정한다.

Ÿ   app.get('/', routes.index); : HTTP GET / 에 대해서 /router/routes.js 에 있는 index 함수를 실행하도록 한다.

Ÿ   app.get('/users', user.list); : HTTP GET /users 요청에 대해서 /router/user.js에 있는 list() 함수를 이용하도록 한다.

user.js routes.js 코드 초기 부분에서 require 이용하여 import 처리가 되어 있다.

Ÿ   var routes = require('./routes');

Ÿ   var user = require('./routes/user');

 

http server의 기동

마지막으로 위의 설정값을 기반으로 http server를 기동 시킨다.

http.createServer(app).listen(app.get('port'), function(){

  console.log('Express server listening on port ' + app.get('port'));

});


Router

Router는 특정 URL로 들어오는 HTTP Request를 처리하는 패턴이다. 앞에서 간단하게 살펴본것과 같이 app.{HTTP_METHOD}(“URL”,{Callback_function}); 으로 정의한다.

express에서는 보통 각 URL Method에 맵핑되는 function /routes/ 디렉토리 아래에 모듈로 만들어 놓고 require를 사용해서 불러서 사용하게 된다.

아래 그림을 보면, app.js에서 /routes/index.js (디렉토리 경로만 지정해놓으면 디폴트로 index.js를 부른다.) /route/user.js 모듈을 부른후에

HTTP/GET “/” route 모듈의 index 메서드를 통해서 처리하게 하고,
HTTP/GET “/users”
route 모듈의 list 메서드를 통해서 처리하도록 한다.



 

HTML Parameter Passing

웹서버를 기동하였으면, 그러면 어떻게 HTML에서 parameter를 넘기는지 보자, HTML에서는 HTTP URL의 일부 또는 Query String 또는 HTML body form value로 값을 넘길 수 있다.

각각의 방법을 알아보면

URL Param

Ÿ   app.get(“URI/:{parameter name}”,callbackfunction…);

식으로 정의하면 URL 내의 경로를 변수로 사용할 수 있다. 예를 들어서

app.get('/tweeter/:name',function(req,res){

          console.log(req.params.name);

      });

의 코드는 /tweeter/{경로} 로 들어오는 HTTP GET 요청에 대해서 {경로} 부분을 변수 처리 한다. /tweeter/terry라고 하면, req.params.name으로 해서 “terry”라는 값을 받을 수 있다.

Query Param

다음으로는 HTTP request로 들어오는 Query String의 값을 추출하는 방법인데, request.query.{query string 키 이름} 으로 추출할 수 있다.

예를 들어 HTTP GET /search?q=조대협 으로 요청이 들어왔을때

Ÿ   var q = request.query.q  로 하면 조대협이라는 value를 추출할 수 있다.

Form Param

마지막으로 HTML <form>을 통해서 들어오는 값을 추출하는 방법이다.

다음과 같은 HTML이 있다고 가정할때,

<input name=”username” …/>

form 값을 읽어 올려면

Ÿ   var q = request.body.username  으로 하면 HTML form에서 name“username”으로 정해진 element의 값을 읽어올 수 있다.

Rendering & Template

HTTP response로 응답을 보내는 방법을 rendering이라고 하는데, 간단한 문자열을 경우, response.send(“문자열”); 을 이용해서 보낼 수 도 있다. 또는 response code를 싫어서 보낼때는 response.send(404,”페이지를 찾을 수 없습니다.”); 와 같은 식으로 첫번째 인자에 HTT response code를 실어서 보내는 것도 가능하다.

그러나 복잡한 HTML을 경우 문자열 처리가 어렵기 때문에, Template이라는 것을 사용할 수 있다.

아래는 Express가 지원하는 템플릿중의 하나인 ejs 템플릿으로, JSP,ASP 와 유사한 형태를 갖는다.

먼저 ejs 모듈을 npm을 이용해서 설치 한 후에,



Express 프로젝트 생성시에 다음과 같이 “—ejs”옵션을 줘서, EJB를 템플릿 엔진으로 사용하도록 지정한다.

% express --session --ejs --css stylus myapp

ejs에 대한 module 의존성을 정의하기 위해서 package.json을 정의한다.

{

  "name": "application-name",

  "version": "0.0.1",

  "private": true,

  "scripts": {

    "start": "node app.js"

  },

  "dependencies": {

    "express": "3.4.8",

    "ejs": "*"

  }

다음으로 생성된 app.js 파일에서의 설정 부분을 보면, 템플릿 엔진이 ejs로 지정되어 있는 것을 확인할 수 있다.그리고 ejs 템플릿 파일을 저장할 위치를 __dirname/views 로 지정한 것을 확인할 수 있다.

app.set('views', path.join(__dirname, 'views'));

app.set('view engine', 'ejs');

 

이제 ejs를 사용할 준비가 되었다. 템플릿을 직접 만들어 보자. 앞에서 지정한것과 같이 “/views”디렉토리에 생성하면 된다. /views/index.ejs 파일을 만들어 보자

<!DOCTYPE html>

<html>

<head>

    <title><%= title %></title>

</head>

<body>

<h1><%= title %></h1>

<p>Welcome to <%= name %></p>

</body>

</html>

일반적인 HTML과 거의 유사하다. Parameter를 사용하고자 할때는 ASP JSP처럼 <%=변수%> 로 사용하면 된다. 마찬가지로, for,while,if등 간단한 스크립트 로직도 작성할 수 있다.

ejs에 대한 스크립트는 http://embeddedjs.com/ 를 참고하면 된다.

템플릿을 만들었으면, router를 정의해서, 이 템플릿을 부르도록 해보자

다음과 같은 코드로 /routes/index.js 파일을 생성한다.

exports.index = function(req, res){

          res.render('index', { title: 'Express',name:'Terry' });

        };

       

이 파일은 이 request에 대해서 rendering을 할 때, index라는 템플릿을 부르고 (앞서 엔진과 view 디렉토리를 지정했기 때문에, __dirname/index.ejs 파일을 부르게 된다.), 이때 인자로 title=”Express”, name=”Terry” 두 변수를 각각의 값으로 넘기게 된다.

이렇게 넘겨진 값은 앞서 정의한 template 파일 내의 <%=변수%> 부분에 의해서 HTML 로 렌더링이 되서 출력되게 된다.



Express ejs이외에도 jadeHogan.js와 같은 다른 template 엔진도 지원한다.

https://github.com/Deathspike/template-benchmark 를 보면, 각 템플릿 엔진의 성능 비교가 나와 있는데, Hogan,ejs,jade 순으로 빠른 것을 볼 수 있다. 100,000 템플릿을 렌더링 했을 시에 Hogan 4257 ms, ejs 5283 ms, jades 13068ms 가 소요됨을 볼 수 있다.

Jade html을 사용하지 않고, 고유의 태그 언어를 이용하여, 템플릿을 정의하는데, 이는 실행시에 자동으로 HTML로 변경이 되게 된다. 아래 그림 참조 (jade 스크립트가 렌더링 후 우측의 HTML처럼 변경이 된다.). Jade 스크립트를 사용하면, HTML 보다 더 구조화 되고 깔끔한 템플릿을 만들 수 있다는 장점이 있지만. HTML Publisher(디자이너)가 직접 Jade를 만들어서 페이지를 만들어 주기가 어려우니 (디자이너가 HTML이외의 jade 스크립트를 다시 배워야 함). 분업이 쉽지 않다.



Jade의 장점은 HTML Layout을 지원한다는 것인데, Layout은 하나의 페이지를 Header,Footer,Left Menu 와 같이 별도의 구역으로 나누어서,개별 템플릿으로 렌더링 할 수 있는 기능이다. EJS의 경우에도 https://github.com/RandomEtc/ejs-locals 확장 모듈을 이용하여 Layout과 유사한 기능을 제공할 수 있다.
ejs에서 layout을 지원하는 모듈들이 있기는 하지만 근래에 들어서 잘 maintenance가 안되고 있으니, 다운로드 횟수나 최종 업데이트 시간등을 확인하고 사용하는 것을 권장한다.

필자의 경우 ejs를 선호하는데, 디자이너로부터 받은 HTML을 작업해서 그대로 템플릿으로 변환하기가 쉽다.

HTTP Header 정보의 처리

HTTP에서 Request/Response Header에 대한 처리 방법은 다음과 같다. 먼저 request에서 Header를 읽는 방법은 간단하다.

var ua = req.headers['user-agent'];

 

와 같이 request 객체에서 headers array에 들어 있는 value를 위와 같이 읽으면 된다.

반대로 response message header 정보를 쓰는 방법은 

response.writeHead({HTTP Response Code},{“key”:”value”});

 

식으로 Http response code와 함께, header 정보를 key,value pair array로 넘겨주면 response message에 같이 실어서 리턴한다. 다음은 사용 예이다.

response.writeHead(200,{‘Content-Type”:’text/html’,’Server’:’terry’ });

 

HTTP Cookie 처리

HTTP에서 사용되는 Cookie에 대한 사용법은 아래 예를 통해서 살펴보자. 아래 예는 Cookie를 쓰고 읽는 부분이다.

App.js 파일 일부이다. 먼저 express cookie를 사용함을 알려줘야 한다.

app.use(express.cookieParser());

 

app.get('/writecookie', routes.writecookie);

app.get('/readcookie', routes.readcookie);

 

다음, request객체에서 cookie 메서드를 이용해서 cookie 값을 쓴다. 이때 인자는 cookie 이름, cookie 값 그리고, 배열로 Cookie에 대한 옵션 (Expire time )을 기술한다.

/routes/index.js 파일

exports.writecookie = function(req, res){

    res.cookie('name','terry',{ expires: new Date(Date.now() + 900000), httpOnly: true });

    res.end();

};

 

exports.readcookie = function(req, res){

    var name = req.cookies.name;

    console.log("name cookie is :"+name);

    res.end();

}

 

아래는 위의 코드를 호출하여, cookie를 읽고 쓴 결과를 console로 출력한 내용이다.



Signed Cookie의 사용

위와 같은 방식을 사용하면, Cookie가 네트워크를 통해서 전송 및 브라우져에 저장될 때 암호화 되지 않은 형태로 전송 및 저장 되기 때문에, 악의 적인 공격에 Cookie값이 노출될 우려가 있다.

아래는 실제로 Cookie read하는 HTTP/GET 프로토콜을 Fiddler를 이용해서 Capture한 케이스이다. 아래 내용을 보면 name이라는 쿠키 값이 암호화 되지 않고 올라오는 것을 확인할 수 있다.


 

이를 방지 하기 위해서 Express에서는 Secure Cookie를 지원한다.

app.use에서 express.cookieParser를 정의할때, 암호화 키를 넣을 수 있다.

app.js에서 아래와 같이 cookieParser안에 쿠키에 대한 암호화 키를 지정한후에

app.use(express.cookieParser('mykey'));

 

쿠키를 쓸때는 쿠키 옵션에 “signed:true”옵션을 주고, 쓰면 암호화 된 형태로 쿠키를 쓸 수 있으며

exports.writecookie = function(req, res){

    res.cookie('name','terry',{ expires: new Date(Date.now() + 900000), httpOnly: true ,signed:true});

    res.end();

};

 

쿠키를 읽을때는 request.Cookies가 아니라 request.signedCookie에서 값을 읽으면 암호화된 쿠키를 읽을 수 있다.

exports.readcookie = function(req, res){

    var name = req.signedCookies.name;

    console.log("name cookie is :"+name);

    res.end();

}


(http://expressjs.com/api.html#req.signedCookies
내용 참조).

쿠키를 쓸 때 Packet을 잡아보면, 아래와 같이 Set-Cookie에서 name 쿠키가 암호화된 형태로 전송되는 것을 확인할 수 있으며



읽을 때도 아래와 같이 암호화된 형태로 쿠키가 전송되는 것을 확인할 수 있다.



HTTP Session 처리

HTTP Session 을 사용하는 방법도 Cookie와 크게 다르지 않다. 다만,HTTP Session의 경우 Signed Cookie를 사용하기 때문에, 아래와 같이 express app.use cookieParser 정의시 secret 키를 반드시 정해주고, HTTP Session을 사용함을 express.session use처리 함으로써 명시적으로 알려줘야 한다.

app.use(express.cookieParser('your secret here'));

app.use(express.session());

 

다음으로 session을 쓸때는 request.session.{key name}={value} 식으로 저장하고

exports.writesession = function(req, res){

  req.session.name='terry';

  console.log('write session = name:'+req.session.name);

  res.end();

};

 

값을 읽을 때는 마찬가지로 request.session.name으로 값을 빼낼 수 있다.

exports.readsession = function(req, res){

    console.log('write session = name:'+req.session.name);

  res.end();

};

 

아래는 브라우져를 연 후에 session read/write/read 순서로 테스트한 결과이다. Session write전에는 session에 값이 없다가 write 후에는 정상적으로 값을 읽을 수 있음을 확인할 수 있다.



클러스터에서의 Session 처리

HTTP Session 사용시에 주의할 점은 여러 개의 node.js 인스턴스를 시용할 시, 특히 클러스터링을 사용할 경우에는 인스턴스간에 Session 정보가 공유가 되지 않는다. 이 경우 앞단에 L4 Reverse Proxy같은 로드 밸런스를 둘 경우, 사용자가 항상 같은 서버로 붙지 않기 때문에 세션 정보가 유실 될 수 있다. (처음에는 1번 서버로 연결되었다가 두번째 request L4 Round Robin 정책에 의해서 2번 서버로 연결되는 케이스)

이런 문제를 해결하기 위해서 각 node.js 인스턴스간의 Session 정보를 공유 스토리지에 저장해놓고 서로 공유할 수 있는데, Redis가 많이 사용된다.

방식은 connect-redis 모듈을 이용하여 redis node와 연결하고, app.use에서 session 설정시에 아래 설정과 같이 RedisStore Session Store로 지정하여, Redis를 통해서 Session 정보를 공유할 수 있다.

var express = require('express');

var RedisStore = require('connect-redis')(express);

var ports = require('./classes/ports.js');

var config = require('./config/config.js');

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

 

var app = express();

app.use(express.cookieParser());

app.use(express.session({

  store: new RedisStore({

    port: config.redisPort,

    host: config.redisHost,

    db: config.redisDatabase,

    pass: config.redisPassword

  }),

  secret: 'Your secret here',

  proxy: true,

  cookie: { secure: true }

}));

 

  자세한 설정은 clustering 부분에서 다시 설명한다.


#1 – node.js의 소개와 내부 구조 http://bcho.tistory.com/881

#2 - 설치와 개발환경 구축 http://bcho.tistory.com/884

#3 - Event,Module,NPM  http://bcho.tistory.com/885

#4 - 웹 개발 프레임웍 Express 1/2 - http://bcho.tistory.com/887


그리드형