본문으로 바로가기

[Node.js] Express 구조 이해하기

category Web/Node.js 2020. 3. 15. 19:03

express-generator를 활용해 프로젝트를 생성했을 때

생성되는 파일들의 역할과 express 서버의 구조 및 동작원리를 이해하기 위해 작성하는 포스트입니다.

엄청 길지만 공부한걸 정리해보려고 작성 ㅜ


1. 핵심 파일 분석

디렉토리

1) bin/www

www 파일은 http 모듈에 express 모듈을 연결하고, 포트를 지정하는 부분입니다.

확장자가 붙어있지 않은데 파일 첫 줄에  #!/usr/bin/env node 라는 주석을 활용해 콘솔 명령어가 됩니다.

 

주요 코드

var app = require('../app');
var debug = require('debug')('express-test:server');
var http = require('http');

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

var server = http.createServer(app);

server.listen(port);
server.on('error', onError);
server.on('listening', onListening);
  • debug 모듈 : 콘솔에 로그를 남기는 모듈
  • app.set('port', port) : 서버가 실행될 포트를 설정, 기본값으로 3000번 포트를 이용하게 됨
    • app.set(키, 값) : 데이터를 저장
    • app.get(키) : 데이터를 가져옴
  • http.createServer : 불러온 app 모듈을 넣어주고 app 모듈이 createServer 메서드의 콜백 함수 역할
  • listen : 포트를 연결하고 서버를 실행

2) 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');

// express 패키지를 호출하여 app 변수 객체 생성
var app = express();

// 익스프레스 앱을 설정
app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'pug');

// 미들웨어 연결
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')));

app.use('/', indexRouter);
app.use('/users', usersRouter);

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');
});

// app 객체를 모듈로 만들어 줌, 이것이 bin/www에서 사용된 app 모듈
module.exports = app;
  • app.set : 익스프레스 앱을 설정
  • app.use : 미들웨어를 연결

구조를 그림으로 표현하면 아래와 같다.

출처: https://thebook.io/006982/ch06/02-04/

위의 구조를 보면 요청에 대해 응답을 보낼 때 중간에 미들웨어들을 거친 후 응답하게 되는데 미들웨어에 대해 알아봅시다.

2. 미들웨어

익스프레스의 핵심으로 요청과 응답의 중간에 위치하여 미들웨어라고 부릅니다.

라우터와 에러 핸들러 또한 미들웨어의 일종입니다.

미들웨어는 요청과 응답을 조작하여 기능을 추가하기도 하고, 나쁜 요청을 걸러내기도 합니다.

미들웨어는 주로 app.use와 함께 사용됩니다.

 

app.js 코드 일부

// 미들웨어 연결
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')));

app.use('/', indexRouter);
app.use('/users', usersRouter);

// 404 처리 미들웨어
app.use(function(req, res, next) {
  next(createError(404));
});

// 에러 핸들러
app.use(function(err, req, res, next) {
  // set locals, only providing error in development
  res.locals.message = err.message;
  res.locals.error = req.app.get('env') === 'development' ? err : {};

  // render the error page
  res.status(err.status || 500);
  res.render('error');
});

// app 객체를 모듈로 만들어 줌, 이것이 bin/www에서 사용된 app 모듈
module.exports = app;

app.use 메서드에서 app.use('인자')이런식으로 인자로 들어있는 함수가 미들웨어 입니다.

미들웨어는 use 메서드로 app에 장착합니다.

가장위의 logger('dev')부터 시작하여 미들웨어들을 순차적으로 거친 후 라우터에서 클라이언트로 응답을 보냅니다.

미들웨어의 요청 흐름은 아래와 같습니다.

출처: https://thebook.io/006982/ch06/03-01/

라우터와 에러 핸들러도 미들웨어의 일종으로 app.use를 통해 app에 연결되어있습니다.

 

위의 구조도에서도 보이듯이 미들웨어는 항상 next()가 호출되어야 다음 미들웨어로 넘어갈 수 있습니다.

➢ next() 함수

next 함수는 몇가지 기능을 가지며 인자의 종류로 기능이 구분됩니다.

  • next() : 다음 미들웨어로
  • next('route') : 다음 라우터로
  • next(error) : 에러 핸들러로

인자로 route를 넣으면 특한 기능을 하게되고 route외에 다른 값을 넣으면

다른 미들웨어나 라우터를 건너 뛰고 바로 에러 핸들러로 이동합니다. 넣어준 값은 에러에 대한 내용으로 간주 합니다.

1) morgan

서버를 실행 시켰을 때 콘솔에 나오는 GET / 200 51.267 ms - 1539 같은 로그는 모두 morgan 미들웨어에서 나오는 것입니다.

요청에 대한 정보를 콘솔에 기록해 주는 역할을 합니다.

 

사용 코드

...
var logger = require('morgan');
...
app.use(logger('dev'));
...

logger 함수의 인자로 dev 외에 short, common, combined 등을 줄 수 있습니다.

인자에 따라 콘솔에 나오는 로그가 달라집니다.

2) body-parser

요청의 본문을 해석해주는 미들웨어입니다.

보통 폼 데이터나 AJAX 요청의 데이터를 처리합니다.

 

