익스프레스 프레임워크를 사용해서 웹 서버를 구축해봅시다. 익스프레스는 많은 의존성 패키들을 사용하기 때문에 조금 어려울 수 있지만 express-generator를 이용하면 package.json과 기본 폴더구조까지 잡아줍니다.
express-generator는 콘솔 명령어를 사용하기 때문에 전역으로 설치해주죠.
$ npm i -g express-generator
익스프레스 프로젝트를 생성해보죠.
$ express express-tutorial --view=pug
여기서 --view=pug
는 템플릿 엔진을 어떤 걸 사용할지 명시하는 부분입니다. 여기서는 Pug를 설치하기 위해 옵션을 줬죠. 그 후에 package.json에 dependencies 들을 설치해줍니다.
$ npm install
먼전 bin/www 파일의 중요한 부분을 살펴보겠습니다.
bin/www
#!/usr/bin/env node // 이 파일을 콘솔 명령어로 만들 때 사용합니다.
var app = require('../app');
var debug = require('debug')('learn-express:server');
var http = require('http');
// 환경변수에서 port값이 있으면 설정하고 없다면 3000번 포트로 설정합니다.
var port = normalizePort(process.env.PORT || 3000);
app.set('port', port);
// app 모듈을 사용해서 서버를 만듭니다.
var server = http.createServer(app);
server.listen(port);
// 'error', 'listening' 이벤트를 핸들링합니다.
server.on('error', onError);
server.on('listening', onListening);
app.js
var createError = require('http-errors');
var express = require('express');
var path = require('path');
var cookieParser = require('cookie-parser');
var logger = require('morgan');
var indexRouter = require('./routes/index');
var usersRouter = require('./routes/users');
var app = express(); // app 객체를 생성했으니 이 객체로 각종 기능을 연결합니다.
// app.set 메서드로 익스프레스 앱을 설정합니다.
app.set('views', path.join(__dirname, 'views');
app.set('view engine', 'pug');
// app.use는 미들웨어를 사용하기 위한 메서드입니다.
app.use(logger('dev'));
app.use(express.json());
app.use(express.urlencoded({extended: false}));
app.use(cookieParser());
app.use(express.static(path.join(__dirname, 'public')));
// URL에 대한 라우터를 연결합니다.
app.use('/', indexRouter);
app.use('/users', usersRouter);
// 404 에러 핸들러
app.use(function(req, res, next) {
next(createError(404));
});
// 에러 핸들러
app.use(function(err, req, res, next) {
res.locals.message = err.message;
res.locals.error = req.app.get('env') ==='development' ? err : {};
res.status(err.status || 500);
res.render('error');
});
module.exports = app;
결국 크게 보자면 http 통신과 같습니다. 요청에 대한 응답을 하는 구조는 같지만 그 사이에 여러가지 미들웨어를 통해 에러를 걸러내고 쿠키를 파싱하는 등 최적의 응답을 반환합니다. 그러면 미들웨어가 무엇이지를 알아야겠죠?
요청과 응답사이에 위치하기 때문에 미들웨어라고 하는데 중간에서 요청과 응답을 조작하기도 하고 기능도 추가하며, bad request를 걸러내기도 합니다. 앞서 살펴본 것과 같이 app.use
메서들 사용합니다. 익스프레스가 곧 미들웨어이다 라고 할 정도로 매우 중요하니 잘 정리하고 넘어가는게 좋아보이네요!
app.js
app.use()
메소드에 인자로 들어있는 함수가 바로 미들웨어입니다. 맨 위에 logger('dev')
부터 차례대로 미들웨어를 거친 후에 라우터를 사용해 클라이언트로 응답을 보냅니다.
요청 → logger → json → urlencoded → cookieparser → static → router → 404 → error → 응답
// app.use는 미들웨어를 사용하기 위한 메서드입니다.
app.use(logger('dev'));
app.use(express.json());
app.use(express.urlencoded({extended: false}));
app.use(cookieParser());
app.use(express.static(path.join(__dirname, 'public')));
// URL에 대한 라우터를 연결합니다.
app.use('/', indexRouter);
app.use('/users', usersRouter);
// 404 에러 핸들러
app.use(function(req, res, next) {
next(createError(404));
});
// 에러 핸들러
app.use(function(err, req, res, next) {
res.locals.message = err.message;
res.locals.error = req.app.get('env') ==='development' ? err : {};
res.status(err.status || 500);
res.render('error');
});
module.exports = app;
그럼 콘솔에 메세지를 출력하는 커스텀 미들웨어를 만들어 볼까요?
app.use((req, res, next) => {
console.log(req.url, "미들웨어 가동 시작!");
next(); // next 함수를 써줘야 다음 미들웨어로 넘어갑니다.
});
app.use(logger('dev'));
app.use(express.json());
app.use(express.urlencoded({extended: false}));
app.use(cookieParser());
app.use(express.static(path.join(__dirname, 'public')));
// URL에 대한 라우터를 연결합니다.
app.use('/', indexRouter);
app.use('/users', usersRouter);
// 404 에러 핸들러
app.use(function(req, res, next) {
next(createError(404));
});
// 에러 핸들러
app.use(function(err, req, res, next) {
res.locals.message = err.message;
res.locals.error = req.app.get('env') ==='development' ? err : {};
res.status(err.status || 500);
res.render('error');
});
module.exports = app;
미들웨어를 사용할 때 주의할 점은 next()
함수를 써줘야 다음 미들웨어로 넘아간다는 점입니다.
만약 써주지 않으면 그 미들웨어에서 응답이 끊겨버립니다.
콘솔에 찍히는 GET / 200 256.902ms - 170
과 같은 로그는 모두 morgan 미들웨어에서 나오는 것입니다. 요청에 대한 정보를 기록해주죠.
var logger = require('morgan');
...
app.use(logger('dev'));
인자로 dev, short, common, combined를 줄 수 있습니다. 개발 시에는 보통 dev, short 배포에서는 common, combined를 사용합니다. 파일이나 db에 로그 정보를 남길 수도 있는데 이에 보통 winston모듈을 많이 사용합니다.
body-parser 모듈은 요청의 본문을 해석해주는 미들웨어입니다. 폼 데이터, AJAX 요청의 데이터를 처리해주죠.
var bodyParser = require('body-parser');
...
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({extended: false});
그런데 익스프레스 4.16.0 버전부터 익스프레스에 내장되었기 때문에 따로 설치할 필요가 없습니다.
app.use(express.json());
app.use(express.urlencoded({extended: false});
그냥 위처럼 사용하면 되죠. 그러나 만약 요청의 본문이 Raw, Text형식일 경우 버퍼데이터와, 텍스트 데이터를 읽어야 하기 때문에 아래와 같이 추가적인 기능을 적용시킬 수 있습니다.
app.use(bodyParser.raw());
app.use(bodyParser.text());
URL-encoded는 주소 형식으로 데이터를 보냅니다. 보통 폼 데이터가 이에 해당되죠. 코드를 보면 { extended: false }
부분이 있는데 이 옵션이 false
면 노드의 내장 모듈인 querystring을 사용해 해석하고 true
면 qs 모듈로 해석합니다. 이는 내장 모듈이 아닌 npm 패키지입니다.
body-parser를 사용하면 내부적으로 본문을 해석해 req.body에 넣어줍니다. POST나 PUT 요청의 본문을 전달받기 위해 req.on('data')와 req.on('end')로 스트림을 사용할 필요가 없죠. JSON은 그대로 JSON형식으로 추가되고 URL-encoded 방식은 name=mecha&age=27 이 { name: "mecha, age=27 }로 변환되어 추가됩니다.
이 모듈은 브라우저에서 서버로 보내진 동봉된 쿠키를 파싱합니다. 예를 들어서 name=mecha 라는 쿠키를 보냈다면 이를 { name: "mecha" }로 req.cookies 객체에 추가됩니다.
app.use(cookieParser('secret code')
과 같이 인자를 추가해서 쿠키에 서명할 수도 있습니다. 서명한 쿠키는 브라우저에서 수정하려할 때 에러가 나기 때문에 사전에 막아둘 수 있겠죠.
static 미들웨어는 정적 파일을 제공하는 미들웨어입니다.
app.use(express.static(path.join(__dirname, 'public')));
public/stylesheets/styles.css 는 http://localhost:3000/stylesheets/style.css로 접근할 수 있습니다. 두 개를 비교해보면 url에서는 public 디렉토리 경로가 빠져있죠? 이렇게 함으로써 서버의 구조를 쉽게 파악하지 못하도록 하면 보안을 좀 더 강화할 수 있습니다.
정적 파일들을 알아서 제공해주기 때문에 fs 모듈, readFile
메소드를 사용해서 파일을 굳이 읽지 않아도 됩니다. 또한 정적 파일을 제공할 주소를 정할 수도 있습니다.
app.use('/img', express.static(path.join(__dirname, 'public')));
위와 같이 '/img
라는 경로를 붙이면 http://localhost:3000/img/...... 로 파일에 접근할 수 있습니다.
static 미들웨어는 요청에 해당되는 정적파일을 찾아내서 응답에 해당 파일을 전송합니다. 따라서 따로 라우터를 해줄 필요가 없고 만약 찾지 못하면 요청을 라우터로 보냅니다. 즉, 자체적인 라우터 기능을 하기 때문에 미들웨어 순서에서 최대한 위쪽에 배치해야 서버 자원 낭비가 되지를 않죠. 콘솔에 정보를 기록해주는 morgan 미들웨어 다음에 배치하는게 좋겠죠?
app.use(logger('dev'));
app.use(express.static(path.join(__dirname, 'public')));
app.use(express.json());
app.use(express.urlencoded({extended: false});
위와 같이 순서를 배치해야 정적 파일과는 상관없는 미들웨어인 json과 urlencoded를 거치지 않습니다.
세션을 관리하는 미들웨어입니다. 로그인 유지 등과 같은 이유로 사용할 때 매우 유용하죠. express-session은 express에 내장되어있는 미들웨어가 아니기 때문에 npm 패키지로 설치해줘야 합니다.
$ npm i express-session
그리고 미들웨어로 배치해보죠.
var session = require('express-session');
...
app.use(cookieParser());
app.use(
session({
resave: false,
saveUninitialized: false,
secret: "secret code",
cookie: {
httpOnly: true,
secure: false,
},
})
);
각각의 옵션에 대해서 알아봅시다.
라우터는 주어진 url 및 http 메서드에 대해서 미들웨어를 실행시킵니다. 네 그렇습니다. 라우터도 미들웨어의 일종이죠. 예를 들어 '/' 요청이 오면 그에 해당되는 미들웨어를 실행시킵니다. 아래와 같이 말이죠.
app.use('/', (req, res, next) => {
console.log('/ 로 요청이 들어왔을 때 이 미들웨어가 실행됩니다.');
next();
});
app.get('/' (req, res, next) => {
console.log('이렇게 http 메소드를 첫 번째 인자로 넣어줄 수도 있습니다.');
});
app.post('/' (req, res, next) => {
console.log('이렇게 http 메소드를 첫 번째 인자로 넣어줄 수도 있습니다.');
});
라우터는 routes 디렉토리에 들어있습니다. 어떻게 구현되어있는 지 확인해보죠.
routes/index.js
var express = require('express');
var router = express.Router();
router.get('/', function(req, res, next) {
res.render('index', { title: 'Express' });
});
module.exports = router;
routes/users.js
var express = require('express');
var router = express.Router();
router.get('/', function(req, res, next) {
res.send('respond');
});
module.exports = router;
위 두 가지 라우터를 보면 라우터도 마찬가지로 use, get, post, put, patch, delete 메소드를 사용할 수 있으며 여러 개의 미들웨어를 연결시킬 수도 있습니다.
router.use('/', middleware1, middleware2, middleware3) // 이런 식으로요!
미들웨어와 마찬가지로 next()
를 사용해서 다음 미들웨어로 넘기는데 만약 next('route')라고 사용하면 나머지 미들웨어를 모두 건너뛰게 됩니다.
router.get('/', middleware1, middleware2, middleware3);
router.get('/', middleware4);
middleware1에서 만약 next('route')
가 호출되면 middleware2, middleware3를 건너뛰고 다음 라우터인 middleware4가 실행됩니다.
url에 파라미터를 넣어줄 수도 있습니다.
router.get('/users/:id', function(req, res, next) {
console.log(req,params, req.query);
});
:id
라고 작성하면 이 값에는 id 값을 넣겠다. 즉, /users/156
이런 식으로 값이 들어가게 되죠. 마치 장고에서 <int: pk>
로 파라미터를 설정하는 것과 같네요. url 파라미터는 req.params, 쿼리스트링은 req.query 속성에 있다는 것을 잘 활용하세요.
/users/156?age=27&gender=male 일 경우
// req.params = { id: 156 } req.query = { age: 27, gender: male}
요청이 들어오고 미들웨어로 이를 조작한 뒤에 응답할 때 사용하는 메소드들이 있습니다.
장고 템플릿과 비슷하게 express에서도 템플릿 엔진을 사용합니다. 예전에는 Jade라고 불렸던 Pug가 문법이 간단하기 때문에 인기가 많습니다. 먼저 템플릿 엔진 세팅 부분을 확인해보죠.
app.js
app.set('views', path.join(__dirname, 'views')); // 템플릿 경로를 설정하고
app.set('view engine', 'pug'); // 어떤 템플릿 엔진을 사용할지 명시합니다.
이렇게 템플릿 경로를 설정하면 미들웨어에서 응답으로 render
메소드를 사용할 때 이 경로의 파일을 찾아서 렌더링합니다. pug template 문법은 여기서 말고 따로 한 번 싹 정리해보죠! pug 말고도 ejs 템플릿 엔진이 있는데 pug보다 html을 덜 변형한 문법을 지원하기 때문에 pug가 익숙하지 않으신 분들은 ejs를 사용해도 무방할 것 같습니다.
에러 핸들링 미들웨어 코드를 살펴보면 다음과 같습니다.
app.use(function(err, req, res, next) {
res.locals.message = err.message;
res.locals.error = req.app.get('env') === 'development' ? err : {};
res.status(err.status || 500);
res.render('error');
});
위 코드는 pug 템플릿에 어떻게 변수를 전달하는 지를 이해하면 쉽게 알 수 있습니다. res.locals 객체에 속성들을 추가하는데 이는 res.render('error') 즉, 'error' 템플릿에서 변수로 활용할 수 있습니다.
error 값은 env가 development
값을 가질 때만 나타나고 배포 모드에서는 빈 객체를 가르키기 때문에 개발모드에서 디버깅할 때 유용하게 활용할 수 있겠죠.
- 조현영『Node.js 교과서』, (주)도서출판 길벗(2019년 2월 2일), p.112~ 128