먹고 기도하고 코딩하라

[Node.js] express 미들웨어란? 본문

Javascript

[Node.js] express 미들웨어란?

사과먹는사람 2020. 7. 7. 10:11
728x90
728x90

미들웨어 사용하기

Express 미들웨어란 무엇인가? 쉽게 말해 함수이다. Express에서는 사실상 모든 것이 미들웨어이다. 내가 이해하기로 미들웨어와 미들웨어 함수는 같은 말이다(아니라면 댓글 부탁드립니다). 문서에는 미들웨어는 애플리케이션의 요청-응답 주기 중 req, res 객체에 대한 접근 권한을 갖고 변형시킬 수 있으며 미들웨어 스택 내 다음 미들웨어 함수에 대한 접근 권한을 next라는 인자로 갖는 함수라고 되어 있다. 또한 next 호출을 통해 다음에 있는 미들웨어를 실행하도록 결정할 수도 있다. 말이 조금 어려운데 아직 미들웨어가 무엇인지 잘 모른다면 생활코딩 express 강의를 보기를 강력히 권한다. 초보자 입장에서 가장 쉽게 이해할 수 있도록 미들웨어를 설명해 주신다. 

미들웨어엔 순서가 있다. 코드의 상단에 있는 것일수록 먼저 실행되고 하단에 있는 것일수록 나중에 실행된다. 하단 미들웨어의 경우 위에서 next를 하느냐 마느냐에 따라 실행이 될 수도 안 될 수도 있는데 경우마다 조금씩 다르다.

 

또한 미들웨어는 애플리케이션 레벨, 라우터 레벨, 오류 처리, 기본 제공, 써드파티 미들웨어가 있다.

 

1. 애플리케이션 레벨 미들웨어

애플리케이션 레벨이란 아래서 살펴볼 express()로 생성할 수 있는 app 객체의 app.use()나 app.METHOD()(ex. app.get, app.post) 함수를 이용해 미들웨어를 app 인스턴스에 바인딩하는 미들웨어이다. 마운트 경로가 없는 미들웨어 함수는 앱이 요청을 수신할 때마다 실행하게 된다.

//마운트 경로가 있으며 다음 미들웨어 함수를 호출함
app.get('/pages/', (req, res, next) => {
  console.log('Time : ', Date.now());
  next();
});

//마운트 경로가 없으며 응답을 여기서 끝냄
app.get((req, res, next) => {
  console.log('Not Found');
});

하나의 경로에 app METHOD 여러 개가 묶일 수 있는데 다음 예시를 주의깊게 보길 바란다.

app.get('/pages/:id', (req, res, next) => {
  //pages id가 0이면 'regular'가 아닌 'special'로 넘어감
  if (req.params.id == 0) next('route');
  //pages id가 0이 아니라면 'regular'로 넘어감
  else next();
}, (req, res, next) => {
  res.send('regular');
}
});

//pages id가 0일 때 넘어올 미들웨어
app.get('/pages/:id', (req, res, next) => {
  res.send('special');
}

이 경우 위에 미들웨어가 2개 묶인 app METHOD가 위에 있으므로 일단 special보다 더 먼저 실행된다.

next('route')는 지금 라우터 미들웨어 스택을 벗어난 다음 라우터(special)로 제어가 넘어가게 하는 것이다. 라우터 미들웨어 스택이란 하나의 app.use()나 app.METHOD()에 묶인 라우팅 미들웨어들을 말한다. 여기서는 if-else 절이 든 미들웨어와 regular 미들웨어가 한 스택에 같이 있다고 할 수 있다.

여기서 그냥 next()는 regular로 넘어가게 된다. id가 0이 아니라서 regular로 넘어간다면, 거기서 next()를 호출하지 않았으므로 special은 호출되지 않는다. 

next() 안에 인자가 들어가는 경우는 아마 next('route')와 next(err)뿐일 것 같다. next 안에 뭔가 인자가 들어가면 express 앱은 그것을 오류 발생이라고 보고 오류 처리와 관련없는 다른 모든 미들웨어를 다 건너뛰고 오류 처리(error handling) 라우터로 넘어간다. 단 하나 'route'만 빼고 말이다. 'route'는 현재 메소드를 벗어나 path에 해당하는 다음 라우터로 제어가 넘어가는 것이다.

 

2. 라우터 레벨 미들웨어

라우터 레벨은 express.Router()로 생성할 수 있는 router 인스턴스에 미들웨어가 바인딩되는 것이다. 그것 외에는 애플리케이션 레벨 미들웨어가 차이가 없다. router.use()나 router.METHOD() 함수를 이용해 로드할 수 있다.

express.Router() 로 router 객체를 생성할 수 있는데, 미들웨어와 HTTP 메소드 라우트를 router 객체에 붙일 수 있다. 아래 코드를 보길 바란다.

//app.js
const express = require('express');
const app = express();
const pageRouter = ('./routes/pages');
app.use('/pages', pageRouter);
//pages.js
const express = require('express');
const router = express.Router();

router.get('/pages/:id', (req, res, next) => {
  //pages id가 0이면 'regular'가 아닌 'special'로 넘어감
  if (req.params.id == 0) next('route');
  //pages id가 0이 아니라면 'regular'로 넘어감
  else next();
}, (req, res, next) => {
  res.send('regular');
}
});