사용 코드

...
var body-parser = require('body-parser');
...
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: false });
...

익스프레스 4.16.0버전부터 body-parser의 일부 기능이 내장되었기 때문에,

기본적으로 생성된 app.js에서는 body-parser를 사용하지 않고 아래와 같이 내장된 가능을 사용하고 있습니다.

app.use(express.json());
app.use(express.urlencoded({ extended: false }));

Raw, Text 본문의 데이터를 다룰 경우에는 body-parser를 설치해 사용하면 됩니다.

3) cookie-parser

요청에 동봉된 쿠키를 해석해줍니다.

 

사용

...
var cookie-parser = require('cookie-parser');
...
app.use(cookieParser());
...

해석된 쿠키들은 req.cookies 객체에 들어가게 됩니다.

4) static

정적인 파일들을 제공하는 내장 미들웨어 입니다.

함수의 인자로 정적 파일들이 담겨 있는 폴더를 지정합니다.

 

사용

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

기본적으로 public 폴더가 지정됩니다.

따라서 public/stylesheets/style.css는 http://localhost:3000/stylesheet/style.css로 접근할 수 있습니다.

실제 서버의 폴더 경로에는 public이 들어 있지만, 요청 주소에는 public이 들어있지 않습니다.

3. Router 객체

단순하게 http 모듈만 활용해 라우터를 만들면 요청 메서드와 주소별로 분기 처리를 하게되어 코드가 매우 복잡해집니다.

익스프레스를 활용하면 라우팅을 깔끔하게 관리할 수 있게 됩니다.

 

app.js의 라우터 부분

...
var indexRouter = require('./routes/index');
var usersRouter = require('./routes/users');
...
// 주소가 /로 시작하면 routes/index.js를 호출
app.use('/', indexRouter);

// 주소가 /users로 시작하면 routes/users.js를 호출
app.use('/users', usersRouter);
...

익스프레스 앱과 app.use로 연결되며 app.use를 사용하므로 라우터도 일종의 미들웨어라고 볼 수 있습니다.

라우팅 미들웨어는 첫 번째 인자로 주소를 받아서 특정 주소에 해당하는 요청이 왔을 때만 미들웨어가 동작하게 할 수도 있습니다.

 

use 메서드는 모든 HTTP 메서드에 대해 요청 주소만 일치하면 실행되지만 get, post, put, patch, delete와 같은 메서드는 주소뿐만 아니라

HTTP메서드까지 일치하는 요청일 때만 실행됩니다.

 

1) routes 폴더의 라우터 파일

routes/index.js

var express = require('express');
var router = express.Router();

/* GET home page. */
router.get('/', function(req, res, next) {
  res.render('index', { title: 'Express' });
});

// 라우터 객체를 모듈로 만듬
module.exports = router;
  • express.Router() : 라우터 객체 생성

처음 생성된 routes/index.js 파일로 살펴보면

router에도 app처럼 use, get, post, put,  patch, delete 같은 메서드를 붙일 수 있습니다.

use를 제외하고는 HTTP 요청 메서드와 상응합니다.

 

라우터에서는 반드시 요청에 대한 응답을 보내거나 에러 핸들러로 요청을 넘겨야 합니다.

res 객체에 들어 있는 메서드들로 응답을 보냅니다.

 

2) 라우터 주소 패턴

  • /요청주소/:키
router.get('/users/:id', function(req, res){
    console.log(req.params, req.query);
});

위의 예시를 보면 주소에 id가 키로 지정된 요청입니다.

/users/12 , /users/hi 이런식의 요청이 있다면 req.params 안에 12, hi 값이 id라는 키의 값으로 담기는 것 입니다.

값을 조회하려면 req.params.id로 조회할 수 있습니다.

 

주소에 쿼리스트링을 활용하기도 합니다.

/users/12?query1=10 이런 요청이 있다면

req.query 객체에 값이 담길 것 입니다.

 

응답 객체는 아래와 같을 것 입니다.

{ id: '12' }, { query1: '10' }

3) 라우터 응답

에러가 발생하지 않았다면 라우터는 요청을 보낸 클라이언트에게 응답을 보내야합니다.

주로 사용하는 응답 메세지는 send, sendFile, json, redirect, render가 있습니다.

  • res.send(버퍼, 문자열, HTML 또는 JSON)
  • res.sendFile(파일 경로)
  • res.json(JSON 데이터)
  • res.redirect(주소)
    : 응답을 다른 라우터로 보냅니다.
  • res.render('템플릿 파일 경로', { 변수 })
    : 템플릿 엔진을 렌더링할 때 사용

라우터가 요청을 처리하지 못할 경우에는 다음 미들웨어로 넘어가고

새로운 에러를 생성해 에러 상태코드를 404로 설정한 뒤 에러처리 미들웨어로 넘겨집니다.

'Web > Node.js' 카테고리의 다른 글

[Node.js] Express+MongoDB, API 서버 구현하기(1)  (0) 2020.04.01
[Node.js] Express 시작하기  (0) 2020.03.15
[Node.js] http모듈 활용 - REST API와 라우팅  (0) 2020.03.14
[Node.js] Node.js란?  (0) 2020.03.12