상세 컨텐츠

본문 제목

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

21-22/21-22 Node.js

by Kimpeep 2021. 11. 15. 13:00

본문

728x90

4장에서와 같이 웹 서버를 만들 때 코드가 복잡하고 확장성이 떨어지는 문제가 있다.

이를 불편함을 해소하고 편의 기능을 추가한 npm에서 제공하는 웹 서버 프레임워크가 있다.

익스프레스가 대표적이다.


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

  • 먼저 learn-express 폴더를 만든다.
  • 항상 package.json을 제일 먼저 생성해야 한다.

직접 파일을 만들어도 되고 명령어를 콘솔에서 호출해도 된다.

version이나 description, author license는 자유롭게 수정 가능하다.

$ npm i express
$ npm i -D nodemon
// package.json
{
    "name": "learn-express",
    "version": "0.0.1",
    "description": "익스프레스를 배우자",
    "main": "app.js",
    "scripts": {
        "start": "nodemon app"
    },
    "author": "ojo",
    "license": "MIT",
    "dependencies": {
        "express": "^4.17.1"
    },
    "devDependencies": {
        "nodemon": "^2.0.15"
    }
}
  • scripts부분에 start 속성은 꼭 넣어주어야 함.

nodemon app은 app.js를 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 변수에 할당. 익스프레스 내부에 http 모듈이 내장되어 있으므로 서버의 역할을 할 수 있음.
  • app.set('port', 포트): 서버가 실행될 포트를 설정. process.env 객체에 PORT 속성이 있으면 그 값을 사용하고, 없으면 디폴트 값으로 3000번 포트를 이용. (app.set(키, 값)으로 데이터를 저장할 수 있음. app.get(키)로 데이터를 가져올 수 있음.)
  • app.get(주소, 라우터): 주소에 대한 GET 요청이 올 때 실행할 동작을 적는 부분. 매개변수 req는 요청에 관한 정보, res는 응답에 관한 정보가 들어 있는 객체. (익스프레스에서는 res.write나 res.end 대신 res.send 사용)
  • app.listen(): http 웹 서버와 동일. 포트는 app.get('port')로 가져옴.

콘솔에 명령을 입력하면 http://localhost:3000으로 접속할 것이고 다음과 같은 페이지가 뜰 것이다.

$ npm start

콘솔 출력
localhost:3000 접속 화면

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

 

localhost:3000 접속 화면


6.2 자주 사용하는 미들웨어

  • 요청과 응답의 중간에 위치한 미들웨어는 익스프레스의 핵심
  • 라우터와 에러 핸들러는 미들웨어의 일종
  • 요청과 응답을 조작하여 기능을 추가하기도 하고, 나쁜 요청을 걸러내기도 함
  • 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'), '번 포트에서 대기 중');
});
  • app.use에 매개변수가 req, res, next인 함수를 넣음
  • 미들웨어는 위에서부터 아래로 순서대로 실행
  • next라는 매개변수는 다음 미들웨어로 넘어가는 함수
  • 주소를 첫번째 인수로 넣어주지 않으면 미들웨어는 모든 요청에서 실행, 주소를 넣으면 해당하는 요청에서만 실행

 

  • 현재 app.get('/')의 두 번째 미들웨어에서 에러가 발생하고, 이 에러는 그 아래에 있는 에러 처리 미들웨어에 전달
  • 에러 처리 미들웨어의 매개변수는 err, req, res, next (err에는 에러에 관한 정보를 담음)
  • 에러 처리 미들웨어를 직접 연결하지 않아도 기본적으로 익스프레스가 처리하지만 직접 연결해주는 것이 좋음
  • 특별한 경우가 아니면 가장 아래에 위치하는 것이 좋음

localhost:3000에 접속할 때 콘솔 출력 내용
&amp;amp;nbsp;localhost:3000 접속 화면


실무에 자주 사용하는 패키지들로 아래와 같은 패키지들이 있다.

$ npm i morgan cookie-parser express-session dotenv

margan, cookie-parser, express-session은 미들웨어이고, 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.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'), '번 포트에서 대기 중');
});
// .env
COOKIE_SECRET=cookiesecret
  • 설치했던 패키지들을 불러온 뒤 app.use에 연결
  • req, res, next 같은 매개변수들은 미들웨어 내부에 포함, next도 내부적으로 호출
  • dotenv 패키지는. env파일을 읽어서 process.env로 만듦
  • process.env를 별도의 파일로 관리하는 이유는 보안과 설정의 편의성 때문 (비밀키들을 소스 코드에 그대로 적어두면 소스 코드가 유출되었을 때 키도 같이 유출됨)

 

