[Node.js] express 미들웨어

김씨·2023년 1월 19일
0

Node.js

목록 보기
12/17
post-thumbnail

공식 문서를 살펴보면 미들웨어는 Request, Response에 접근 및 변경이 가능하며, Request-Response 사이클을 종료할 수 있고, next() 함수 호출을 통해 다음 미들웨어의 실행 여부를 결정할 수 있다고 설명하고 있다.
https://expressjs.com/en/guide/writing-middleware.html

간단한 라우터와 미들웨어를 작성하며 자세히 알아보도록 하자.

const express = require('express');

const app = express();

app.use('/', (req, res, next) => {
    console.log('middleware');
  	next();
});

app.get('/', (req, res) => {
    console.log('GET /');
    res.end('ok');
});


app.listen(3000, () => {
    console.log('server is running at 3000');
});

위 코드처럼 일반적으로 미들웨어는 app.use()로 등록하고, 라우터는 app.get(), app.post() 등의 Http Method와 대응되는 함수로 등록한다.

하지만 app.use()는 모든 Http Method에 대응되고, app.get()은 GET Method에, app.post()는 POST Method에 대응될 뿐, 사실은 모두 미들웨어가 될 수 있다.

다만 app.use()app.get() 사이에는 약간의 차이점이 존재하는데, 가볍게 살펴보자.

아까 작성한 코드를 실행한 뒤, Postman으로 GET /에 접속해보자.

정상적으로 응답이 오는 것을 확인할 수 있다.

터미널을 확인하면 middleware가 콘솔에 찍히며 미들웨어도 정상적으로 작동했음을 알 수 있다.

그러나 GET /a로 요청을 보내게 되면,

404 Not Found를 응답으로 받는다.
URI가 정확히 일치하지 않기 때문에 GET / 라우터가 작동하지 않은 것이다.

그러나 터미널을 보면 미들웨어는 정상적으로 실행된 것을 볼 수 있다.
URI는 똑같이 /인데 말이다.

이로 말미암아 라우터는 URI가 정확히 일치해야 작동하고, 미들웨어는 하위 URI는 신경쓰지 않는다는 사실을 유추할 수 있다.
/로 등록된 미들웨어는 /a던, /a/b던, 맨 앞 경로가 /이기만 하면 모두 일치하는 것으로 판단한다는 것이다.

app.use('/a', (req, res, next) => {
    console.log('middleware');
  	next();
});

미들웨어 경로만 '/a'로 살짝 바꾸었다.

이번에는 터미널에 middleware라는 글자가 찍히지 않는다.

미들웨어가 작동하지 않은 것이다.

미들웨어에 등록된 URI는 /a이므로 맨 앞 URI를 비교했을 때, /a, /가 일치하지 않기 때문이다.

그러나 맨 앞 URI만 일치한다면, /a, /a/b, /a/b/c 모두 미들웨어가 작동할 것이다.

/a는 당연히 제대로 작동한다.


/a/b도 마찬가지다.


역시 /a/b/cmiddleware가 콘솔에 찍히는 것을 확인할 수 있다.

이렇게 app.use()를 통한 미들웨어 등록은 URI가 정확히 일치하지 않아도, 맨 앞 경로만 일치하면 동작하도록 설계되어 있으며, app.get()은 exact path, 정확히 일치하는 경로일 때만 실행된다는 차이가 있다.

그리고 짐작했겠지만, app.use()에 아무런 경로를 등록하지 않으면 app.use('/')와 동일하게 작동한다.

다음으로 next() 함수에 대해 자세히 알아보자.

next

이 부분을 설명하지 않고 경로에 대해서만 설명했기 때문에 조금 헷갈렸을 수도 있는데, app.use(), app.get(), app.post() 등으로 등록된 미들웨어, 라우터들은 작성된 순서대로 위에서 아래로 실행된다고 생각하면 된다.

물론 경로가 일치해야 하며, app.use()app.get()이 경로 인식을 다르게 하는 것은 위에서 충분히 설명했다.

그래서 app.use('/'), app.get('/') 2개의 미들웨어, 라우터가 있으며 경로가 같은 상황에서 GET /로 요청을 보내게 되면 위에서부터 작성된 순서대로 실행된다.

위 코드에서는 app.use('/')가 먼저 실행될 것이다.
그리고 그 안에서 next()가 실행되는 것을 볼 수 있는데,
next()가 호출하는 것이 다음 미들웨어/라우터를 실행하라는 의미다.

그래서 위 코드를 살짝 바꾸면 라우터가 실행되지 않게 만들 수 있다.

const express = require('express');

const app = express();

app.use('/', (req, res, next) => {
    console.log('middleware');
  	// next();
});

app.get('/', (req, res) => {
    console.log('GET /');
    res.end('ok');
});


app.listen(3000, () => {
    console.log('server is running at 3000');
});

next()를 호출하지 않도록 변경한 뒤, GET /으로 요청을 보냈다.

미들웨어는 실행되었기 때문에 middleware가 터미널에 출력된다.

