리셋 되지 말자

[nodejs 교과서] 6장-익스프레스 웹 서버 만들기 본문

NodeJS

[nodejs 교과서] 6장-익스프레스 웹 서버 만들기

kyeongjun-dev 2020. 11. 2. 16:14

6.1 익스프레스 프로젝트 시작하기

  • package.json
{
  "name": "learn-express",
  "version": "0.0.1",
  "description": "learning express using nodejs book",
  "main": "app.js",
  "scripts": {
    "start": "nodemon app"
  },
  "author": "Penguin135",
  "license": "ISC"
}

learn-express 폴더를 만들고 npm init 또는 직접 생성, 수정을 통해 위와 같이 package.json을 생성. scripts 부분에      start 속성을 넣어준다. 'nodemon app'은 app.js를 nodemon으로 실행한다는 뜻이다. nodemon 모듈은 코드 수정이 있을 때 서버를 자동으로 재시작한다. nodemon이 실행되는 콘솔에 rs를 입력해서 수동으로 재시작할 수도 있다.

 

npm i express
npm i -D nodemon

express와 nodemon 모듈을 설치해준다. 이때 nodemon은 개발용 모듈로써 설치한다.

 

  • app.js
const express = require('express');

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

app.get('/', (req,res)=>{
    res.send('Hello, Express');
});

app.listen(app.get('port'), ()=>{
    console.log(`${app.get ('port')} listening...`);
});

Express 모듈을 실행해 app 변수에 할당한다. express 내부에 http 모듈이 내장되어 있어서 서버의 역할을 할 수 있다.

app.set('port', 포트)로 서버가 실행될 포트를 설정한다. process.env 객체에 PORT 속성이 있다면 그 값을 사용하고, 없다면 기본값으로 3000번 포트를 이용하도록 코드에 반영되어 있다. 이렇게 app.set(키, 값)을 사용해서 데이터를 저장할 수 있다. 저장한 값을 app.get(키)로 가져올 수 있다.

app.get(주소, 라우터)는 주소에 대한 GET 요청이 올 때 어떤 동작을 할지 적는 부분이다. 매개변수 req는 요청에 관한 정보가 들어있는 객체이고, res는 응답에 관한 정보가 들어 있는 객체이다. 현재 GET '/' 요청 시 응답으로 'Hello, Express'를 전송한다. Express에서는 res.write나 res.end 대신 res.send를 사용한다.

GET 요청 외에도 POST, PUT, PATCH, DELETE, OPTIONS에 대한 라우터를 위한 app.post, app.put, app.patch, app.delete, app.options  메서드가 존재한다.

listen을 하는 부분은 http 웹 서버와 동일하다. 포트를 가져올 때 ap.get('port')로 가져왔다.

$ npm start

> learn-express@0.0.1 start H:\VSCodeFiles\JS\practice\part6\learn-express
> nodemon app

[nodemon] 2.0.6
[nodemon] to restart at any time, enter `rs`
[nodemon] watching path(s): *.*
[nodemon] watching extensions: js,mjs,json
[nodemon] starting `node app.js`
3000 listening...

package.json의 script에 등록해 놓은 'start'를 사용하여 nodemon을 실행한다. 위처럼 app.js가 실행된다.

3000번 포트 접속

 

  • index.html
<html>
    <head>
        <meta charset="UTF-8"/>
        <title>익스프레스 서버</title>
    </head>
    <body>
        <h1>익스프레서</h1>
        <p>배워봅시다.</p>
    </body>
</html>
  • app.js
const express = require('express');
const path = require('path');

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

app.get('/', (req,res)=>{
    res.sendFile(`${__dirname}/index.html`);

    // path 모듈 사용
    res.sendFile(path.join(__dirname, '/index.html'));

});

app.listen(app.get('port'), ()=>{
    console.log(`${app.get ('port')} listening...`);
});

단순 문자열 대신 HTML로 응답하기 위해 res.sendFile을 사용할 수 있다. 이때 파일의 경로를 path 모듈을 사용해서 지정할 수도 있다.


6.2 자주 사용하는 미들웨어

미들웨어는 Express의 핵심이다. 요청과 응답 중간에 위치하여 미들웨어라고 부른다. 미들웨어는 요청과 응답을 조작하여 기능을 추가하기도 하고, 나쁜 요청을 걸러내기도 한다.