6.2.1 morgan

모든 요청에 다 실행됩니다.
GET / 요청에서만 실행됩니다.
Error: 에러는 에러 처리 미들웨어로 갑니다.
// 에러 스택 트레이스
GET / 500 21.878 ms - 50
  • 추가적인 로그인 GET / 500 21.878 ms - 50 로그는 morgan 미들웨어에서 나오는 것
  • 위 로그는 각각 [HTTP 메서드] [주소] [HTTP 상태 코드] [응답 속도] - [응답 바이트]를 의미
  • 요청과 응답에 대한 정보를 콘솔에 기록
  • app.use(morgan('dev')); 에 인수로 combined, common, short, tiny 등을 넣을 수 있고 로그가 달라짐

 

6.2.2 static

  • 정적인 파일들을 제공하는 라우터 역할
app.use('요청 경로', express.static('실제 경로'));
app.use('/', express.static(path.join(__dirname, 'public')));

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

 

6.2.3 body-parser

  • 요청의 본문에 있는 데이터를 해석해서 req.body 객체로 만들어주는 미들웨어
  • 보통 폼 데이터나 AJAX 요청의 데이터를 처리, 멀티파트(이미지, 동영상, 파일) 데이터는 처리하지 못함
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
  • 익스프레스 4.16.0 버전부터 일부 기능이 익스프레스에 내장되었으므로 따로 설치할 필요 없음
  • 단,  Raw, Text 형식의 데이터를 추가로 해석할 때에는 설치할 필요가 있음
  • Raw는 요청의 본문이 버퍼 데이터일 때, Text는 텍스트 데이터일 때 해석하는 미들웨어
$ npm i body-parser
const bodyParser = require('body-parser'); 
app.use(bodyParser.raw());
app.use(bodyParser.text());
  • 요청 데이터 종류로는 JSON과 URL_encoded이 있음
  • JSON은 JSON 형식의 데이터 전달 방식, URL-encoded는 주소 형식으로 데이터를 보내는 방식이며 폼 전송은 이 방식으로 주로 사용함
  • { extended: false }라는 옵션에서 false일때 querystring 모듈을 사용하여 쿼리 스트링을 해석하고, true면 npm 패키지에 포함되고 querystring 모듈보다 확장된 qs 모듈을 사용하여 쿼리 스트링을 해석
  • 4장에서 POST와 PUT 요청을 전달받을때 쓰던 req.on('data')와 req.on('end') 대신, body-parser 패키지가 내부적으로 처리하여 req.body에 추가

 

6.2.4 cookie-parser

  • 요청에 동봉된 쿠키를 해석해 req.cookies 객체로 만듦. 4장의 parseCookies 함수와 기능이 비슷함
app.use(cookieParser(비밀키));
  • 첫 번째 인자는 비밀키를 넣을 수 있고, 서명된 쿠키가 있는 경우, 이 비밀키를 통해 해당 쿠키가 내 서버가 만든 쿠키임을 검증할 수 있음
  • 쿠키는 클라이언트에서 위조하기 쉬우므로 비밀 키를 통해 만들어낸 서명을 쿠키 값 뒤에 붙임
  • 서명된 쿠키는 req.cookies 대신 req.signedCookies 객체에 들어 있음
  • 쿠키 생성/제거 메서드: res.cookie, res.clearCookie
res.cookie('name', 'ojo', {
  expires: new Date(Date.now() + 900000),
  httpOnly: true,
  secure: true,
});
res.clearCookie('name', 'ojo', { httpOnly: true, secure: true });
  • 쿠키를 지울 때, 키와 값 외에 옵션도 정확히 일치해야 쿠키가 지워짐. expires maxAge옵션은 일치하지 않아도 됨
  • signed라는 옵션을 true로 설정하면 쿠키 뒤에 서명이 붙음

 

6.2.5 express-session

  • 세션 관리용 미들웨어
  • 세션 관리시 클라이언트에 쿠키를 보냄 (4장의 세션 쿠키)
  • 로그인 등의 이유로 세션을 구현하거나 특정 사용자를 위한 데이터를 임시적으로 저장해둘 때 매우 유용
  • 세션은 사용자별로 req.session 객체 안에 유지