그러나 그 뒤에 응답을 보내주는 라우터는 실행되지 않기 때문에 응답을 무한히 기다리는 상태가 된다.

반드시 next()가 호출되어야 다음 미들웨어/라우터가 실행되는 것이다.

하나의 리스트 안에 미들웨어, 라우터 함수들이 나열된 모습을 생각하면 이해하는데 도움이 될 것이다.

위에 작성한 코드를 리스트로 생각하면 다음과 같은 형태일 것이다.
[app.use('/'), app.get('/')]

GET /로 요청을 보내면 위에 작성된 미들웨어(app.use('/'))가 먼저 실행되고,
그 안에서 next()가 호출되면 그 다음으로 라우터(app.get('/'))가 실행되는 그림이다.

다음 미들웨어가 있더라도 이전 미들웨어에서 next()가 호출되지 않으면 해당 미들웨어를 끝으로 동작이 중단된다.

다음과 같이 미들웨어 5개가 있다고 생각해보자.
[A, B, C, D, E]
경로는 모두 /를 가리키고 있다고 가정하고, C를 제외한 모든 미들웨어에서 next()가 호출된다고 생각해보자.

먼저 A가 실행되고, next() 호출, B가 실행되고, next()호출, Cnext() 호출이 없으므로 C를 끝으로 D, E는 실행되지 않고 중단될 것이다.

이를 실제 코드로 작성하면 아래와 같은 형태일 것이다.

const express = require('express');

const app = express();

// middleware A
app.use((req, res, next) => {
    console.log('A');
  	next();
});

// middleware B
app.use((req, res, next) => {
    console.log('B');
  	next();
});

// middleware C
app.use((req, res) => {
    console.log('C');
});

// middleware D
app.use((req, res, next) => {
    console.log('D');
  	next();
});

// middleware E
app.use((req, res, next) => {
    console.log('E');
  	next();
});

app.listen(3000, () => {
    console.log('server is running at 3000');
});

코드를 실행시키고 GET /으로 접속해보자.

res.send()로 응답을 보내주는 부분이 없기 때문에 응답을 받지 못하고 계속해서 기다리는 상태가 되겠지만, 어쨌든 미들웨어 A, B, C가 실행되어 터미널에 글자가 출력되는 것을 볼 수 있다.
미들웨어 C는 next()를 호출하지 않기 때문에 앞서 가정했던 것처럼 D, E는 실행되지 않는다.

app.use()로 각 미들웨어를 하나씩 등록하지 않고, 하나의 app.use()에 여러 개의 미들웨어를 등록하는 것도 가능하다.

const express = require('express');

const app = express();

// middleware A
app.use((req, res, next) => {
    console.log('A');
  	next();
// middleware B
}, (req, res, next) => {
    console.log('B');
    next();
// middleware C
}, (req, res) => {
    console.log('C');
// middleware D  
}, (req, res, next) => {
    console.log('D');
    next();
// middleware E
}, (req, res, next) => {
    console.log('E');
    next();
});

app.listen(3000, () => {
    console.log('server is running at 3000');
});

위 코드를 실행하고 GET /에 접속하면 아까와 같은 결과가 출력되는 것을 볼 수 있다.

앞서 살펴봤던 미들웨어들은 app.use()로 경로 설정 없이 등록하여 어떤 URI에 접속하더라도 미들웨어가 실행되었는데, 특정 URI에 접속했을 때만 실행되는 미들웨어를 만들고 싶다면, 아래와 같은 형태로 작성하는 것도 나쁘지 않다.

app.get('/a', (req, res, next) => {
    console.log('middleware only for /a');
  	next();
}, (req, res) => {
	res.setHeader('Content-Type', 'text/html');
    res.status(200).send('<h1>/a</h1>');
});

이렇게 작성하면 GET /a로 접속했을 때만 해당 미들웨어가 실행될 것이다.
물론 3번째 인자로 주어진 핸들러도 정상적으로 작동할 것이다.

이를 살짝 확장해보자.

const express = require('express');

const app = express();

app.use((req, res, next) => {
    console.log('middleware for every routes');
    next();
});

app.get('/a', (req, res, next) => {
    console.log('middleware only for /a');
  	next();
}, (req, res) => {
    res.setHeader('Content-Type', 'text/html');
    res.status(200).send('<h1>/a</h1>');
});

app.get('/', (req, res) => {
    res.setHeader('Content-Type', 'text/html');
    res.status(200).send('<h1>/</h1>'); 
});


app.listen(3000, () => {
    console.log('server is running at 3000');
});

첫 미들웨어는 경로가 지정되어 있지 않기 때문에 어떤 경로로 접속하더라도 실행될 것이다.
그러나 두번째 미들웨어는 오직 GET /a로 접속했을 때만 실행된다.

GET /, GET /a로 요청을 보내고 터미널을 확인하여 검증해보자.

GET /로 접속했다.

첫번째 미들웨어는 경로 상관없이 실행되어 middleware for every routes가 출력되는 것을 볼 수 있다.


GET /a로 접속했다.

