본문 바로가기

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

[2024 여름방학 Node.js 스터디] 백채린 #4주차 5~6장

반응형

5장 패키지 매니저

 

 

5.1 npm 알아보기

 

- npm(Node Package Manager) 이란?

노드 패키지 매니저, 다른 사람들이 미리 작성해 놓은 코드들이 공개되어 있는 서버

npm에 업로드된 노드 모듈을 패키지라고 부른다.

 

* yarn: npm의 대체자, 페이스북이 내놓은 패캐지 매니저

* pnpm: npm의 성능을 개선한 패키지 매니저

 

 

5.2 package.json으로 패키지 관리하기

 

같은 패키지라도 버전별로 기능이 다를 수 있기 때문에 프로젝트를 설치할 때 패키지도 동일한 버전을 설치해야 하는데

이때 설치한 패키지의 버전을 관리하는 파일이 package.json

→ 노드 프로젝트를 시작하기 전, 폴더 내부에 무조건 package.json부터 만들고 시작해야 한다 !!

 

npm으로 package.json을 만들어보자! 

 

콘솔로 프로젝트를 시작할 폴더로 이동 → npm init 입력

 

 

 package name: 패키지의 이름

 version: 패키지의 버전

 entry point: 자바스크립트 실행 파일 진입점

보통 마지막으로 module.exports를 하는 파일을 지정한다. package.json의 main 속성에 저장된다.

 test command: 코드를 테스트할 때 입력할 명령어

package.json scripts속성 안의 test 속성에 저장된다.

 git repository: 코드를 저장해둔 깃(Git) 저장소 주소

package.json의 repository 속성에 저장됩니다.

 keywords: 키워드는 npm 공식 홈페이지(https://npmjs.com)에서 패키지를 쉽게 찾을 수 있게 한다. package.json의 keywords 속성에 저장된다.

 license: 해당 패키지의 라이선스를 넣으면 된다.

 

 

 

 

* 라이선스별로 특징이 다르므로 오픈 소스를 사용하기 전에는 반드시 라이선스를 확인해야 한다.

 

 

scripts 부분은 npm 명령어를 저장해두는 부분

 

콘솔에서 npm run [스크립트 명령어] 를 입력하면 

해당 스크립트가 실행된다.

 

 

 

 

 

 

 

익스프레스(Express)를 설치해보자!

 

npm install [패키지 이름] 을 package.json이 있는 폴더의 콘솔에 입력

 

 

* vulnerability

npm은 패키지를 설치할 때 패키지에 있을 수 있는 취약점을 자동으로 검사한다.

found 0 vulnerabilities 문장 출력 → 취약점 X

[숫자] [심각도] severity vulnerability 문장 출력 → 취약점 O 

npm audit: 패키지의 알려진 취약점을 검사할 수 있는 명령어

npm audit fix: npm이 스스로 수정할 수 있는 취약점을 수정해주는 명령어

 

dependencies 라는 속성 봐주세요 !

 

* 프로젝트 이름과 설치하는 패키지 이름은 달라야 한다.

 

 

node_modules 폴더: 설치한 패키지들이 들어 있다.

→ Express 하나만 설치했는데 패키지가 왜 여러가지일까?

Express가 의존하는 패키지들!

패키지들끼리 의존 관계가 복잡하게 얽혀 있기 때문에 package.json이 필요!

package.json: 직접 설치한 패키지를 기록하는 파일

package-lock.json 파일: 패키지 간의 의존 관계를 명시한 파일

 

의존 관계

 

 

 

모듈 여러 개를 동시에 설치해보자!

 

npm install [패키지1] [패키지2] [...] 

 

 

개발용 패키지를 설치해보자!

 

배포 시에는 사용되지 않고, 개발 중에만 사용되는 패키지들

npm install --save-dev [패키지] [...] (--save-dev 가 개발용 패키지임을 나타낸다.)

 

nodemon 패키지: 소스 코드가 바뀔 때마다 자동으로 노드를 재실행해주는 패키지

 

devDependencies 속성: 개발용 패키지들만 따로 관리

 

peerDependencies

 

직접적으로 사용(import나 require)하지는 않지만, 설치되어 있다고 생각하고 코드를 작성했다는 의미

 

ex) package.json

 

