본문 바로가기

WINK-(Web & App)/Express.js (Node.js) 스터디

[2023 백엔드 스터디] 이지원 #2주차 - 6.1~6.2장 Express 웹 서버 만들기

반응형

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

실습을 위해 package.json을 다음과 같이 작성한다.
{
  "name": "learn-express",
  "version": "0.0.1",
  "description": "익스프레스를 배우자",
  "main": "app.js",
  "scripts": {
    "start": "nodemon app"
  },
  "author": "ZeroCho",
  "license": "MIT"
}

필요한 패키지를 설치한다.

$ npm i express
$ npm i -D nodemon

 

서버 코드에 수정 사항이 생길 때마다 매번 서버를 재시작하기는 귀찮다.
nodemon 모듈은 이 작업을 자동화해준다.
따라서 앞으로 서버 코드를 수정하면 nodemon이 서버를 자동으로 재시작하며, nodemon이 실행되는 콘솔에 rs를 입력해서 수동으로 재시작할 수도 있다.
nodemon은 개발용으로만 사용할 것을 권장한다.
배포 후에는 서버 코드가 빈번하게 변경될 일이 없으므로 nodemon을 사용하지 않아도 된다.

 

app.js에 다음 코드를 작성한다.

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

 

Express 모듈을 실행해 app 변수에 할당한다

app.set('port', 포트)로 서버가 실행될 포트를 설정한다.

app.set(키, 값)을 사용해서 데이터를 저장할 수 있다.

app.get(키)로 데이터를 가져올 수 있다.

app.get(주소, 라우터)는 주소에 대한 GET 요청이 올 때 어떤 동작을 할지 적는 부분이다.

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

 

$ npm start

 

서버를 실행하면 Hello, Express라는 화면이 표시된다.

 

단순한 문자열 대신 HTML로 응답하고 싶다면 res.sendFile 메서드를 사용하면 된다. 단, 파일의 경로를 path 모듈을 사용해서 지정해야 한다.
[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.send('Hello, Express');
  res.sendFile(path.join(__dirname, '/index.html'));
});

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

 

 

 

6.2 자주 사용하는 미들웨어

 

미들웨어는 익스프레스의 핵심이다.

요청과 응답의 중간(middle)에 위치하기 때문에 미들웨어라고 불린다.

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

미들웨어는 app.use와 함께 사용한다.

 

[app.js]

...
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'), () => {
...
app.use(미들웨어) 모든 요청에서 미들웨어 실행
app.use('/abc', 미들웨어) abc로 시작하는 요청에서 미들웨어 실행
app.post('/abc', 미들웨어) abc로 시작하는 POST 요청에서 미들웨어 실행

일반 미들웨어

매개변수가 req, res, next인 함수

 

에러처리 미들웨어

매개변수가 err, req, res, next인 함수

 

서버에 접속하면 콘솔에 다음과 같이 표시된다.

[console]

모든 요청에 다 실행됩니다.
GET / 요청에서만 실행됩니다.
Error: 에러는 에러 처리 미들웨어로 갑니다.
...

 

미들웨어를 통해 요청과 응답에 다양한 기능을 추가할 수 있다.

실무에 자주 사용되는 패키지들은 다음과 같다.

 

$ npm i morgan cookie-parser express-session dotenv

dotenv 패키지는 .env 파일을 읽어서 process.env로 만든다.

process.env를 별도의 파일로 관리하는 이유는 보안과 설정의 편의성 때문이다.

소스 코드가 유출되더라도 .env 파일만 잘 관리하면 비밀 키는 지킬 수 있다.

 

6.2.1 morgan

morgan 미들웨어는 다음과 같은 로그를 관리/출력해준다.

[console]

3000 번 포트에서 대기 중
모든 요청에 다 실행됩니다.
GET / 요청에서만 실행됩니다.
Error: 에러는 에러 처리 미들웨어로 갑니다.
// 에러 스택 트레이스 생략
GET / 500 7.409 ms - 50

 

6.2.2 static

static 미들웨어는 정적인 파일들을 제공하는 라우터 역할을 한다.

app.use('요청 경로', express.static('실제 경로'));
app.use('/', express.static(path.join(__dirname, 'public')));​
함수의 인수로 정적 파일들이 담겨 있는 폴더를 지정한다.
예를 들어 public/stylesheets/style.css는 http://localhost:3000/stylesheets/style.css로 접근할 수 있다.
public 폴더를 만들고 나서 css나 js, 이미지 파일들을 public 폴더에 넣으면 브라우저에서 접근할 수 있게 된다.
서버의 폴더 경로와 요청 경로가 다르므로 외부인이 서버의 구조를 쉽게 파악할 수 없다.
이는 보안에 큰 도움이 된다.

 

6.2.3 body-parser

요청의 본문에 있는 데이터를 해석해서 req.body 객체로 만들어주는 미들웨어

보통 폼 데이터나 AJAX 요청의 데이터를 처리한다.
단, 멀티파트(이미지, 동영상, 파일) 데이터는 처리하지 못한다.
이 경우에는 뒤에 나오는 multer 모듈을 사용하면 된다.
app.use(express.json());
app.use(express.urlencoded({ extended: false }));

익스프레스 4.16.0 버전부터 body-parser 미들웨어의 일부 기능이 익스프레스에 내장되었으므로 따로 설치할 필요가 없다.