첫번째 미들웨어가 역시 잘 실행되어 middleware for every routes가 출력되었으며, middleware only for /a도 정상적으로 출력되었다.

이전에도 봤던 내용이지만 미들웨어는 실행되는 순서가 매우 중요하다.

const express = require('express');

const app = express();

app.use(express.json());
app.get('/', (req, res) => {
  	const body = req.body;
	console.log(body);
    res.status(200).json(body); 
});

app.listen(3000, () => {
    console.log('server is running at 3000');
});

json body를 파싱하는 미들웨어를 등록한 뒤, body를 파싱하여 그대로 클라이언트에 응답을 보내주는 간단한 코드다.
참고로 res.send()가 아닌 res.json()를 사용해서 응답을 보냈는데, 이러면 자동으로 Response Header의 Content-Type을 application/json으로 설정해준다.

만약 미들웨어가 라우터 밑에 등록된다면?

const express = require('express');

const app = express();

app.get('/', (req, res) => {
  	const body = req.body;
	console.log(body);
    res.status(200).json(body); 
});

app.use(express.json());

app.listen(3000, () => {
    console.log('server is running at 3000');
});

파싱되기 전에 req.body에 접근하면 undefined가 되기 때문에 응답으로 받는 json도 비어있을 것이다.

또 아까 봤듯, app.get()도 미들웨어가 될 수 있다.

const express = require('express');

const app = express();

app.use((req, res, next) => {
    console.log('GET everywhere');
    next();
});

// app.get can be used as a middleware
app.get('/', (req, res, next) => {
    console.log('GET /');
    next();
});

app.get('/', (req, res) => {
    res.status(200).json({
        ok: true,
    }); 
});

app.listen(3000, () => {
    console.log('server is running at 3000');
});

GET /로 요청을 보내보자.

업로드중..

두 미들웨어의 출력이 정상적으로 작동하는 것을 볼 수 있다.

그러나 여기서도 한가지 주의할 점이 있다.

express 공식 문서에 정의된 미들웨어의 역할 중에 Request-Response 싸이클 내에 Request, Response 객체에 접근, 수정할 수 있는 기능이 있다고 적혀 있다.

말이 조금 어려운데, 우리가 계속해서 클라이언트에 응답을 보낼 때, res.status(200).send() 등의 메소드를 호출했었던 것을 생각해보자.
req는 Request 객체고, res가 Response 객체다.
대부분의 사람들이 이를 줄여서 req, res라고 작성하기 때문에 이게 컨벤션이 된 것이다.
res.status(200)은 응답 상태 코드를 200으로 지정해주는데, 이것이 Response 객체에 접근, 수정하는 행위인 것이다.

그리고 Request-Response 사이클은 클라이언트가 요청을 보낸 뒤부터 서버에서 경로가 일치하는 미들웨어, 라우터를 쭉 실행하고 res.send(), res.json() 등으로 응답을 반환할 때까지의 과정을 말한다.

아까 말한 주의할 점이란,
res.send(), res.json() 등으로 클라이언트에 응답을 반환한 뒤에 다시 한 번 res.send()로 응답을 반환하지 않도록 하는 것이다.

const express = require('express');

const app = express();

app.get('/', (req, res) => {
    res.json({
        ok: true,
    }); 
    res.json({
        ok: true,
    }); 
});

app.listen(3000, () => {
    console.log('server is running at 3000');
});

당장 위 코드만 해도 res.json()으로 클라이언트에 응답을 2번 반환하도록 작성되어 있다.

Error [ERR_HTTP_HEADERS_SENT]: Cannot set headers after they are sent to the client

요청을 보내면 위와 같은 오류가 발생한다.

이미 클라이언트에 응답을 반환한 뒤에 헤더를 변환해도 무의미하다는 것이다.

이전에 말했듯, 번지 점프를 한다면 뛰어 내리기 전에 생명줄을 걸어야 의미가 있다.

이미 res.json()이나 res.send()로 클라이언트에 응답을 보낸 순간, 점프를 한 것이다.

점프를 한 뒤에 생명줄을 걸려고 해도 걸 대상이 없기 때문에 불가능하다.

모든 작업을 완료하고 딱 한번만 응답을 보내야 하는 것이다.

이는 여러 미들웨어에서 동작할 때도 동일하다.


const express = require('express');

const app = express();

app.get('/', (req, res, next) => {
    res.status(200).json({
        ok: true,
    }); 
    next();
});

app.get('/', (req, res) => {
    res.json({
        ok: true,
    }); 
});

app.listen(3000, () => {
    console.log('server is running at 3000');
});

위 코드도 미들웨어, 라우터에서 각각 응답을 보내고 있기 때문에 한 번은 무의미한 응답을 보내는 것이 된다.
그래서 방금 봤던 코드와 완벽히 똑같은 오류가 발생한다.

글이 너무 길어졌기 때문에 나머지는 다음 포스트에 나눠서 정리하도록 하겠다.
express는 수많은 미들웨어들이 모여서 서버를 이루는 것과 다름 없기 때문에 매우 중요한 개념이므로 반드시 정확히 숙지해야 한다.

0개의 댓글