{
  ...
  "peerDependencies": {
    "jQuery": "^3.0.0"
  }
}
// jQuery를 미리 설치하지 않았거나 3 버전이 아닌 버전을 설치한 경우 에러 발생

 

peerDependencies와 다른 버전이 설치 → ERESOLVE unable to resolve dependency tree 에러 메시지 표시

 

▶ 해결 방법 

1. peerDependencies에 맞게 다시 설치 (A 패키지와 B 패키지의 버전이 다른 경우 해결 불가능)

2. npm i --force로 모든 버전 설치

3. npm i --legacy-peer-deps로 peerDependencies 무시

→ 최선의 방법은 peerDependecies가 서로 충돌하는 패키지를 같이 설치하지 않는 것! 

 

전역(global) 설치

 

패키지를 현재 폴더의 node_modules에 설치하는 것이 아니라 npm이 설치되어 있는 폴더에 설치

→ 이 폴더의 경로는 보통 시스템 환경 변수에 등록되어 있으므로 전역 설치한 패키지는 콘솔의 명령어로 사용 가능

 

 

 

rimraf라는 패키지를 설치하려고 했더니 에러가 발생했어요 😰

 

리눅스나 맥에서는 전역 설치 시 관리자 권한이 필요한 경우가

있으므로 앞에 sudo 를 붙여야 합니다.

 

 

 

 

 

* 명령어 줄여 쓰기

npm install → npm    i  --save-dev -D     --global → -g

 

rimraf 패키지 

: 리눅스나 맥의 rm -rf 명령어를 윈도에서도 사용할 수 있게 해주는 패키지로, 지정한 파일이나 폴더를 지우는 명령어

 

node_modules는 언제든지 npm install 로 설치할 수 있으므로 보관할 필요가 X

다만, npm i를 할 때마다 package.json, package-lock.json이 변하므로 실제 서비스를 배포할 때는 npm ci 명령어를 사용해서 package-lock.json에 적힌 대로 설치

 

* npx

패키지를 전역 설치한 것과 같은 효과 (전역 설치한 패키지는 package.json에 기록되지 않아 다시 설치할 때 어려움이 있기 때문)

 

// 콘솔
$ npm install --save-dev rimraf
$ npx rimraf node_modules

 

* npm에 등록되지 않은 패키지

npm install [저장소 주소] 명령어를 통해 설치

 

* node_modules 내부 수정하기

patch package 패키지: node_modules 내부의 수정 사항을 영구적으로 반영해주는 패키지

ex) package.json 아래와 같이 수정 → patch-package 패키지 설치

 

"scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "postinstall": "patch-package"
  },
$ npm i patch-package
(node_modules 내부의 원하는 패키지 수정하기)
$ npx patch-package [수정한 패키지 이름]

 

 

 

5.3  패키지 버전 이해하기

 

노드의 패키지 버전은 SemVer(Semantic Versioning) 방식의 버전 넘버링을 따르기 때문에 항상 세 자리로 이뤄져 있다.

* SemVer: 버전 번호를 어떻게 정하고 올려야 하는지를 명시하는 규칙

 

 

 

- 메이저

메이저 버전이 0 → 초기 개발 중

1 → 정식 버전

- 마이너

- 패치

 

 

 

 

package.json 에는 SemVer식 세 자리 버전 외에도 버전 앞에 ^ 이나 ~ 또는 >, < 같은 문자가 붙어 있다.

→ 버전에는 포함되지 않지만 설치하거나 업데이트할 때 어떤 버전을 설치해야 하는지 알림

 

^ 마이너 버전까지 설치하거나 업데이트 ex) npm i express@^1.1.1.
(1.1.1 이상부터 2.0.0 미만 버전까지 설치)
~ 패치 버전까지만 설치하거나 업데이트 ex) npm i express@~1.1.1
(1.1.1 이상부터 1.2.0 미만 버전까지 설치)
>, <, > =, < =, = 초과, 미만, 이상, 이하, 동일 ex) npm i express@>1.1.1
(1.1.1 보다 높은 버전 설치)
@latest(@x로도 표현 가능) 안정된 최신 버전의 패키지 설치 ex) npm i. xpress@latest
@next 가장 최근 배포판 사용 가능,
알파나 베타 버전의 패키지를 설치할 수도 있음
ex) 1.1.1-alpha.0
2.0.0-rc.0 (출시 직전의 패키지)

 

 

 