//pages id가 0일 때 넘어올 미들웨어
router.get('/pages/:id', (req, res, next) => {
  res.send('special');
}

module.exports = router;

이 때는 app.use('/', router); 하면서 main에서 app.use에 router 인스턴스를 받아야 한다. 이 router 인스턴스는 사실상 미들웨어처럼 행동하기에 app.use()의 매개변수가 될 수 있다.

 

3. 오류 처리 미들웨어

오류 처리 미들웨어는 (err, req, res, next)를 인자로 받는 것이다. 항상 4개의 매개변수가 필요하다. 이게 오류 처리 미들웨어의 시그니처다. err.stack으로 에러 메시지를 볼 수 있다.

//오류의 종류에 상관없이 모든 오류를 잡는 미들웨어
app.get((err, req, res, next) => {
  console.log(err.stack);
  res.status(500).send('Something broke!');
});

주의할 점은 오류 처리 미들웨어는 app.use() 및 라우트 호출을 정의한 뒤 거의 코드의 맨 끝에 정의해야 한다는 점이다. 

위의 경우 모든 오류를 잡는 미들웨어 하나만 만들었는데, 에러마다 다른 오류 처리 미들웨어 함수를 정의할 수도 있다. 이 경우 catch-all 에러 핸들러는 그 오류 처리 미들웨어들 중에서도 가장 아래 있어야 한다.

오류 처리 미들웨어는 다음과 같이 부를 수 있다.

app.get('/pages/:id', (req, res, next) => {
  if (!req.params.id) next(err);
});

이런 식으로 명시적으로 next(err)를 해줘야 오류 처리 미들웨어로 넘어갈 수 있다.

 

4. 기본 제공 미들웨어

기본 제공은 정적 리소스를 제공할 루트 디렉토리를 정하는 express.static 같은 것이 있다. 현재 문서에는 빌트인 미들웨어는 express.static 뿐이라고 되어 있다. 정적 파일을 전달해주는데, 여기서는 /public 디렉토리가 정적 파일들이 모여 있는 루트 디렉토리가 된다. 아래서 더 자세히 설명하겠다.

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

 

5. 써드파티 미들웨어

마지막으로 써드파티 미들웨어는 npm 에서 설치한 helmet이나 cookie-parser 같은 모듈들이 해당이 된다. 쉽게 말해 express 자체적으로 제공하지 않고 따로 설치해야 하는 것들은 다 써드파티라고 보면 된다.

npm i cookie-parser
const express = require('express');
const app = express();
const cookieParser = require('cookie-parser');

app.use(cookieParser());

이 때 cookieParser()를 하면 미들웨어를 반환한다. 

 

 

 

 

진짜 미들웨어 사용하기

본격적인 CRUD를 들어가기 전에 먼저 main 코드를 보며 미들웨어를 사용해보자. 이 코드는 Node.js 로 CRUD 구현 (3-1)의 main.js 일부이다.

 

const express = require('express');
const app = express();
const helmet = require('helmet');
const compression = require('compression');
const session = require('express-session');
const fileStore = require('session-file-store')(session);

app.use(helmet());
app.use(compression());
app.post('*', express.urlencoded({ extended: false }));
//'public' 디렉토리 안에서 static file을 찾겠다는 의미
app.use(express.static('public'));

app.use(session({
  secret: '!%#!#DAset#tGHBAX3sdag62437',
  resave: false,
  saveUninitialized: true,
  cookie: {
    httpOnly: true
  },
  store: new fileStore()
}));

일단 express를 받아와서 express()를 하면 application Object가 생성된다. 보통 이 Object는 app이라는 이름으로 받아준다. 이 Express app이 정말 많은 일을 하게 된다.

그 다음 설치한 모듈들을 하나씩 가져와서 app.use 안에 넣게 된다.

helmet, compression, session, fileStore를 다 보다 보면 공통점이 있다. app.use 안에 쓰며 이름 뒤에 ()를 붙여 함수를 호출하는 방식으로 사용한다는 것이다.

일례로 helmet을 보면 

app.use(helmet());

이렇게 쓰게 되는데, helmet()이 미들웨어를 반환하기 때문이다. app.use의 매개변수로 path는 선택이지만 미들웨어는 반드시 필요하다. 보통 이런 써드파티 미들웨어는 npm에 소개된 API 문서들을 보고 그 사용법대로 쓰면 되는데 helmet과 compression의 경우 그냥 가능한 높이 올려 app.use(helmet()) 하고 app.use(compression()) 해주면 그만이다.

이렇게 상단에 app.use를 하게 되면 서버에 요청이 들어올 때마다 무조건 helmet()을 실행하게 되는 것이다. 같은 논리로, helmet 다음에는 compression, 그 다음 express.static, session 미들웨어가 실행된다.

우리가 아침에 일어나서 세수하고 양치하고 밥 먹고 머리 빗고 옷을 갖추고 가방을 챙기는 프로세스 다음 외출을 하듯 우리 app도 일단 helmet을 씌워 앱을 공격으로부터 보호하고 압축을 하고 정적 리소스를 챙겨오고 세션도 만들어준 다음 사용자 브라우저에 내보내야 하는 것이다. 물론 우리 사람은 지각하면 밥 먹는 과정을 건너뛰기도 하지만 app은 그렇지 않다는 차이가 있긴 하다.

 

만약 POST 메소드로 요청이 들어올 때마다 어떤 동작을 하게 하려면 다음과 같이 하면 된다.

app.post('*', express.urlencoded({ extended: false }));

app.post로 들어오는 모든 경로('*')에 urlencoded 미들웨어를 실행하겠다는 이야기다. extended 옵션은 querystring 라이브러리(false)를 사용해 URL-encoded 데이터를 파싱할지, qs 라이브러리(true)를 사용해 파싱할지 정하는 옵션인데 기본이 true이다. qs 라이브러리는 npm i qs 로 설치할 수 있고 querystring은 그냥 원래 있는 querystring 을 쓰는 것이다.

POST 방식 폼 데이터가 들어왔을 때는 URL에 데이터가 붙어 나오지 않기 때문에 이전에는 req 객체에 이벤트 에미터를 붙여 데이터를 받아오고 data 이벤트가 끝나면 querystring으로 파싱해주는 걸 했는데, 저렇게 하기만 하면 req.body로 접근했을 때 querystring.parse(body) 한 것과 똑같은 결과가 나온다. 사용자가 전송한 폼 데이터를 내부적으로 받아 콜백을 호출해 req.body에 넣어주는 것이다. req.on('data')와 req.on('end')를 안 해도 된다.

express.urlencoded 는 body-parser에 기반해 request body에 붙어 오는 정보를 파싱해 준다. return 형식은 미들웨어이다. utf-8 인코딩된 request body만 받아들이고, gzip과 deflate 압축을 자동으로 해준다.

 

기본 제공 미들웨어인 express.static은 정적인 파일들을 서비스하기 위한 미들웨어이다. 매개변수로 prefix는 옵션, path는 필수이다. 여기서는 prefix 없이 path만 줬다. 이 말은 public이라는 디렉토리를 루트 디렉토리로 삼고 거기서 정적 리소스들을 가져오겠다는 말이다. 그래서 맨 위에 디렉토리 구조를 보면 public 밑에 img 폴더가 있고 그 아래 고양이 사진이 있는데, 이것을 나중에 <img src="/img/cat.jpeg" alt="고양이"> 식으로 가져올 수 있다. public이 루트 디렉토리 노릇을 하기 때문에 public을 적어줄 필요가 없다!

만약 파일이 없다면 404 에러를 뱉는 대신 next()를 호출해서 다음 미들웨어로 옮겨 간다.

 

 

 

Reference

생활코딩 Node.js - Express

 

 

728x90
반응형
Comments