app.use(session({
  resave: false,
  saveUninitialized: false,
  secret: process.env.COOKIE_SECRET,
  cookie: {
    httpOnly: true,
    secure: false,
  },
  name: 'session-cookie',
}));
  • express-session은 인수로 세션에 대한 설정을 받음
  • resave: 요청이 올 때 세션에 수정 사항이 생기지 않더라도 세션을 다시 저장할지 설정
  • saveUninitialized: 세션에 저장할 내역이 없더라도 처음부터 세션을 생성할지 설정 
  • secret: 쿠키를 서명하는데 필요한 값. cookie-parser의 sercret과 같게 설정하는 것이 좋음
  • cookie: 세션 쿠키에 대한 설정. "httpOnly: true"로 클라이언트에서 쿠키를 확인하지 못하도록 했고, "secure: false"로 https가 아닌 환경에서도 사용할 수 있게 함
  • name: 세션 쿠키의 이름 설정
req.session.name = 'ojo'; // 세션 등록
req.sessionID; // 세션 아이디 확인
req.session.destroy(); // 세션 모두 제거

express-session으로 만들어진 req.session 객체에 값을 대입하거나 삭제해서 세션을 변경할 수 있다. req.session.destroy 메서드를 호출해서 세션을 한 번에 삭제할 수 있고, req.sessionID로 현재 세션의 아이디를 확인할 수 있다.

 

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

미들웨어의 특성을 총정리해보자.

  • req, res, next를 매개변수로 가지는 함수 (에러 처리 미들웨어는 err, req, res, next)
  • app.use, app.get, app.post 등으로 장착
  • 특정한 주소의 요청에만 미들웨어가 실행되게 하려면 첫 번째 인수로 그 주소를 넣음
  • 동시에 여러 개의 미들웨어를 장착할 수 있음
  • 다음 미들웨어로 넘어가려면 next 함수를 호출해야 함
  • next를 호출하지 않는 미들웨어는 res.send res.sendFile 등의 메서드로 응답을 보내야 함
  • express.static 미들웨어는 정적 파일을 제공할 때 next 대신 res.sendFile 메서드로 응답을 보냄

next 함수에 인수를 넣을 수도 있다. route라는 문자열을 넣으면 다음 라우터의 미들웨어로 바로 이동하고, 그 외의 인수를 넣는다면 바로 에러 처리 미들웨어로 이동한다. 이 인수는 에러 처리 미들웨어의 err 매개변수가 된다.


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

세션을 사용한다면 req.session 객체에 데이터를 넣어도 되지만 세션이 유지되는 동안 데이터도 계속 유지된다는 단점이 있다. 요청이 끝날 때까지만 데이터를 유지하고 싶다면 req 객체에 데이터를 넣어두면 된다. req.data를 통해 미들웨어 간에 데이터를 공유할 수 있다. 새로운 요청이 오면 req.data는 초기화된다.

app.use((req, res, next) => {
  req.data = '데이터 넣기';
  next();
}, (req, res, next) => {
  console.log(req.data); // 데이터 받기
  next();
});

  • 미들웨어를 사용할 때 유용한 패턴

미들웨어 안에 미들웨어를 넣는 방식이다. 

app.use(morgan('dev'));
// 또는
app.use((req, res, next) => {
  morgan('dev')(req, res, next);
});

위 코드는 같은 기능을 하는데 이 패턴이 유용한 이유는 기존 미들웨어의 기능을 확장할 수 있기 때문이다. 아래 코드와 같이 조건문에 따라 분기 처리를 할 수도 있다.

app.use((req, res, next) => {
  if (process.env.NODE_ENV === 'production') {
    morgan('combined')(req, res, next);
  } else {
    morgan('dev')(req, res, next);
  }
});

 

6.2.7 multer

  • 이미지, 동영상 등의 파일들을 멀티파트 형식으로 업로드할 때 사용하는 미들웨어
// multipart.html
<form action="/upload" method="post" enctype="multipart/form-data">
  <input type="file" name="image" />
  <input type="text" name="title" />
  <button type="submit">업로드</button>
</form>

위와 같은 html이 있으면 멀티파트 형식으로 데이터를 업로드할 수 있다.

이런 폼으로 업로드하는 파일은 body-parser로 처리할 수 없고 직접 파싱(해석)하기도 어렵다.

$ npm i 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 },
});

multer 함수의 인수로 설정을 넣는다.

  • storage 속성

destination과 filename으로 어디에 어떤 이름으로 저장할지를 갖고 있다.

req는 요청에 대한 정보, file에는 업로드한 파일에 대한 정보가 있다.