5.4 기타 npm 명령어

 

npm outdated

 

npm outdated 업데이트할 수 있는 패키지가 있는지 확인
npm update 업데이트 가능한 모든 패키지가 Wanted에 적힌 버전으로 업데이트
npm uninstall [패키지 이름] (npm re [패키지 이름]) 해당 패키지를 제거하는 명령어
npm search [검색어]  npm의 패키지 검색
npm info [패키지 이름]  패키지의 세부 정보를 파악하고자 할 때 사용하는 명령어
npm login npm 로그인을 위한 명령어
npm whoami 로그인한 사용자가 누구인지 알림
npm logout npm login으로 로그인한 계정 로그아웃
npm version [버전] package.json의 버전을 올림
ex) npm version 5.3.2, npm version minor
npm deprecate [패키지 이름] [버전] [메시지] 해당 패키지를 설치할 때 경고 메시지를 띄우게 하는 명령어
npm publish 자신이 만든 패키지를 배포할 때 사용
npm unpublish 배포한 패키지를 제거할 때 사용,
24시간 이내에 배포한 패키지만 제거 가능

 

 

 

5.5 패키지 배포하기

 

 

 

6장 익스프레스 웹 서버 만들기

 

 

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

 

1. learn-express 폴더 만들기

2. package.json 생성 (npm init 또는 npm intit -y 명령어 입력)

 

package.json

// 콘솔
$ npm i express
$ npm i -D nodemon

 

scripts 의 start 속성은 꼭 넣어줘야 한다.

nodemon app: app.js를 nodemon으로 실행한다는 뜻

* nodemon 모듈: 서버 코드를 수정하면 서버를 자동으로 재시작하고 nodemon이 실행되는 콘솔에 rs를 입력하면 수동으로 재시작,

                            개발용으로만 사용할 것을 권장

 

3. app.js(서버의 역할)를 다음과 같이 적는다.

 

const express = require('express');

const app = express();
// Express 모듈을 실행해 app 변수에 할당
app.set('port', process.env.PORT || 3000);
// process.env 객체에 PORT 속성이 있다면 그 값을 사용하고, 없다면 기본값으로 3000번 포트를 이용

app.get('/', (req, res) => {
  res.send('Hello, Express');
});
// 매개변수 req는 요청에 관한 정보가 들어 있는 객체이고, res는 응답에 관한 정보가 들어 있는 객체
// GET / 요청 시 응답으로 Hello, Express를 전송

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

 

- app.set('port', 포트)

: 서버가 실행될 포트를 설정,

app.set(키, 값)을 사용해서 데이터를 저장할 수 있으며, 나중에 데이터를 app.get(키)로 가져올 수 있다.

- app.get(주소, 라우터)

: 주소에 대한 GET 요청이 올 때 어떤 동작을 할지 적는 부분

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

 

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

 

4. listen을 하는 부분은 http 웹 서버와 동일 (포트를 연결하고 서버를 실행, 포트는 app.get('port')로 가져옴)

 

$ npm start

> learn-express@0.0.1 start
> nodemon app

[nodemon] 2.0.16
[nodemon] to restart at any time, enter `rs`
[nodemon] watching dir(s): *.*
[nodemon] watching extensions: js,mjs,json
[nodemon] starting `node app.js`
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'), '번 포트에서 대기 중');
});

 

 

 

6.2 자주 사용하는 미들웨어

 

미들웨어

: 익스프레스의 핵심, 요청과 응답의 중간(middle)에 위치하기 때문에 미들웨어(middleware)라고 부른다.

  요청과 응답을 조작해 기능을 추가하기도 하고, 나쁜 요청을 걸러내기도 함

  app.use와 함께 사용되며 app.use(미들웨어) 

 

익스프레스 서버에 미들웨어를 연결해보자!

 

app.js

 

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

// app.use에 매개변수가 req, res, 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'), () => {
...

 

미들웨어는 위에서부터 아래로 순서대로 실행되면서 요청과 응답 사이에 특별한 기능을 추가할 수 있다.

세 번째 매개변수 next: 다음 미들웨어로 넘어가는 함수, next를 실행하지 않으면 다음 미들웨어 실행 X

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

 

미들웨어가 실행되는 경우

 

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

 

