본문 바로가기

Back-End/Node.js

[Node.js 교과서] 6. 익스프레스 웹 서버 만들기

반응형

노드로만 웹 서버를 만들 때 코드가 보기 좋지 않고 확장성도 떨어진다. 

npm에서는 서버를 제작하는 과정에서의 불편함을 해소하고 편의 기능을 추가한 웹 서버 프레임워크가 있는데, 이 중에서 가장 대표적인 것이 익스프레스(Express)이다.

 

익스프레스는 http 모듈의 요청(req), 응답(res) 객체에 추가 기능들을 부여했다.

기존 메소드들에 편리한 메소드들을 추가하여 기능을 보완하였고,

코드를 분리하기 쉽게 만들었기 때문에 관리하기에도 용이하다.

그리고 if문으로 요청 메소드와 주소를 구별하지 않아도 된다.

 

 

1. 익스프레스 프로젝트 시작하기

learn-express 폴더를 생성하자.

 

1.1 package.json 생성하기

항상 package.json을 가장 먼저 생성해야 한다.

package.json을 생성해주는 npm init 명령어를 콘솔에서 작성해도 되고, 혹은 직접 파일을 생성해도 된다.

// package.json
{
  "name": "learn-express",
  "version": "1.0.0",
  "description": "익스프레스를 배우자",
  "main": "app.js",
  "scripts": {
    "start": "nodemon app"
  },
  "author": "nno3onn",
  "license": "MIT",
}

아래 코드를 작성하여 express와 nodemon을 설치하자.

$ npm i express
$ npm i -D nodemon

 

nodemon

scripts 부분에 start 속성은 잊지 않고 작성한다.

nodemon app을 작성하면, app.js를 nodemon으로 실행한다는 의미이다. 서버 코드에 수정 사항이 생길 때마다 매번 서버를 재시작하기 귀찮으므로 nodemon 모듈로 서버를 자동으로 재시작한다.

nodemon이 실행되는 콘솔에 rs를 입력하여 수동으로 재시작할 수도 있다.

nodemon은 개발용으로만 사용하는 것이 권장된다. 배포 후에는 서버 코드가 빈번하게 변경될 일이 없기 때문에 nodemon을 사용하지 않아도 된다.

 

 

1.2 서버(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'), '번 포트에서 대기 중');
});

 

코드를 하나씩 뜯어 살펴보자.

const express = require('express');

const app = express();

Express 모듈을 실행하고 app변수에 할당한다. 익스프레스 내부에 http 모듈이 내장되어 있으므로 서버의 역할을 할 수 있다.

 

app.set(키, 포트값)

서버가 실행될 포트를 설정한다.

키 값에 포트값 데이터를 저장할 수 있다. 나중에 데이터를 app.get(키)로 가져올 수 있다.

app.set('port', process.env.PORT || 3000);
  • process.env 객체에 PORT 속성이 있으면 그 값을 사용하고, 없으면 기본값으로 3000번 포트를 이용한다.

 

app.get(주소, 라우터)

주소에 대한 GET 요청이 올 때 어떤 동작을 할지 작성한다.

  • req: 요청에 관한 정보가 들어 있는 객체
  • res: 응답에 관한 정보가 들어 있는 객체

익스프레스에서는 res.write나 res.end 대신 res.send를 사용한다.

app.get('/', (req, res) => {
    res.send('Hello, express');
}
  • 현재 GET '/'으로 요청하면, 응답으로 'Hello, Express'를 전송한다.

 

app.listen(포트값, callback)

포트를 연결하고 서버를 실행한다. 서버가 실행되면 callback 함수를 동작시킨다.

app.listen(app.get('port'), () => {
    console.log(app.get('port'), '번 포트에서 대기 중');
});
  • 포트를 app.get('port')로 가져왔다. 포트를 연결하고 서버가 실행되면 '3000번 포트에서 대기 중'이 콘솔된다.

 

작성 후 localhost:3000 으로 접속하면 아래와 같이 뜬다.

localhost:3000 접속 화면

 

 

1.3 HTML로 응답하기

웹 페이지에 단순한 문자열 대신 HTML로 응답하고 싶다면 res.sendFile 메서드를 사용하면 된다. 단, 파일의 경로를 path  모듈을 사용하여 지정해야만 한다.

<!-- index.html -->
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Express Server</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.send('Hello, express');
    res.sendFile(path.join(__dirname, '/index.html'));
});