미들웨어는 app.use로 사용한다.

  • app.js
const express = require('express');
const path = require('path');

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

app.use((req, res, next)=>{
    console.log('요청마다 실행됩니다.');
    next();
});

app.get('/', (req,res, next)=>{
    console.log('GET / 요청에서만 실행됩니다.');
    next();
}, (req, res)=>{
    throw new Error('에러는 에러 처리 미들웨어로 갑니다.');
});

app.use((err, req, res, next)=>{
    console.error(err);
    res.status(500).send(err.message);
});

app.listen(app.get('port'), ()=>{
    console.log(`${app.get ('port')} listening...`);
});

app.use에 매개변수가 (req, res, next)인 함수를 넣으면 된다. 미들웨어는 위에서부터 아래 순서로 실행되면서 요청과 응답 사이에 특별한 기능을 추가할 수 있다. next라는 세 번재 매개변수를 사용하여 next() 함수를 이용해 다음 미들웨어로 넘어갈 수 있다. next()를 실행하지 않으면 다음 미들웨어가 실행되지 않는다.

주소를 첫 번째 인수로 넣어주지 않으면 미들웨어는 모든 요청에서 실행되고, 주소를 넣으면 해당하는 요청에서만 실행된다고 보면 된다.

app.use(미들웨어) 모든 요청에서 미들웨어 실행
app.use('/abc', 미들웨어) abc로 시작하는 요청에서 미들웨어 실행
app.post('/abc', 미들웨어) abc로 시작하는 POST 요청에서 미들웨어 실행

 

app.use나 app.get 같은 라우터에 미들웨어를 여러 개 장착할 수 있다. 위의 코드에서는 app.get 라우터에 미들웨어가 두 개 연결되어 있다. 이때도 next()를 호출해야 다음 미들웨어로 넘어갈 수 있다.

현재 app.get('/')의 두 번째 미들웨어에서 에러가 발생하고, 이 에러는 그 아래에 있는 에러 처리 미들웨어인 (err, req, res, next)에 전달된다. 에러 처리 미들웨어는 매개변수가 (err, req, res, next)로 네 개다. 모든 매개변수를 사용하지 않아도 반드시 네 개여야 하며 첫 번째 매개변수에 에러에 관한 정보가 담겨있다. 그리고 res.status 메서드로 HTTP 상태 코드를 지정할 수 있다. 기본값은 200(성공)이다.

에러처리 미들웨어

 

npm i morgan cookie-parser express-session dotenv

실무에 자주 사용하는 패키지들을 설치한다. dotenv를 제외한 다른 패키지는 미들웨어다. dotenv는 process.env를 관리하기 위해 설치.

  • app.js
const express = require('express');
const morgan = require('morgan');
const cookieParser = require('cookie-parser');
const session = require('express-session');
const dotenv = require('dotenv');
const path = require('path');

dotenv.config();
const app = express();
app.set('port', process.env.PORT || 3000);

app.use(morgan('dev'));
app.use('/', express.static(path.join(__dirname, 'public')));
app.use(express.json());
app.use(express.urlencoded({extended: false}));
app.use(cookieParser(process.env.COOKIE_SECRET));
app.use(session({
    resave: false,
    saveUninitialized: false,
    secret: process.env.COOKIE_SECRET,
    cookie:{
        httpOnly: true,
        secure: false,
    },
    name: 'session-cookie',
}));

app.use((req, res, next)=>{
    console.log('모든 요청에 다 실행됩니다.');
    next();
});

app.use((req, res, next)=>{
    console.log('요청마다 실행됩니다.');
    next();
});

app.get('/', (req,res, next)=>{
    console.log('GET / 요청에서만 실행됩니다.');
    next();
}, (req, res)=>{
    throw new Error('에러는 에러 처리 미들웨어로 갑니다.');
});

app.use((err, req, res, next)=>{
    console.error(err);
    res.status(500).send(err.message);
});

app.listen(app.get('port'), ()=>{
    console.log(`${app.get ('port')} listening...`);
});

 

  • .env
COOKIE_SECRET=cookiesecret

app.js를 위처럼 수행하고, .env 파일도 생성한다.