에러 처리 미들웨어는 매개변수가 err(에러에 관한 정보), req, res, next로 네 개, 모두 사용하지 않더라도 매개변수는 반드시 네 개

res.status 메서드로 HTTP 상태 코드를 지정할 수 있으며, 기본값은 200(성공) 

 

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



실무에 자주 사용하는 패키지들을 설치해보자!

 

$ npm i morgan cookie-parser express-session dotenv

 

 

dotenv: 미들웨어가 아니지만 process.env를 관리하기 위해 설치

 

1. 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();
});
...

 

2. .env 파일 생성

 

COOKIE_SECRET=cookiesecret

 

 

- dotenv 패키지는 .env 파일을 읽어서 process.env로 만든다. (dotenv 패키지의 이름이 dot(점)+env인 이유)

- process.env.COOKIE_SECRET cookiesecret 값이 할당되며, 키=값 형식으로 추가하면 된다.

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

    → 비밀 키들을 소스 코드에 그대로 적어두면 소스 코드가 유출되었을 때 키도 같이 유출되기 때문에 .env 같은 별도의 파일에  

         비밀 키를 적어두고 dotenv 패키지로 비밀 키를 로딩하는 방식으로 관리

 

 

6.2.1 morgan

 

morgan 연결 후 localhost:3000에 다시 접속해보면 기존 로그 외에 추가적인 로그를 볼 수 있다.

 

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

 

GET / 500 7.409ms - 50 로그는 morgan 미들웨어에서 나오는 것으로 요청과 응답에 대한 정보를 콘솔에 기록한다.

 

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

 

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

 

인수로 dev 외에 combined, common, short, tiny 등을 넣을 수 있다.

dev 모드 기준으로 GET / 500 7.409 ms - 50은 각각 [HTTP 메서드] [주소] [HTTP 상태 코드] [응답 속도] - [응답 바이트]를 의미

→ 요청과 응답을 한눈에 볼 수 있어 편리

 

 

6.2.2 static

 

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

 

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

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

 

함수의 인수로 정적 파일들이 담겨 있는 폴더를 지정, 현재는 public 폴더가 지정

ex) public/stylesheets/style.css는 http://localhost:3000/stylesheets/style.css로 접근 가능

(실제 서버의 폴더 경로에는 public이 들어 있지만, 요청 주소에는 public이 들어 있지 않다는 점에 주목 !)

→ 서버의 폴더 경로와 요청 경로가 다르므로 외부인이 서버의 구조를 쉽게 파악할 수 없으므로 보안에 좋다.

→ 정적 파일들을 알아서 제공해주므로 파일을 직접 읽어서 전송할 필요 X

만약 요청 경로에 해당하는 파일이 없으면 알아서 내부적으로 next를 호출하며 파일을 발견했다면 다음 미들웨어 실행 X

 

 

6.2.3 body-parser

 

요청의 본문에 있는 데이터를 해석해서 req.body 객체로 만들어주는 미들웨어로, 보통 폼 데이터나 AJAX 요청의 데이터를 처리

단, 멀티파트(이미지, 동영상, 파일) 데이터는 처리하지 못하는데 이 경우, 뒤에 나오는 multer 모듈을 사용

 

app.use(express.json());
app.use(express.urlencoded({ extended: false }));
// 이 옵션이 false이면 노드의 querystring 모듈을 사용해 쿼리스트링을 해석하고, 
// true이면 qs 모듈을 사용해 쿼리스트링을 해석

 

 

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

* JSON은 JSON 형식의 데이터 전달 방식이고, URL-encoded는 주소 형식으로 데이터를 보내는 방식

ex) JSON 형식: { name: 'zerocho', book: 'nodejs' }, URL-encoded 형식: name=zerocho&book=nodejs

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

 

 

6.2.4 cookie-parser

 

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

 

app.js

 

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

 

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

ex) name=zerocho 쿠키를 보냈다면 req.cookies는 { name: 'zerocho' }가 된다.

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

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

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

→ 서명이 붙으면 쿠키가 name=zerocho.sign과 같은 모양이 되고 서명된 쿠키는 req.signedCookies 객체에 들어 있다.

- 쿠키를 생성/제거하려면 res.cookie, res.clearCookie 메서드를 사용, res.cookie(키, 값, 옵션) 형식으로 사용

 

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

 