done의 첫 번째 인수에는 에러가 있다면 에러를 넣고, 두 번째 인수에는 실제 경로나 파일 이름을 넣는다. req나 file의 데이터를 가공해서 done으로 넘기는 형식이다.

위 코드는 uploads라는 폴더에 [파일명+현재시간.확장자] 파일명으로 업로드하고 있다.

  • limits 속성

업로드에 대한 제한 사항을 설정할 수 있다. 파일 사이즈를 5MB로 제한하였다.

 

위 설정을 실제로 활용하기 위해서는 서버에 uploads 폴더가 꼭 존재해야 한다.

const fs = require('fs');

try {
  fs.readdirSync('uploads');
} catch (error) {
  console.error('uploads 폴더가 없어 uploads 폴더를 생성합니다.');
  fs.mkdirSync('uploads');
}

설정이 끝나면 upload 변수가 생기고, 다양한 종류의 미들웨어가 들어있다.

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

파일을 하나만 업로드하는 경우에 사용한다. (현재 multipart.html과 같은 경우)

업로드 결과는 req.file에 들어있다.

 

 

여러 파일을 업로드하는 경우, input 태그에 multiple을 쓴다.

// multipart.html
<form id="form" action="/upload" method="post" enctype="multipart/form-data">
  <input type="file" name="many" multiple />
  <input type="text" name="title" />
  <button type="submit">업로드</button>
</form>
app.post('/upload', upload.array('many'), (req, res) => {
  console.log(req.files, req.body);
  res.send('ok');
});
  • array 미들웨어

파일을 여러 개 업로드하는 경우에 사용한다.

업로드 결과는 req.files 배열에 들어있다.

 

 

파일을 여러 개 업로드하지만 input 태그나 폼 데이터의 키가 다른 경우에는 fields 미들웨어를 사용한다.

// multipart.html
<form id="form" action="/upload" method="post" enctype="multipart/form-data">
  <input type="file" name="image1" />
  <input type="file" name="image2" />
  <input type="text" name="title" />
  <button type="submit">업로드</button>
</form>
app.post('/upload',
  upload.fields([{ name: 'image1' }, { name: 'image2' }]),
  (req, res) => {
    console.log(req.files, req.body);
    res.send('ok');
  },
);
  • fileds 미들웨어

인수로 input 태그의 name을 각각 적는다.

업로드 결과는 req.files.image1, req.files.image2에 각각 들어 있다.

 

 

특수한 경우지만, 파일을 업로드하지 않고도 멀티파트 형식으로 업로드하는 경우가 있다.

// multipart.html
<form id="form" action="/upload" method="post" enctype="multipart/form-data">
  <input type="text" name="title" />
  <button type="submit">업로드</button>
</form>
app.post('/upload', upload.none(), (req, res) => {
  console.log(req.body);
  res.send('ok');
});
  • none 미들웨어

파일을 업로드하지 않고 멀티파트 형식으로 업로드하는 경우에 사용한다.

파일을 업로드하지 않았으므로 req.body만 존재한다.

 

 

실제로 multer 예제를 실습하려면 아래와 같이 app.js와 multipart.html을 수정한다.

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

const multer = require('multer');
const fs = require('fs');

try {
  fs.readdirSync('uploads');
} catch (error) {
  console.error('uploads 폴더가 없어 uploads 폴더를 생성합니다.');
  fs.mkdirSync('uploads');
}
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 },
});
app.get('/upload', (req, res) => {
  res.sendFile(path.join(__dirname, 'multipart.html'));
});
app.post('/upload',
  upload.fields([{ name: 'image1' }, { name: 'image2' }]),
  (req, res) => {
    console.log(req.files, req.body);
    res.send('ok');
  },
);

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'), '번 포트에서 대기 중');
});
// multipart.html
<form id="form" action="/upload" method="post" enctype="multipart/form-data">
    <input type="file" name="image1" />
    <input type="file" name="image2" />
    <input type="text" name="title" />
    <button type="submit">업로드</button>
  </form>

6.3 Router 객체로 라우팅 분리하기

app.js에서 app.get 같은 메서드가 라우터이다.

라우터를 많이 연결하면 app.js가 매우 길어지므로 익스프레스에서는 라우터를 분리하는 방법을 제공한다.

 

routes폴더를 만들고 그 안에 index.js와 user.js를 작성한다.

// routes/index.js
const express = require('express');

const router = express.Router();

// GET / 라우터
router.get('/', (req, res) => {
  res.send('Hello, Express');
});

module.exports = router;
// routes/user.js
const express = require('express');