설치한 패키지들을 불러온 뒤(const 이름 = require('패키지')) app.use에 연결한다.

dotenv 패키지는 .env 파일을 읽어서 process.env로 만든다. 그래서 실행하면 process.env.COOKIE_SECRET에 cookiesecret 값이 할당된다. '키=값' 형식으로 추가하면 된다. 보안과 설정의 편의성을 위해서 .env 같은 별도의 파일을 사용한다.

 

6.2.1 morgan

> nodemon app

[nodemon] 2.0.6
[nodemon] to restart at any time, enter `rs`
[nodemon] watching path(s): *.*
[nodemon] watching extensions: js,mjs,json
[nodemon] starting `node app.js`
3000 listening...
모든 요청에 다 실행됩니다.
요청마다 실행됩니다.
GET / 요청에서만 실행됩니다.
Error: 에러는 에러 처리 미들웨어로 갑니다.
    (에러 내용 생략)
GET / 500 9.197 ms - 50

app.js를 실행하면 아래에 'ms - 50'같은 추가적인 로그를 확인할 수 있다. morgan 미들웨어로 인해 출력된 것이며 요청과 응답에 대한 정보를 콘솔에 기록한다.

morgan 미들웨어는 다음과 같이 사용한다.

app.use(morgan('dev'));

인수로 dev 외에 combined, common, short, tiny 등을 넣을 수 있다. 인수를 바꾸면 로그가 달라진다. 개발 환경에서는 dev, 배포 환경에서는 combined를 자주 사용한다고 한다.(저자가)

dev 모드 기준으로 'GET / 500 9.197 ms -50'은 각각 [HTTP 메서드] [주소] [HTTP 상태 코드] [응답 속도] - [응답 바이트]를 의미한다. 요청과 응답을 한눈에 확인할 수 있다.

 

6.2.2 static

static 미들웨어는 정적인 파일들을 제공하는 라우터 역할을 한다. 기본적으로 제공되므로 따로 설치가 필요 없이 express 객체에서 사용하면 된다. 아래와 같이 사용한다.

app.use('/', express.static('실제 경로'));
app.use('/', express.static(path.join(__dirname, 'public')));

함수의 인수로 정적 파일들이 담겨 있는 폴더를 지정하면 된다. 코드에서는 public 폴더가 지정되어 있다. 예를 들어 public/stylesheets/style.css는 http://localhost:3000/stylesheets/style.css로 접근할 수 있다. public 폴더를 만들고 css나 js, 이미지 파일들을 public 폴더에 넣으면 브라우저에서 접근할 수 있게 된다.

실제 서버의 폴더 경로에는 'public'이 들어 있지만, 요청 주소에는 'public'이 없다. 서버의 폴더 경로와 요청 경로가 다르므로 외부인이 서버의 구조를 쉽게 파악할 수 없다. 이는 보안에 큰 도움이 된다.

또한, 정적 파일들을 알아서 제공해주므로 4.3절처럼 fs.readFile로 파일을 직접 읽어서 전송할 필요가 없다. 만약 요청 경로에 해당 파일이 없으면 내부적으로 알아서 next()를 호출한다. 만약 파일을 발견했다면 다음 미들웨어는 실행되지 않는다. 응답으로 파일을 보내고 next()를 호출하지 않기 때문.

 

6.2.3 body-parser

요청의 본문에 있는 데이터를 해석하여 req.body 객체로 만들어주는 미들웨어다. 보통 form 데이터나 AJAX 요청의 데이터를 처리한다. 단, 멀티파트(이미지, 동영상, 파일) 데이터는 처리하지 못한다. 그 경우에는 뒤에 나오는 multer 모듈을 사용하면 된다.

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

위처럼 사용한다. 익스프레스 4.16.0 버전부터 body-parser 미들웨어의 일부 기능이 익스프레스에 내장되었으므로 따로 설치할 필요가 없다. 단, JSON과 URL-encoded 형식의 데이터 외에 Raw, Text 형식의 데이터를 해석하기 위해서는 따로 설치해 주어야할 필요가 있다.

Raw는 요청의 본문이 Buffer일 때, text는 텍스트 데이터일 때 해석하는 미들웨어다. 버퍼나 텍스트 요청을 처리할 필요가 있다면 body-parser를 설친 후 아래와 같이 추가한다.