- 쿠키를 지우려면, 키와 값 외에 옵션도 정확히 일치해야 지워진다. 단, expires maxAge 옵션은 일치할 필요 X

- 서명을 위한 비밀 키는 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은 인수로 세션에 대한 설정을 받는다. 

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

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

- express-session은 세션 관리 시 클라이언트에 쿠키를 보낸다.

- 안전하게 쿠키를 전송하려면 쿠키에 서명을 추가해야 하고, 쿠키를 서명하는 데 secret의 값이 필요

- cookie-parser의 secret과 같게 설정하는 것이 좋으며, 세션 쿠키의 이름은 name 옵션으로 설정한다. (기본 이름은 connect.sid)

- cookie 옵션은 세션 쿠키에 대한 설정이다. 

 

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

 

 

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

 

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

 

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

 

- 미들웨어는 req, res, next를 매개변수로 갖는 함수(에러 처리 미들웨어만 예외적으로 err, req, res, next)

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

 

app.use(
  morgan('dev'),
  express.static('/', path.join(__dirname, 'public')),
  express.json(),
  express.urlencoded({ extended: false }),
  cookieParser(process.env.COOKIE_SECRET),
);

 

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

- next를 호출하지 않는 미들웨어는 res.send res.sendFile 등의 메서드로 응답을 보내야 한다. 

- express.static과 같은 미들웨어는 정적 파일을 제공할 때 next 대신 res.sendFile 메서드로 응답을 보내기 때문에 정적 파일을 제공하는 경우 express.json, express.urlencoded, cookieParser 미들웨어는 실행되지 않는다.

- 만약 next도 호출하지 않고 응답도 보내지 않으면 클라이언트는 응답을 받지 못해 하염없이 기다리게 된다.

 

 

next의 동작

 

- 지금까지는 next에 아무런 인수를 넣지 않았지만 next 함수에 인수를 넣을 수도 있다.

- route라는 문자열을 넣으면 다음 라우터의 미들웨어로 바로 이동하고, 그 외의 인수를 넣는다면 바로 에러 처리 미들웨어로 이동

 

 

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

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

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

→ 현재 요청이 처리되는 동안 res.locals 객체를 통해 미들웨어 간에 데이터를 공유 가능, 새로운 요청이 오면 res.locals는 초기화

 

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

 

 

 

 

 

6.2.7 multer

 

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

* 멀티파트 형식: 다음과 같이 enctype multipart/form-data인 폼을 통해 업로드하는 데이터의 형식을 의미

 

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>

 

개발자 도구 NetWork 탭

 

이미지 하나를 선택하고 title 인풋에 제목이라 적어서 업로드하면 다음과 같은 데이터가 전송

 

이러한 폼을 통해 업로드하는 파일은 body-parser로는 처리할 수 없고 직접 파싱(해석)하기도 어려우므로 multer 미들웨어를 따로 사용하면 편리하다.

 

 

 

 

 

- multer 설치

$ 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) 저장할지를 넣었다.

destination filename 함수의 req 매개변수: 요청에 대한 정보

file 객체: 업로드한 파일에 대한 정보

done 매개변수: 함수, 첫 번째 인수에는 에러가 있다면 에러를 넣고, 두 번째 인수에는 실제 경로나 파일 이름을 넣어주면 된다. 

- uploads라는 폴더에 [파일명+현재시간.확장자] 파일명으로 업로드하고 있는데, 현재 시간을 넣는 이유는 업로드하는 파일명이 겹치는 것을 막기 위함이다.

- limits 속성: 업로드에 대한 제한 사항을 설정, 파일 사이즈(fileSize, 바이트 단위)를 5MB로 제한해둠

 

 

 

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;

 

index.js와 user.js를 app.use를 통해 app.js에 연결하고 에러 처리 미들웨어 위에 404 상태 코드를 응답하는 미들웨어를 하나 추가

 

app.js

 

...
const path = require('path');

dotenv.config();
const indexRouter = require('./routes');
const userRouter = require('./routes/user');
...
  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) => {
...

 

- indexRouter를 ./routes require할 수 있는 이유는 index.js는 생략할 수 있기 때문

ex) require('./routes/index.js') = require('./routes')

- index.js user.js app.use로 연결할 때의 차이 때문에 비슷하지만, 다른 주소의 라우터 역할을 하고 있다. 