const router = express.Router();

// GET /user 라우터
router.get('/', (req, res) => {
  res.send('Hello, User');
});

module.exports = router;
// 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 indexRouter = require('./routes');
const userRouter = require('./routes/user');
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('/', indexRouter);
app.use('/user', userRouter);

app.use((req, res, next) => {
  res.status(404).send('Not Found');
});

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'), '번 포트에서 대기 중');
});
  • index.js와 user.js를 app.use로 app.js에 연결
  • 404 상태 코드에 응답하는 에러 처리 미들웨어를 추가
  • require('./routes/index.js')는 require('./routes')로 생략 가능

localhost:3000로 접속한 페이지
localhost:3000/user로 접속한 페이지


router.get('/', function(req, res, next) {
  next('route');
}, function(req, res, next) {
  console.log('실행되지 않습니다');
  next();
}, function(req, res, next) {
  console.log('실행되지 않습니다');
  next();
});
router.get('/', function(req, res) {
  console.log('실행됩니다');
  res.send('Hello, Express');
});
  • next('router')

라우터에 연결된 나머지 미들웨어들을 무시하고 다음 라우터로 바로 건너뛸 수 있다.


router.get('/user/:id', function(req, res) {
  console.log(req.params, req.query);
});
  • 라우터 매개변수

req.params.id를 조회하여 :id에 해당하는 주소를 넣을 수 있다. 

:type이면 req.params.type으로 조회할 수 있다.

단, 일반 라우터보다 뒤에 위치해야 한다.

router.get('/user/:id', function(req, res) {
  console.log('얘만 실행됩니다.');
});
router.get('/user/like', function(req, res) {
  console.log('전혀 실행되지 않습니다.');
});

 


6.4 req, res 객체 살펴보기

익스프레스의 req, res 객체는 http 모듈의 req, res 객체를 확장한 것이므로 res.writeHead, res.write, res.end, res.send, res.sendFile 같은 메서드를 그대로 사용할 수 있다.

  • req.app: req 객체를 통해 app 객체에 접근할 수 있다. req.app.get('port')와 같은 식으로 사용할 수 있다.
  • req.body: body-parser 미들웨어가 만드는 요청의 본문을 해석한 객체
  • req.cookies: cookie-parser 미들웨어가 만드는 요청의 쿠키를 해석한 객체
  • req.ip: 요청의 ip 주소가 담겨 있다.
  • req.params: 라우트 매개변수에 대한 정보가 담긴 객체
  • req.query: 쿼리 스트링에 대한 정보가 담긴 객체
  • 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일 수도 있으며, 버퍼일 수도 있고 객체나 배열일 수도 있다.
  • res.sendFile(경로): 경로에 위치한 파일을 응답한다.
  • res.set(헤더, 값): 응답의 헤더를 설정한다.
  • res.status(코드): 응답 시의 HTTP 상태 코드를 지정한다.

아래와 같이 메서드 체이닝을 활용하여 코드 양을 줄일 수 있다.

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

 


6.5 템플릿 엔진 사용하기

템플릿 엔진은 자바스크립트를 사용해서 HTML을 렌더링 할 수 있게 한다.

대표적인 템플릿 엔진인 퍼그(Pug)와 넌적스(Nunjucks)를 살펴보자.

 

6.5.1 퍼그(제이드)

$ npm i pug
// app.js
...
app.set('port', process.env.PORT || 3000);
app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'pug');

app.use(morgan('dev'));
...
  • views는 템플릿 파일들이 위치한 폴더를 지정. res.render 메서드가 이 폴더 기준으로 템플릿 엔진을 찾아서 렌더링. (res.render('admin/main')라면 views/admin/main.pug를 렌더링함)
  • view engine은 어떠한 종류의 템플릿 엔진을 사용할지를 나타냄. 현재 pug로 설정되어 있다.

 

6.5.1.1 HTML 표현

퍼그 HTML
doctype html
html
  head
    title= title
    link(rel='stylesheet', href='/stylesheets/style.css')
<!DOCTYPE html>
<html>
  <head>
    <title>익스프레스</title>
    <link rel="stylesheet" href="/style.css" />
  </head>
</html>
#login-button
.post-image
span#highlight
p.hidden.full
<div id="login-button"></div>
<div class="post-image"></div>
<span id="highlight"></span>
<p class="hidden full"></p>
p Welcome to Express
button(type='submit') 전송
<p>Welcome to Express</p>
<button type="submit">전송</button>
p
  | 안녕하세요.
  | 여러 줄을 입력합니다.
  br
  | 태그도 중간에 넣을 수 있습니다.