npm i body-parser

const bodyParser = require('body-parser')
app.use(bodyParser.raw());
app.use(bodyParer.text());

JSON은 JSON 형식의 데이터 전달 방식이고, URL-encoded는 주소 형식으로 데이터를 보내는 방식이다. Form 전송은 URL-encoded 방식을 주로 사용한다. urlencode 메서드를 보면 {extended: false}라는 옵션이 들어있다. 이 옵션이 false이면 노드의 querystring 모듈을 사용하여 쿼리스트링을 해석하고, true면 qs 모듈을 사용하여 쿼리스트링을 해석한다. qs 모듈은 내장 모듈이 아니라 npm 패키지이며, querystring 모듈의 기능을 좀 더 확장한 모듈이다.

body-parser를 사용하면 req.on('data')와 req.on('end')로 스트림을 사용하지 않고 간단하게 처리할 수 있다.

 

6.2.4 cookie-parser

cookie-parser는 request에 동봉된 쿠키를 해석해 req.cookies 객체로 만든다. 사용방법은 아래와 같다.

app.use(cookieParser(비밀키));

해석된 쿠키들은 req.cookies 객체에 들어있다. 유효 기간이 지난 쿠키는 알아서 걸러낸다.

첫 번째 인수로 비밀 키를 넣어줄 수 있다. 서명된 쿠키가 있는 경우, 제공한 비밀 키를 통해 해당 쿠키가 내 서버가 만든 쿠키임을 검증할 수 있다. 쿠키는 클라이언트에서 위조하기 쉬우므로 비밀 키를 통해 만들어낸 서명을 쿠키 값 뒤에 붙인다. 서명이 붙으면 쿠키가 keq=value.sign과 같은 모양이 된다. 서명된 쿠키는 req.cookies 대신 req.signedCookies 객체에 들어있다.

쿠키를 생성/제거하기 위해서는 res.cookie, res.clearCookie 메서드를 사용해야 한다. res.cookie(키, 값, 오셥) 형식으로 사용해야 한다. 옵션은 4.3절에서 살펴본 쿠키 옵션과 동일하다. domain, expires, httpOnly, maxAge, path, secure 등이 있다.

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

쿠키를 지우려면, 키와 값 외에 옵션도 정확히 일치해야 쿠키가 지워진다. 단, expires나 maxAge 옵션은 일치할 필요가 없다. 옵션 중에 signed라는 옵션을 true로 설정하면 쿠키 뒤에 서명이 붙게된다. 내 서버가 쿠키를 만들었다는 것을 검증할 수 있으므로 대부분의 경우 서명 옵션을 켜두는 것이 좋다고 한다. 서명을 위한 비밀 키는 cookieParser 미들웨어에 인수로 넣은 process.env.COOKIE_SECRET이 된다.

6.2.5 express-session

세션 관리용 미들웨어다. 로그인 등의 이유로 세션을 구현하거나 특정 사용자를 위한 데이터를 임시적으로 저장해둘 때 매우 유용하다. 세션은 사용자별로 req.session 객체 안에 유지된다.

app.use(session({
	resave: false,
    saveUninitialized: false,
    secret: process.env.COOKIE_SECRET,
    cookie: {
    	httpOnly: true,
        secure: false,
    },
    name: 'session-cookie',
}));

express-session 1.5 버전 이전에는 내부적으로 cookie-parser를 사용하고 있어서 cookie-parser 미들웨어보다 뒤에 위치해야 했지만, 1.5 버전 이후부터는 사용하지 않게 되어 순서가 상관없어졌다. 그래도 현재 어떤 버전을 사용하고 있는지 모른다면 cookie-parser 미들웨어 뒤에 놓는 것이 안전하다고 한다.

express-session은 인수로 세션에 대한 설정을 받는다. resave는 요청이 올 때 세션에 수정 사항이 생기지 않더라도 세션을 다시 저장할지 설정하는 것이고, saveUninitialized는 세션에 저장할 내역이 없더라도 처음부터 세션을 생성할지 설정하는 것이다.

express-session은 세션 관리 시 클라이언트에 쿠키를 보낸다. 안전하게 쿠키를 전송하려면 쿠키에 서명을 추가해야 하고, 쿠키를 서명하는데 secret의 값이 필요하다. cookie-parser의 secret과 같게 설정하는 것이 좋다고 한다. 세션 쿠키의 이름은 name 옵션으로 설정한다. 기본 이름은 connect.sid이다.