- indexRouter app.use('/')에 연결, userRouter app.use('/user')에 연결

- indexRouter use '/' +  get '/' GET / 라우터, userRouter는 use '/user' +  get '/' GET /user 라우터 

 

 next('route'): 다음 라우터로 넘어가는 기능, 라우터에 연결된 나머지 미들웨어들을 건너뛰고 싶을 때 사용한다.

 

 

6.4 req, res 객체 살펴보기

 

익스프레스의 req, res 객체는 http 모듈의 req, res 객체를 확장한 것 !

기존 http 모듈의 메서드도 사용할 수 있고, 익스프레스가 추가한 메서드나 속성을 사용할 수도 있다.

 

req 객체를 살펴보자!

 

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 객체를 살펴보자!

 

res.app res 객체를 통해 app 객체에 접근 가능
res.cookie(키, 값, 옵션) 쿠키를 설정하는 메서드
res.clearCookie(키, 값, 옵션) 쿠키를 제거하는 메서드
res.end() 데이터 없이 응답을 보낸다.
res.json(JSON) JSON 형식의 응답을 보낸다.
res.locals 하나의 요청 안에서 미들웨어 간에 데이터를 전달하고 싶을 때 사용하는 객체
res.redirect(주소) 리다이렉트할 주소와 함께 응답을 보낸다.
res.render(뷰, 데이터) 템플릿 엔진을 렌더링해서 응답할 때 사용하는 메서드
res.send(데이터) 데이터와 함께 응답을 보낸다.
res.set(헤더, 값) 응답의 헤더를 설정
res.status(코드) 응답 시의 HTTP 상태 코드를 지정

 

메서드 체이닝(method chaining)

 

req res 객체의 메서드는 메서드 체이닝(method chaining)을 지원하는 경우가 많기 때문에 이를 활용하면 코드양을 줄일 수 있다.

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

 

 

 

6.5 템플릿 엔진 사용하기

 

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

 

 

6.5.1 퍼그(제이드)

 

문법이 간단하지만 HTML과는 문법이 많이 달라서 호불호가 갈린다.

퍼그

 

- 퍼그 설치

 

$ 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 메서드가 이 폴더 기준으로 템플릿 엔진을 찾아서 렌더링한다.

ex) res.render('index') → views/index.pug 렌더링, res.render('admin/main') → views/admin/main.pug 렌더링

- view engine: 어떠한 종류의 템플릿 엔진을 사용할지를 나타낸다.

 

 

퍼그의 문법을 알아보자!

 

6.5.1.1 HTML 표현

 

- 기존 HTML과 다르게 화살괄호(< >)와 닫는 태그가 없다.

- 탭 또는 스페이스로만 태그의 부모 자식 관계를 규명하며, 모든 파일에 동일한 종류의 들여쓰기를 적용

- 자식 태그는 부모 태그보다 들여쓰기되어 있어야 하며, 들여쓰기에 오류가 있으면 제대로 렌더링되지 않으니 주의!

- 화살괄호가 없으므로 태그의 속성은 태그명 뒤에 소괄호로 묶어 적는다.

 

 

- 속성 중 아이디와 클래스가 있는 경우 다음과 같이 표현하며, div 문자는 생략 가능

 

 

- HTML 텍스트는 다음과 같이 태그 또는 속성 뒤에 한 칸을 띄고 입력

 

 

- 에디터에서 텍스트를 여러 줄 입력하고 싶다면 파이프(|)를 넣는다. HTML 코드에서는 한 줄로 나온다.

 

 

- style이나 script 태그로 CSS 또는 자바스크립트 코드를 작성하고 싶다면 태그 뒤에 점(.)을 붙인다.

 

 

 

6.5.1.2 변수

 

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

 

routes/index.js

 

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

 

- res.render(템플릿, 변수 객체): 익스프레스가 res 객체에 추가한 템플릿 렌더링을 위한 메서드

- index.pug를 HTML로 렌더링하면서 { title: 'Express' }라는 객체를 변수로 집어넣는다.

- layout.pug와 index.pug의 title 부분이 모두 Express로 치환 → HTML에도 변수 사용 가능

 

res.render 메서드에 두 번째 인수로 변수 객체 대신, res.locals 객체를 사용해서 변수를 넣을 수도 있다.

 

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

 