<p>
 안녕하세요. 여러 줄을 입력합니다.
 <br />
 태그도 중간에 넣을 수 있습니다.
</p>
style.
  h1 {
    font-size: 30px;
  }
script.
  const message = 'Pug';
  alert(message);
<style>
  h1 {
    font-size: 30px;
  }
</style>
<script>
  const message = 'Pug';
  alert(message);
</script>

 

6.5.1.2 변수

퍼그는 자바스크립트 변수를 템플릿에 렌더링 할 수 있다. res.render 호출 시 보내는 변수를 퍼그가 처리한다.

router.get('/', function(req, res, next) {
  res.render('index', { title: 'Express' });
});
  • res.render(템플릿, 변수 객체) 

이렇게 변수 객체를 넣는 대신, res.locals 객체를 이용해서 변수를 넣을 수도 있다.

이 방식은 다른 미들웨어에서도 res.locals 객체에 접근할 수 있다는 장점이 있다.

router.get('/', function(req, res, next) {
  res.locals.title = 'Express';
  res.render('index');
});

 

퍼그 HTML
h1= title
p Welcome to #{title}
button(class=title, type='submit') 전송
input(placeholder=title + ' 연습')
<h1>Express</h1>
<p>Welcome to Express</p>
<button class="Express" type="submit">전송</button>
<input placeholder="Express 연습" />
- const node = 'Node.js'
- const js = 'Javascript'
p # {node}와 # {js}
<p>Node.js와 Javascript</p>
p= '<strong>이스케이프</strong>'
p!= '<strong>이스케이프하지 않음</strong>'
<p>&lt;strong&gt;이스케이프&lt;/strong&gt;</p>
<p><strong>이스케이프하지 않음</strong></p>

 

6.5.1.3 반복문

퍼그 HTML
ul
  each fruit in ['사과', '배', '오렌지', '바나나', '복숭아']
    li= fruit
<ul>
  <li>사과</li>
  <li>배</li>
  <li>오렌지</li>
  <li>바나나</li>
  <li>복숭아</li>
</ul>
ul
  each fruit, index in ['사과', '배', '오렌지', '바나나', '복숭아']
    li= (index + 1) + '번째 ' + fruit
<ul>
  <li>1번째 사과</li>
  <li>2번째 배</li>
  <li>3번째 오렌지</li>
  <li>4번째 바나나</li>
  <li>5번째 복숭아</li>
</ul>
  • each 대신 for 사용 가능

 

6.5.1.4 조건문

퍼그 HTML
if isLoggedIn
  div 로그인 되었습니다.
else
  div 로그인이 필요합니다.
<!-- isLoggedIn이 true일 때 -->
<div>로그인 되었습니다.</div>
<!-- isLoggedIn이 false일 때 -->
<div>로그인이 필요합니다.</div>
case fruit
  when 'apple'
    p 사과입니다.
  when 'banana'
    p 바나나입니다.
  when 'orange'
    p 오렌지입니다.
  default
    p 사과도 바나나도 오렌지도 아닙니다.
<!-- fruit이 apple일 때 -->
<p>사과입니다.</p>
<!-- fruit이 banana일 때 -->
<p>바나나입니다.</p>
<!-- fruit이 orange일 때 -->
<p>오렌지입니다.</p>
<!-- 기본값 -->
<p>사과도 바나나도 오렌지도 아닙니다.</p>

 

6.5.1.5 include

다른 퍼그나 HTML 파일을 넣을 수 있다.

퍼그 HTML
// header.pug
header
  a(href='/') Home
  a(href='/about') About

// footer.pug
footer
  div 푸터입니다

// main.pug
include header
main
  h1 메인 파일
  p 다른 파일을 include할 수 있습니다.
include footer
<header>
  <a href="/">Home</a>
  <a href="/about">About</a>
</header>
<main>
  <h1>메인 파일</h1>
  <p>다른 파일을 include할 수 있습니다.</p>
</main>
<footer>
  <div>푸터입니다.</div>
</footer>

 

6.5.1.6 extends와 block

레이아웃을 정할 수 있다.

퍼그 HTML
// layout.pug
doctype html
html
  head
    title= title
    link(rel='stylesheet', href='/style.css')
    block style
  body
    header 헤더입니다.
    block content
    footer 푸터입니다.
    block script

// body.pug
extends layout