단, body-parser는 JSON과 URL-encoded 형식의 데이터 외에도 Raw, Text 형식의 데이터를 추가로 해석할 수 있다.

 

body-parser는 내부적으로 스트림을 처리해 req.body에 추가한다.
예를 들어, JSON 형식으로 { name: 'zerocho', book: 'nodejs' }를 본문으로 보낸다면 req.body에 그대로 들어간다. URL-encoded 형식으로 name=zerocho&book=nodejs를 본문으로 보낸다면 req.body에 { name: 'zerocho', book: 'nodejs' }가 들어간다.

 

6.2.4 cookie-parser

cookie-parser는 요청에 동봉된 쿠키를 해석해 req.cookies 객체로 만든다.

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

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

예를 들어 name=zerocho 쿠키를 보냈다면 req.cookies는 { name: 'zerocho' }가 된다. 유효 기간이 지난 쿠키는 알아서 걸러낸다.

 

인수

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

서명된 쿠키가 있는 경우, 제공한 비밀 키를 통해 해당 쿠키가 내 서버가 만든 쿠키임을 검증할 수 있다.

쿠키는 클라이언트에서 위조하기 쉬우므로 비밀 키를 통해 만들어낸 서명을 쿠키 값 뒤에 붙인다.

서명된 쿠키는 req.cookies 대신 req.signedCookies 객체에 들어 있다.

res.cookie('name', 'zerocho', { 
  expires: new Date(Date.now() + 900000),
  httpOnly: true, 
  secure: true,
});
res.clearCookie('name', 'zerocho', { 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',
}));

resave

요청이 올 때 세션에 수정 사항이 생기지 않더라도 세션을 다시 저장할지 설정

 

saveUninitialized

세션에 저장할 내역이 없더라도 처음부터 세션을 생성할지 설정

 

maxAge, domain, path, expires, sameSite, httpOnly, secure 등 일반적인 쿠키 옵션이 모두 제공된다.

 

req.session.destroy() : 세션 삭제

req.sessionID(또는 req.session.id) : 현재 세션 아이디

req.session.save() : 세션 강제 저장

 

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

 

미들웨어는 req, res, next를 매개변수로 갖는 함수(에러 처리 미들웨어만 예외적으로 err, req, res, next를 가진다)로서 app.use app.get, app.post 등으로 장착한다.

특정한 주소의 요청에만 미들웨어가 실행되게 하려면 첫 번째 인수로 주소를 넣으면 된다.

동시에 여러 개의 미들웨어를 장착할 수도 있으며, 다음 미들웨어로 넘어가려면 next 함수를 호출해야 한다.

 

next를 호출하지 않는 미들웨어는 res.send나 res.sendFile 등의 메서드로 응답을 보내야 한다.
만약 next도 호출하지 않고 응답도 보내지 않으면 클라이언트는 응답을 받지 못해 하염없이 기다리게 된다.

 

미들웨어 간에 데이터를 전달하는 방법

세션을 사용한다면 req.session 객체에 데이터를 넣어도 되지만, 세션이 유지되는 동안에 데이터도 계속 유지된다는 단점이 있다.

만약 요청이 끝날 때까지만 데이터를 유지하고 싶다면 res.locals 객체에 데이터를 넣어두면 된다.

현재 요청이 처리되는 동안 res.locals 객체를 통해 미들웨어 간에 데이터를 공유할 수 있다.

새로운 요청이 오면 res.locals는 초기화된다.

 

app.set과의 차이
app.get 또는 req.app.get으로 어디서든지 데이터를 가져올 수 있다.
하지만 app.set은 익스프레스에서 전역적으로 사용되므로 하나의 요청 안에서만 유지되어야 하는 값을 넣기에는 부적절하다.
res.locals 객체는 하나의 요청 안에서만 유지되므로 res.locals 객체를 통해 요청에 종속되는 데이터를 전달하는 것이 좋다.

 

6.2.7 multer

이미지, 동영상 등을 비롯한 여러 가지 파일을 멀티파트 형식으로 업로드할 때 사용하는 미들웨어

 

const multer = require('multer');

const upload = multer({
  storage: multer.diskStorage({
    destination(req, file, done) {
      done(null, 'uploads/');
    },
    filename(req, file, done) {
      const ext = path.extname(file.originalname);
      done(null, path.basename(file.originalname, ext) + Date.now() + ext);
    },
  }),
  limits: { fileSize: 5 * 1024 * 1024 },
});

 

storage

어디(destination)에 어떤 이름(filename)으로 저장할지 설정

limits

업로드에 대한 제한 사항을 설정

 

 

single 미들웨어

단일 파일 업로드

app.post('/upload', upload.single('image'), (req, res) => {
  console.log(req.file, req.body);
  res.send('ok');
});

 

array 미들웨어

여러 파일 업로드

app.post('/upload', upload.array('many'), (req, res) => {
  console.log(req.files, req.body);
  res.send('ok');
});

 

fields 미들웨어

input 태그나 폼 데이터의 키가 다른 경우

app.post('/upload',
  upload.fields([{ name: 'image1' }, { name: 'image2' }]),
  (req, res) => {
    console.log(req.files, req.body);
    res.send('ok');
  },
);

 

반응형