템플릿 엔진이 res.locals 객체를 읽어서 변수를 집어넣는다.

- 장점: 현재 라우터뿐만 아니라 다른 미들웨어에서도 res.locals 객체에 접근 가능

 

퍼그에서 변수를 사용하는 방법을 살펴보자!

 

 

- 변수를 텍스트로 사용하고 싶다면 태그 뒤에 =을 붙인 후 변수를 입력, 속성에도 =을 붙인 후 변수 사용 가능

- 텍스트 중간에 변수를 넣으려면 #{변수}를 사용

- #{}의 내부와 = 기호 뒷부분은 자바스크립트로 해석하므로 자바스크립트 구문을 써도 된다.

 

- 빼기(-)를 먼저 입력하면 뒤에 자바스크립트 구문을 작성할 수 있다. 여기에 변수를 선언하면 다음 줄부터 해당 변수 사용 가능

 

 

- 퍼그는 기본적으로 변수의 특수 문자를 HTML 엔티티(entity)로 이스케이프(escape)(문법과 관련 없는 문자로 바꾸는 행위)한다.

  (이스케이프를 원하지 않는다면 = 대신 !=을 사용)

 

 

 

6.5.1.3 반복문

 

HTML과 다르게 반복문도 사용할 수 있으며, 반복 가능한 변수인 경우에만 해당된다.

 

- each로 반복문을 돌릴 수 있으며, each 대신 for를 써도 된다.

 

 

- 반복문 사용 시 인덱스도 가져올 수 있다.

 

 

 

6.5.1.4 조건문

 

조건문으로 편리하게 분기 처리할 수 있으며 if, else if, else 사용 가능

 

- isLoggedIn 변수로 로그인 여부에 따라 다르게 HTML을 렌더링하는 예시

 

 

- case문도 가능

 

 

 

6.5.1.5 include

 

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

- 웹을 제작할 때 공통되는 부분을 따로 관리할 수 있어 페이지마다 동일한 HTML을 넣어야 하는 번거로움을 없앤다. 

- include 파일 경로와 같은 형태로 사용한다.

 

 

 

6.5.1.6 extends와 block

 

레이아웃을 정할 수 있으며, 공통되는 레이아웃 부분을 따로 관리할 수 있다. include와도 함께 사용하기도 한다.

 

 

- 레이아웃이 될 파일에는 공통된 마크업을 넣되, 페이지마다 달라지는 부분을 block으로 비워둔다.

- block [블록명]과 같은 형태로 block을 선언

- block이 되는 파일에서는 extends 키워드로 레이아웃 파일을 지정하고 block 부분을 넣는다.

( block 선언보다 한 단계 더 들여쓰기되어 있어야 함)

- 익스프레스에서 res.render('body')를 사용해 하나의 HTML로 합쳐 렌더링 가능

- 퍼그 확장자는 생략 가능하며 block 부분이 서로 합쳐진다.

 

 

 

6.5.2 넌적스

 

퍼그의 HTML 문법 변화에 적응하기 힘든 분에게 유용한 템플릿 엔진이며, 파이어폭스를 개발한 모질라에서 만들었다.

HTML 문법을 그대로 사용하되 추가로 자바스크립트 문법을 사용할 수 있으며, 파이썬의 템플릿 엔진인 Twig와 문법이 상당히 유사

 

- 넌적스 설치

 

$ npm i nunjucks

 

view engine을 퍼그 대신 넌적스로 교체

 

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 폴더의 경로를 넣고, 두 번째 인수로 옵션을 넣는다.

- 이때 express 속성에 app 객체를 연결, watch 옵션이 true이면 HTML 파일이 변경될 때 템플릿 엔진을 다시 렌더링한다.

- 파일은 pug와 같은 특수한 확장자 대신 html을 그대로 사용해도 된다.

(넌적스임을 구분하려면 확장자로 njk 사용, 단, 이때는 view engine도 njk로 바꿔야 한다.)

 

 

6.5.2.1 변수

 

res.render 호출 시 보내는 변수를 넌적스가 처리한다.

routes/index.js 코드

 

router.get('/', (req, res, next) => {
  res.render('index', { title: 'Express' });
});
넌적스

 