block content
  main
    p 내용입니다.

  block script
    script(src="/main.js")
<!DOCTYPE html> 
<html> 
  <head> 
    <title>Express</title> 
    <link rel="stylesheet" href="/style.css" /> 
  </head> 
  <body> 
    <header>헤더입니다.</header> 
    <main> 
      <p>내용입니다.</p> 
    </main> 
    <footer>푸터입니다.</footer> 
    <script src="/main.js"></script> 
  </body>
</html>

 


6.5.2 넌적스

$ npm i nunjucks
// app.js
...
const path = require('path');
const nunjucks = require('nunjucks');

dotenv.config();
const indexRouter = require('./routes');
const userRouter = require('./routes/user');

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

nunjucks.configure('views', {
  express: app,
  watch: true,
});

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

configure의 첫 번째 인수로 views 폴더의 경로를 넣고, 두 번째 인수로는 옵션을 넣는다. exptress 속성에 app 객체를 연결하고 watch 옵션이 true이므로 HTML 파일이 변경될 때 템플릿 엔진을 다시 렌더링 한다.

 

퍼그와 달리 html 확장자를 그대로 사용해도 된다. 혹은 njk를 쓸 수 있다.

 

 

6.5.2.1 변수

// routes/index.js
router.get('/', function(req, res, next) {
  res.render('index', { title: 'Express' });
});
넌적스 HTML
<h1>{{title}}</h1>
<p>Welcome to {{title}}</p>
<button class="{{title}}" type="submit">전송</button>
<input placeholder="{{title}} 연습" />
<h1>Express</h1>
<p>Welcome to Express</p>
<button class="Express" type="submit">전송</button>
<input placeholder="Express 연습" />
{% set node = 'Node.js' %}
{% set js = 'Javascript' %}
<p>{{node}}와 {{js}}</p>
<p>Node.js와 Javascript</p>
<p>{{'<strong>이스케이프</strong>'}}</p>
<p>{{'<strong>이스케이프하지 않음</strong>' | safe }}</p>
<p>&lt;strong&gt;이스케이프&lt;/strong&gt;</p>
<p><strong>이스케이프하지 않음</strong></p>

 

6.5.2.2 반복문

넌적스 HTML
<ul>
  {% set fruits = ['사과', '배', '오렌지', ' 바나나', '복숭아'] %}
  {% for item in fruits %}
  <li>{{item}}</li>
  {% endfor %}
</ul>
<ul>
  <li>사과</li>
  <li>배</li>
  <li>오렌지</li>
  <li>바나나</li>
  <li>복숭아</li>
</ul>
<ul>
  {% set fruits = ['사과', '배', '오렌지', ' 바나나', '복숭아'] %}
  {% for item in fruits %}
  <li>{{loop.index}}번째 {{item}}</li>
  {% endfor %}
</ul>
<ul>
  <li>1번째 사과</li>
  <li>2번째 배</li>
  <li>3번째 오렌지</li>
  <li>4번째 바나나</li>
  <li>5번째 복숭아</li>
</ul>

 

6.5.2.3 조건문

넌적스 HTML
{% if isLoggedIn %}
<div>로그인 되었습니다.</div>
{% else %}
<div>로그인이 필요합니다.</div>
{% endif %}
<!-- isLoggedIn이 true일 때 -->
<div>로그인 되었습니다.</div>
<!-- isLoggedIn이 false일 때 -->
<div>로그인이 필요합니다.</div>
{% if fruit === 'apple' %}
<p>사과입니다.</p>
{% elif fruit === 'banana' %}
<p>바나나입니다.</p>
{% elif fruit === 'orange' %}
<p>오렌지입니다.</p>
{% else %}
<p>사과도 바나나도 오렌지도 아닙니다.</p>
{% endif %}
<!-- fruit이 apple일 때 -->
<p>사과입니다.</p>
<!-- fruit이 banana일 때 -->
<p>바나나입니다.</p>
<!-- fruit이 orange일 때 -->
<p>오렌지입니다.</p>
<!-- 기본값 -->
<p>사과도 바나나도 오렌지도 아닙니다.</p>
<div>{{'참' if isLoggedIn}}</div>
<div>{{'참' if isLoggedIn else '거짓'}}</div>
<!-- isLoggedIn이 true일 때 -->
<div>참</div>
<!-- isLoggedIn이 false일 때 -->
<div>거짓</div>

 

6.5.2.4 include

넌적스 HTML
// header.html
<header>
  <a href="/">Home</a>
  <a href="/about">About</a>
</header>