app.listen(app.get('port'), () => {
    console.log(app.get('port'), '번 포트에서 대기 중');
});

 

HTML 응답 화면

 

이제 익스프레스 서버에 다양한 기능을 추가해보자.

 

 

2. 자주 사용하는 미들웨어

2.1 미들웨어란?

웹에서 모든 요청(req)은 동일하지 않다.

예로 들면 '/'와 같이 아무 조건 없는 모든 요청일 수도 있고, '/abc' 즉 abc로 시작하는 요청일 수도 있다. 이처럼 각각 다른 종류의 요청에 따라 다른 응답(res)을 주도록 하는 게 미들웨어의 역할이다.

즉, 익스프레스는 요청이 들어올 때 이에 따른 응답을 보내주는데 응답을 보내주기 전에 미들웨어가 지정한 동작을 수행한다.

 request(요청) ➖ middleware(미들웨어)  response(응답)

 

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

컨베이어 벨트 위에 올라가 있는 request에 무언가 악세사리를 붙이거나 혹은 불량품을 걸러내는 역할을 한다고 생각하면 된다.

 

익스프레스는 기본적으로 일련의 미들웨어 함수 호출이다.

라우터(=app.get, app.post, app.put, app.delete)와 에러 핸들러(app.use((err, req, res, next)도 미들웨어의 일종이다. 따라서 미들웨어가 익스프레스의 핵심이자 전부라고 할 수 있다.

 

대표적인 미들웨어로는 Morgan, Compression, Session, Body-parser, Cookie-parser, Method-override, Cors, Multer 등이 있다. 필요에 따라 npm install로 따로 설치해야 한다.

  • Morgan : 익스프레스 프레임워크가 동작하면서 나오는 메시지들을 콘솔에 표시해준다.
  • Compression : 페이지를 압축해서 전송해준다.
  • Session : 세션을 사용할 수 있게 해준다.
  • Body-parser : 폼에서 전송되는 POST를 사용할 수 있게 해준다.
  • Cookie-parser : 쿠키를 사용할 수 있게 해준다.
  • Method-override : REST API에서 PUT와 DELETE 메소드를 사용할 수 있게 한다.
  • Cors : 크로스오리진(다른 도메인 간의 AJAX 요청)을 가능하게 한다.
  • Multer : 파일업로드를 할 때 주로 쓰인다.

 

간단하게 '안녕!'이라고 말하는 미들웨어를 만들어보자.

app.use((req, res, next) => {
   console.log('안녕!');
   next();
}));

이게 끝이다. 앞으로 모든 요청이 올 때마다 콘솔에 '안녕!'하고 외치게 된다.

 

라우팅

익스프레스의 또다른 장점은 라우팅이 편리하다는 것이다. 라우팅은 클라이언트에서 보내는 주소에 따라 다른 처리를 하는 것이다. 익스프레스는 REST API에 따라 처리하는데, 그 방법은 간단하다.

app.[REST메소드]('주소', 콜백함수)

앞에서 app.get('/', 콜백); 이 라우팅을 한 것이다. '/'주소로 GET 요청이 올 때 콜백하라고 등록한 거다.

app.get 외에도 app.post, app.put, app.delete 메소드를 사용한다.

  • put, delete 메소드를 사용하려면 앞에서 언급한 Method-override 패키지를 설치해야 한다.

 

:와일드 카드

'주소'부분은 정규 표현식도 가능하고, 콜론(:)을 이용한 와일드 카드도 가능하다. 와일드 카드란 어떤 주소가 들어오던지  처리하는 것이다. 

예로 들어 app.get('/post/:id')가 있는 경우에는 /post/a든 /post/b든 어느 주소가 들어와도 걸리게 된다.

또 와일드 카드를 사용할 때는 순서가 중요하다.

app.get('/post/:id', () => {});
app.get('/post/a', () => {});

이렇게 되어 있다면 /post/a에 요청이 들어오더라도 /post/:id에 먼저 걸린다. 즉, /post/a의 콜백 함수가 실행되지 않게 된다. 따라서 와일드 카드 라우터는 항상 다른 라우터들보다 뒤에 적어주는 게 좋다.

 

자, 그럼 이제 다시 미들웨어로 돌아와서 익스프레스 서버에 미들웨어를 연결해보자.

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

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

app.use((err, req, res, next) => {	// 미들웨어4 : 에러처리 미들웨어
    console.error(err);
    res.status(500).send(err.message);
});

app.listen(app.get('port'), () => {
...
  • localhost:3000/ 에 접속했을 때 콘솔 출력:
모든 요청에 다 실행됩니다.
GET / 요청에서만 실행됩니다.
Error: 에러는 에러 처리 미들웨어로 갑니다.
... // 에러 작성

localhost:3000 접속 화면

미들웨어는 매개변수로 req, res, next인 함수를 넣어 작성한다.

const app = express();를 한 후에 미들웨어1을 시작으로 위에서부터 아래로 순서대로 실행된다.

  • 순서: 미들웨어1 ➡ 미들웨어2 ➡ 미들웨어3 ➡ 미들웨어4

그래서 미들웨어2와 미들웨어1의 위치를 변경하면 미들웨어2 ➡ 미들웨어1 ➡ ... 순으로 실행되므로, 'GET / 요청에서만 실행됩니다.' ➡ '모든 요청에 다 실행됩니다' 순으로 콘솔이 출력된다.

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

 

next()

next()는 다음 미들웨어로 넘어가는 함수다. 작성하지 않으면 다음 미들웨어로 넘어가지 않고 해당 요청은 정지된 채로 방치되므로 반드시 작성해주어야 한다.

 

에러처리 미들웨어

미들웨어3은 에어처리 미들웨어이다. 매개변수로 err, req, res, next 네 개를 반드시 작성해야 한다.

에러처리 미들웨어로 넘어가려면 throw new Error()를 작성하거나 next(error)처럼 next의 인자로 error 정보를 넣어 넘겨준다.

만약 미들웨어2를 주석처리하면 에러(Error())가 발생하지 않으므로 미들웨어3은 실행되지 않는다.

 

 

이미 많은 사람들이 유용한 기능들을 패키지로 만들어두었다. 실무에 자주 사용되는 패키지들을 설치하자.

$ npm i morgan cookie-parser express-session dotenv

여기서 dotenv를 제외한 다른 모든 패키지들은 미들웨어다. dotenv는 process.env를 관리하기 위해 설치하였다.

 

app.js를 아래와 같이 수정하고 .env 파일도 생성하자. (파일명이 .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.get('/', (req, res, next) => {	// 미들웨어2
    console.log('GET / 요청에서만 실행됩니다.');
    next();
}, (req, res) => {	// 미들웨어3
    throw new Error('에러는 에러 처리 미들웨어로 들어갑니다.')
});

app.use((err, req, res, next) => {	// 미들웨어4 : 에러처리 미들웨어
    console.error(err);
    res.status(500).send(err.message);
});

app.listen(app.get('port'), () => {
    console.log(app.get('port'), '번 포트에서 대기 중');
});
// .env
COOKIE_SECRET=cookiesecret

 

 

2.2 morgan

요청과 응답에 대한 정보를 콘솔 출력한다. 요청과 응답을 한눈에 볼 수 있어서 편리하다.

구문

app.use(morgan('dev'));
app.use(morgan('combined'));
app.use(morgan('common'));
app.use(morgan('short'));
app.use(morgan('tiny'));
// dev
GET / 500 18.554 ms - 56
// combined
::1 - - [11/Apr/2021:06:52:41 +0000] "GET / HTTP/1.1" 500 56 "-" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.114 Safari/537.36"
// common
::1 - - [11/Apr/2021:06:52:41 +0000] "GET / HTTP/1.1" 500 56
// short
::1 - GET / HTTP/1.1 500 56 - 18.554 ms
// tiny
GET / 500 56 - 18.554 ms
  • 인수 : dev(개발 환경), combined(배포 환경), common, short, tiny

dev 모드 기준으로 각각 [HTTP 메서드] [주소] [HTTP 상태 코드] [응답 속도] - [응답 바이트] 를 의미한다.

 

2.3 static

(+) 정적 사이트와 동적 사이트

정적(static) 사이트 동적(dynamic) 사이트
하나의 html 파일이 하나의 웹 페이지만을 가지는 사이트
(html에 변화가 없다)
하나의 html 파일이 여러 개의 웹 페이지를 가지는 사이트
(필요에 따라 html의 내용이 변화한다)
웹 사이트의 route 구조가 파일 디렉토리 구조와 동일하다 웹 사이트의 route를 제작자가 직접 파일에 연결할 수 있다
파일에 접근 제한이 없다 (=public) router를 통해 개별 파일에 접근을 제한할 수 있다

 

static file folder

static-file(정적인 파일)이란 image, video, sound, javascript, css와 같이 파일 자체를 말한다.

서버에 저장된 static-file의 url을 프론트에서 사용하기 위해 static-file-folder를 설정하여 사용한다.

<img src=URL />을 통해 이미지를 HTML 문서에 넣었다면 이미지의 경로를 작성하는데, 여기서 URL이 static-file-folder의 경로가 된다.

 

정적인 파일 폴더를 설정하지 않고 서버에 저장된 파일의 경로를 클라이언트에서 사용한다면?

만약 이미지 파일이라면 해당 url(<img src=URL />)을 서버로 요청하지만,

서버에서 파일 경로를 찾지 못하므로 404 not found 응답과 이미지-엑박을 출력한다.

 

express.static

express.static('경로', [선택인자]) 정적인 파일을 제공하는 라우터 역할을 한다.

즉, 정적인 파일 폴더를 설정하여 서버에 저장된 정적인 파일의 url을 프론트에서 사용한다.

express에 내장된 미들웨어이므로 기본적으로 제공된다. 따라서 따로 설치할 필요 없이 express 객체 안에서 꺼내 장착하면 된다.

 

next()

만약 '요청 경로'에 해당하는 파일(='실제 경로')이 없다면 404 응답 대신에 알아서 내부적으로 next()를 호출하여 다음 미들웨어를 실행할 수 있다.

  • 따로 next() 함수를 작성할 필요없이 자동적으로 다음 미들웨어로 넘어가는 것이다.

만약 파일을 발견했다면 다음 미들웨어는 실행되지 않는다. 응답으로 next()를 호출하지 않고 파일을 보내기 때문이다.

 

구문

app.use('요청 경로', express.static('실제 경로'));

 

예제

예제 1

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

정적 파일들이 담겨 있는 public 폴더를 지정하였다.

public 폴더를 만들고 css나 js, 이미지 파일들을 public 폴더에 넣으면 브라우저에서 접근할 수 있다.

예를 들어 public/stylesheets/style.css는 http://localhost:3000/stylesheets/style.css 로 접근할 수 있다.

 

예제 2

프로젝트 구조

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


app.use(express.static(__dirname + '/public')); // 1번 미들웨어
app.use((req, res, next) => {	// 2번 미들웨어
    ...
    next();
});
app.get('/', (req, res) => { // 3번 미들웨어
    res.status(200).sendFile(__dirname + '/index.html');
});
  • 1번 미들웨어 : use() 메서드를 통해 static 폴더를 설정한다.
  • 2번 미들웨어 : 코드를 실행하고 next()로 다음 미들웨어인 3번 미들웨어로 넘어간다. 
    • 요청 경로가 따로 없으므로 모든 요청에서 실행된다.
    • next()를 입력하지 않으면 3번 미들웨어가 실행되지 않는다.
  • 3번 미들웨어 : 클라이언트의 요청에 HTML 파일로 응답한다. (res.sendFile(...))
<!-- index.html -->
<!DOCTYPE html>
<html lang="ko">
  <head>
    <title>NODE-PRAC2</title>
  </head>
  <body>
    <h1>NODE PRAC2 #6 express - static folder</h1>
    <img src="./node.jpg" alt="" /> 
  </body>
</html>

이미지 경로는 public 이하 경로로 작성하면 된다.

  • static 파일의 기본 경로는 public이기 때문에 public 이하 경로로 작성하면 된다.
  • 위의 경로는 서버에서 작성되었기 때문에 ./node.jpg 이다.
  • 클라이언트에서 서버의 이미지를 사용한다면 http://localhost:3000/node.jpg 가 된다.

localhost:3000/node.jpg에 접속했을 때

만약 app.use(express.static(__dirname)); 으로 작성했다면,

클라이언트에서 http://localhost:3000/public/node.jpg를 통해 동일한 서버의 이미지를 띄울 수 있다.

localhost:3000/public/node.jpg에 접속했을 때

(+) 참고 자료

 

static 미들웨어는 보안에 큰 도움이 된다.

왜냐하면 실제 서버의 폴더 경로에는 public이 있지만, 요청 주소에는 public이 들어있지 않기 때문이다.

즉, 서버의 폴더 경로와 요청 경로가 다르기 때문에 외부인이 서버의 구조를 쉽게 파악할 수 없다.

 

또한, 정적 파일들을 알아서 제공해주기 때문에 fs.readFile로 파일을 직접 읽어서 전송할 필요가 없다. 

  • 폴더 내에 특정 파일의 이름을 알고 있고, 그 특정 파일을 불러오고 싶을 경우에는 static 폴더 하위 경로를 작성하여 손쉽게 가져올 수 있다.
  • 하지만, 그저 폴더 내의 모~든 파일을 읽어오고 싶은 경우에는 fs.readFile() 메서드를 사용한다.

 

 

2.4 body-parser

폼 데이터나 AJAX 요청의 본문에 있는 데이터를 해석해서 req.body 객체로 만들어준다.

(단, 멀티파트(이미지, 동영상, 파일) 데이터는 처리하지 못한다. 이 데이터들을 처리하려면 multer 모듈을 사용하면 된다)

 

설치

익스프레스 4.16.0 버전부터는 익스프레스에 내장되었으므로 따로 설치할 필요가 없다.

 

  • JSON : JSON 형식의 데이터 전달 방식
  • URL-encoded : 주소 형식으로 데이터를 보내는 방식. 주로 폼 전송이 이 방식을 사용한다.

 

그러나 JSON, URL-encoded 포맷의 데이터가 아닌 Raw, Text 포맷의 데이터를 추가로 해석해야 할 경우에는 body-parser를 직접 설치해야 한다.

  • Raw : 요청의 본문이 버퍼 데이터일 때 해석하는 미들웨어
  • Text : 요청의 본문이 텍스트 데이터일 때 해석하는 미들웨어

다음 명령어를 입력하여 설치할 수 있다.

$ npm i body-parser

 

구문

1. JSON

app.use(express.json());

 

2. url-encoded

app.use(express.urlencoded({ extended: false }));
  • 옵션 { extended: false }
    • false : 노드의 querystring 모듈을 사용하여 쿼리스트링을 해석한다.
    • true : qs 모듈을 사용하여 쿼리스트링을 해석한다.

qs 모듈은 querystring 모듈의 기능을 좀 더 확장한 모듈로, 내장 모듈이 아니라 npm 패키지이다. 

 

3. Raw, Text의 경우

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

 

적용하기

4.2절에서 POST와 PUT 요청의 본문을 전달받으려면 req.on('data')와 req.on('end')로 스트림을 사용해야 했다.

하지만 body-parser를 사용하면 그럴 필요 없다. 패키지가 내부적으로 스트림을 처리하여 req.body에 추가한다.

 

예로 들어서, JSON 형식으로 { name: 'nno3onn', book: 'nodejs' } 를 본문으로 보낸 경우 req.body에 그대로 들어간다.

URL-encoded 형식으로 name=nno3onn&book=nodejs 를 본문으로 보낸 경우도 req.body에 { name: 'nno3onn', book: 'nodejs' } 가 들어간다.

 

 

2.5 cookie-parser

요청에 동봉된 쿠키를 해석하여 req.cookies 객체로 만든다.

4.3절의 parseCookies 함수와 기능이 비슷하다. 쿠키에 대한 이해가 부족하다면 아래 글의 4.3절을 참고하자.

2021.04.12 - [Web/Node.js] - 4. http 모듈로 서버 만들기

 

구문

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

해석된 쿠키들은 req.cookies 객체에 들어간다.

 

첫 번째 인수로 비밀 키를 넣어준다.

서명된 쿠키가 있는 경우,

 

 

적용하기

예로 들어 name=nno3onn 쿠키를 보냈다면 req.cookies는 { name: 'nno3onn' }가 된다.

유효기간이 지난 쿠키는 알아서 걸러낸다.

 

 

 

 

 

 

 

2.6 express-session

 

 

 

 

2.7 미들웨어의 특성 활용하기

 

 

 

 

 

2.8 multer

 

 

 

 

반응형