<h1>{{title}}</h1>
<p>Welcome to {{title}}</p>
<button class="{{title}}" type="submit">전송</button>
<input placeholder="{{title}} 연습" />

 

- 넌적스에서 변수는 {{ }}로 감싼다.

HTML

 

<h1>Express</h1>
<p>Welcome to Express</p>
<button class="Express" type="submit">전송</button>
<input placeholder="Express 연습" />

 

- 내부에 변수 사용 가능하며, 변수를 선언할 때는 {% set 변수 = '값' %}를 사용

 

 

- HTML을 이스케이프하고 싶지 않다면 {{ 변수 | safe }}를 사용

 

 

 

6.5.2.2 반복문

 

- 넌적스에서는 특수한 문(반복문 등)을 {% %} 안에 쓴다. for in문과 endfor 사이에 위치

 

 

- 반복문에서 인덱스를 사용하고 싶다면 loop.index라는 특수한 변수를 사용

 

 

 

6.5.2.3 조건문

 

- 조건문은 {% if 변수 %} {% elif %} {% else %} {% endif %}로 이뤄져 있다.

 

 

- case문은 없지만 elif(else if 역할)를 통해 분기 처리 가능

 

 

- {{ }} 안에서는 다음과 같이 사용

 

 

 

6.5.2.4 include

 

- 다른 HTML 파일을 넣을 수 있다.

- 웹 제작 시 공통되는 부분을 따로 관리할 수 있어 페이지마다 동일한 HTML을 넣어야 하는 번거로움을 없앤다. 

- include 파일 경로와 같은 형태로 사용한다.

 

 

 

6.5.2.5 extends와 block

 

레이아웃을 정할 수 있으며, 공통되는 레이아웃 부분을 따로 관리할 수 있어 좋다. include와도 함께 사용하곤 한다.

 

 

- 레이아웃이 될 파일에는 공통된 마크업을 넣되, 페이지마다 달라지는 부분을 block으로 비워둔다. 

- block을 선언하는 방법은 {% block [블록명] %}, {% endblock %}로 블록을 종료한다.

- block이 되는 파일에서는 {% extends 경로 %} 키워드로 레이아웃 파일을 지정하고 block 부분을 넣는다.

- 나중에 익스프레스에서 res.render('body')로 하나의 HTML로 합친 후 렌더링할 수 있으며 같은 이름의 block 부분이 서로 합쳐진다.

 

 

 

6.5.3 에러 처리 미들웨어

 

 404 응답 미들웨어와 에러 처리 미들웨어를 다음과 같이 수정해 에러 발생 시 error.html에 에러 내용을 표시한다.

 

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

 

- 404 에러가 발생한다면 res.locals.message는 ‘${req.method} ${req.url} 라우터가 없습니다.’가 된다.

( next(error)에서 넘겨준 인수가 에러 처리 미들웨어의 err로 연결되기 때문 )

- 에러 처리 미들웨어는 error라는 템플릿 파일(넌적스이므로 error.html 파일)을 렌더링한다.

( 렌더링 시 res.locals.message res.locals.error에 넣어준 값을 함께 렌더링 )

- res.render에 변수를 대입하는 것 외에도, 이렇게 res.locals 속성에 값을 대입해 템플릿 엔진에 변수를 주입할 수 있다.

- error 객체의 스택 트레이스(error.html의 error.stack)는 시스템 환경(process.env.NODE_ENV)이 production(배포 환경)이 아닌 경우에만 표시되며,  배포 환경인 경우에는 에러 메시지만 표시된다.

( 에러 스택 트레이스가 노출되면 보안에 취약할 수 있기 때문 )

 

서버를 실행하고 localhost:3000/abc에 접속하면 에러 메시지와 함께 응답 코드, 스택 트레이스를 확인할 수 있다.

 

404 아래의 Error 부분이 스택 트레이스

→ 스택 트레이스를 통해 서버 폴더 구조를 유추할 수 있으므로 배포 환경에서는 숨기는 것

 

 

 

이제 서버 만들기를 위한 기본 준비는 다 끝났고, 앞으로 한 가지만 더 배우면 실제 서비스를 만들 수 있다. 사용자들이 보낸 데이터들을 저장하는 곳, 바로 데이터베이스이다. 이건 다음장에서 알아보도록 하자 ✌🏻(-⏝-)✌🏻

반응형