// footer.html
<footer>
  <div>푸터입니다.</div>
</footer>

// main.html
{% include "header.html" %}
<main>
  <h1>메인 파일</h1>
  <p>다른 파일을 include할 수 있습니다.</p>
</main>
{% include "footer.html" %}
 
<header>
  <a href="/">Home</a>
  <a href="/about">About</a>
</header>
<main>
  <h1>메인 파일</h1>
  <p>다른 파일을 include할 수 있습니다.</p>
</main>
<footer>
  <div>푸터입니다.</div>
</footer>

 

6.5.2.5 extends와 block

넌적스 HTML
// layout.html
<!DOCTYPE html>
<html>
  <head>
    <title>{{title}}</title>
    <link rel="stylesheet" href="/style.css" />
    {% block style %}
    {% endblock %}
  </head>
  <body>
    <header>헤더입니다.</header>
    {% block content %}
    {% endblock %}
    <footer>푸터입니다.</footer>
    {% block script %}
    {% endblock %}
  </body>
</html>

// body.html
{% extends 'layout.html' %}

{% block content %}
<main>
  <p>내용입니다.</p>
</main>
{% endblock %}

{% block script %}
<script src="/main.js"></script>
{% endblock %}
<!DOCTYPE html>
<html>
  <head>
    <title>Express</title>
    <link rel="stylesheet" href="/style.css" />
  </head>
  <body>
    <header>헤더입니다.</header>
    <main>
      <p>내용입니다.</p>
    </main>
    <footer>푸터입니다.</footer>
    <script src="/main.js"></script>
  </body>
</html>

 나중에 익스프레스에서 res.render('body')를 사용해 같은 이름의 block 부분이 서로 합쳐지며 하나의 HTML로 렌더링 할 수 있다.

 


6.5.3 에러 처리 미들웨어

// app.js
... 
app.use((req, res, next) => { 
  const error = new Error(`${req.method} ${req.url} 라우터가 없습니다.`); 
  error.status = 404; 
  next(error); 
}); 

app.use((err, req, res, next) => { 
  res.locals.message = err.message; 
  res.locals.error = process.env.NODE_ENV !== 'production' ? err : {}; 
  res.status(err.status || 500); 
  res.render('error'); 
}); 
...

 

에러 처리 미들웨어는 error라는 템플릿 파일을 렌더링 한다. 넌적스이므로 error는 error.html을 의미한다. 렌더링 시 res.locals.message res.locals.error에 넣어준 값을 함께 렌더링한다. 실제 404 에러가 발생했을 때, error.html의 페이지를 보이게 된다.

 

에러가 발생했을 때 에러 스택 트레이스가 보이는데 노출되면 보안에 취약할 수 있기 때문에 배포 환경에서는 숨겨야 한다.

 


Quiz

  1. 웹 서버를 만들 때 코드가 복잡하고 확장성이 떨어지는 문제를 해소해주는 npm에서 제공하는 대표적인 웹 서버 프레임워크는 (                )이다.
  2.  코드가 수정될 때 자동으로 서버를 재시작하는 데몬은 (                )이고 배포용으로는 적합하지 않다.
  3. 요청과 응답을 조작하여 기능을 추가하기도 하고 나쁜 요청을 걸러내기도 하는 역할을 하는 요청과 응답의 중간에 위치한 (               )는 익스프레스의 핵심이다.
  4. 미들웨어의 매개변수는 (       ), (       ), (       )이 있다.
  5. 요청의 본문에 있는 데이터를 해석해서 req.body객체로 만들어주는 미들웨어는 (                )이다.
  6. 다음 미들웨어로 넘어가려면 (               ) 함수를 호출해야 한다.
  7. HTML 문법을 그대로 사용하고 파이썬의 템플릿 엔진인 Twig와 문법이 유사한 템플릿 엔진은 (              )이다.

Answer

더보기
  1. 익스프레스
  2. nodemon
  3. 미들웨어
  4. req, res, next
  5. body-parser
  6. next
  7. 넌적스(nunjucks)
728x90

'21-22 > 21-22 Node.js' 카테고리의 다른 글

[Node.js] 8장 몽고디비  (0) 2021.12.01
[Node.js] 7장 MySQL  (0) 2021.11.29
[Node.js] 5장 패키지 매니저  (0) 2021.11.08
[Node.js] 4장 http 모듈로 서버 만들기  (0) 2021.11.08
[Node.js] 3장 기능 알아보기(2)  (0) 2021.10.30

관련글 더보기