cookie 옵션은 세션 쿠키에 대한 설정이다. maxAge, domain, path, expires, sameSite, httpOnly, secure 등 일반적인 쿠키 옵션이 모두 제공된다. httpOnly를 true로 설정해두면 클라이언트에서 쿠키를 확인하지 못한다. secure는 false로 하면 https가 아닌 환경에서 사용할 수 있다. 배포 시에는 https를 적용하고 secure도 true로 설정하는 것이 좋다.

코드에는 안보이지만, store라는 옵션도 있다. 현재는 메모리에 세션을 저장하도록 되어 있다. 서버를 재시작하면 메모리가 초기화되어 세션이 모두 사라진다. 따라서 배포 시에는 store에 Database를 지정하여 세션을 유지하는게 좋다. 보통 레디스가 자주 쓰인다. 레디스는 15.1.8절에서 설명한다.

req.session.name = 'person'; // 세션 등록
req.sessionID; // 세션 아이디 확인
req.session.destroy(); // 세션 모두 제거

express-session으로 만들어진 req.session 객체에 값을 대입하거나 삭제해서 세션을 변경할 수 있다. 나중에 세션을 한 번에 삭제하려면 req.session.destroy 메서드를 호출하면 된다. 현제 세션의 아이디는 req.sessionID로 확인할 수 있다. 세션을 강제로 저장하기 위해 req.session.save 메서드가 존재하지만, 일반적으로 요청이 끝날 때 자동으로 호출되므로 직접 save 메서드를 호출할 일은 거의 없다.

express-session에서 서명한 쿠키 앞에는 's:'이 붙는다. 실제로는 encodeURIComponent 함수가 실행되어 's%3A'가 된다.


6.4 req, res 객체 살펴보기

익스프레스의 req, res 객체는 http 모듈의 req, res 객체를 확장한 것이다. 그래서 기존 http 모듈의 메서드도 사용할수 있고, 익스프레스가 추가한 메서드나 속성을 사용할 수도 있다.

  • req.app : req 객체를 통해 app 객체에 접근할 수 있다. req.app.get('port')와 같은 식으로 사용할 수 있다.
  • req.body : body-parser 미들웨어가 만드는 요청의 본문을 해석한 객체
  • req.cookies : cookie-parser 미들웨어가 만드는 req의 쿠키를 해석한 객체
  • req.ip : 요청의 ip 주소가 담겨있음
  • req.params : 라우트 매개변수에 대한 정보가 담긴 객체
  • req.signedCookies : 서명된 쿠키들이 req.cookies 대신에 담겨있는 객체
  • req.get(헤더 이름) : 헤더의 값을 가져오고 싶을 때 사용하는 메서드

 

  • res.app : req.app처럼 res 객체를 통해 app 객체에 접근할 수 있음
  • res.cookie(키, 값, 옵션) : 쿠키를 설정하는 메서드
  • res.clearCookie(키, 값, 옵션) : 쿠키를 제거하는 메서드
  • res.end() : 데이터 없이 응답을 보냄
  • res.json(JSON) : JSON 형식의 응답을 보냄
  • res.redirect(주소) : 리다이렉트할 주소와 함께 응답을 보냄
  • res.render(뷰, 데이터) : 다음 절에서 다룰 템플릿 엔진을 렌더링해서 응답할 때 사용하는 메서드
  • res.send(데이터) : 데이터와 함께 응답을 보냄. 데이터는 문자열일 수도 있고 HTML, Buffer, 객체나 배열일 수도 있다.
  • res.sendFile(경로) : 경로에 위치한 파일을 응답함
  • res.set(헤더, 값) : 응답의 헤더를 설정함
  • res.status(코드) : 응답 시의 HTTP 상태 코드를 지정함

req나 res 객체의 메서드는 아래와 같이 메서드 체이닝을 지원하는 경우가 많다. 메서드 체이닝을 이용하면 코드양을 줄일 수 있다.

res
    .status(201)
    .cookie('test', 'test')
    .redirect('/admin');

6.5 템플릿 엔진 사용하기

 

Comments