[Node.js] express 미들웨어 2

김씨·2023년 1월 19일
0

Node.js

목록 보기
13/17
post-thumbnail

미들웨어는 작성 순서가 매우 중요하다.

라우터도 결국 Request-Response Cycle에 실행되는 미들웨어라고 볼 수 있을 것이다.
그렇기에 라우터도 작성 순서가 중요하다.

1. 와일드카드 라우터

특히 다음과 같은 경우에는 순서가 달라지면 개발자의 의도와 전혀 다르게 동작할 가능성이 있다.

const express = require('express');

const app = express();

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

app.get('/apple', (req, res) => {
    res.send('/apple');
});

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

id라는 Route Paramter를 받는 라우터와 GET /apple로 들어오는 요청을 처리하는 라우터가 있다.

GET /11, GET /abc 등의 요청을 보내면 첫번째 라우터에서 id로 인식하여 처리할 것이다.

예상대로 동작한다.

그럼 GET /apple로 요청을 보내면 둘 중 어떤 라우터가 실행될까.

첫번째 라우터가 먼저 작성되었기 때문에, 두번째 라우터는 도달조차 하지 못하게 된다.
URI가 완전히 일치함에도 불구하고.

그래서 GET /apple 라우터는 영원히 실행될 수 없게 된다.

GET /apple로 요청을 보낼 때만 두번째 라우터가 실행되게 하려면 어떻게 할까.


const express = require('express');

const app = express();

app.get('/apple', (req, res) => {
    res.send('/apple');
});

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

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

매우 간단하다.
두 라우터의 순서를 뒤바꾸면 된다.

이제 GET /apple로 들어오는 요청은 /apple을 응답으로 받게 될 것이다.

GET /111, GET /abc 등은 아까랑 동일하게 GET /:id 라우터에서 처리될 것이다.

순서만 바꿨을 뿐인데 결과가 이전과 완전히 달라졌다.

이렇게 하나의 경로가 아닌, 동적인 경로를 다루는 라우터들을 다룰 때는 순서를 더욱 더 조심해야 할 것이다.


const express = require('express');

const app = express();

app.get('*', (req, res) => {
    res.send('*');
});

app.get('/apple', (req, res) => {
    res.send('/apple');
});

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

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

*는 asterisk라 불리는 기호인데, 와일드카드라고도 불린다.
모든 경로를 의미하기 때문이다.

/, /abc, /sce 등 어떤 경로라도 *에 포함되는 것이다.

그래서 위 코드를 실행하면 GET /apple이던, GET /111이던 상관 없이 언제나 첫번째 라우터만 실행된다.

그 어떤 경로라도 하나의 라우터에서 실행되기 때문에 와일드카드 라우터 아래에 등록된 모든 라우터들은 의미가 없어진다.

그래서 저런 와일드카드 라우터는 반드시 맨 아래에 두어야 한다.

const express = require('express');

const app = express();

app.get('/apple', (req, res) => {
    res.send('/apple');
});

app.get('/banana', (req, res) => {
    res.send('/banana'); 
})


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

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

그러면 우리가 가진 어떤 라우터와도 일치하지 않는 경로로 접속했을 경우, 404 Not Found라는 응답을 보내줄 수 있게 된다.

라우터를 살짝 수정해서 GET /apple, GET /banana 두 개의 라우터를 가진 서버로 만들어보자.

GET /apple, GET /banana로 접속하면 잘 작동한다.

그러나 실수로 오타가 나서 GET /panana로 접속한다면?

존재하지 않는 리소스를 뜻하는 응답 코드 404과 함께 404 Not Found라는 메세지를 받게 될 것이다.

맨 위에 작성했을 때는 언뜻 모든 라우터를 무시하게 만들어서 쓸모 없는 기능처럼 보였지만,
맨 아래 작성하면 이렇게 기존 라우터들로 커버되지 않는 경로에 접속했을 때, 잘못된 경로임을 알리는 용도로 상당히 유효하게 사용할 수 있다는 사실을 알게 되었다.


2. 오류 처리 미들웨어

만약 라우터 함수 실행 중에 오류가 발생한다면 어떻게 될까?

const express = require('express');

const app = express();

app.get('/', (req, res) => {
    throw new Error('something went wrong');
});

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

GET /에 요청을 보내면 무조건 오류가 발생하도록 코드를 작성하였다.
결과를 보기 위해 요청을 보내자.

우리가 입력한 메세지 something went wrong이 제대로 출력되었으며, 상태 코드는 자동으로 Internal Server Error임을 뜻하는 500이 응답으로 주어졌다.

하지만 너무 많은 정보가 추가적으로 노출되었다.
소스 코드의 폴더 구조라던지, 오류가 발생한 코드 라인이라던지...
상용 서버라면 이런 정보를 사용자에게 노출하는 것은 매우 바람직하지 않다.

사용자가 이해할 수 있는 쉽고 간단한 메세지를 보내주는 것이 정보 노출도 적고 사용자에게도 훨씬 도움이 될 것이다.

그래서 미들웨어나 라우터 실행 중에 오류가 발생했을 때, 이런식으로 오류를 던지고 마는 것이 아니라 오류 처리를 담당하는 미들웨어를 만들어줄 필요가 있다.

오류가 발생했을 때의 처리를 담당할 특별한 미들웨어가 있는데, 함수 시그니쳐가 다른 미들웨어와 조금 다르다.
매개변수로 받는 인자의 갯수와 순서가 다르다는 뜻이다.

app.use((err, req, res, next) => {
	// ...
});

오류 처리 미들웨어의 함수 시그니쳐는 위의 형태와 같다.
첫 번째 매개변수로 오류를 받고, 나머지는 원래 미들웨어와 동일하지만, 한 칸씩 우측으로 밀린 형태다.

라우터나 미들웨어에서 오류가 발생했을 때, 이 특별한 오류 처리용 미들웨어가 실행되도록 처리를 넘길 수 있다.

아래와 같이 5개의 미들웨어가 있다고 생각해보자.
[A, B, C, D, E]
여기에 추가로 오류 처리 미들웨어가 있고, C에서 오류가 발생한다면,
A 미들웨어에서 처리를 완료하고 next()를 호출하여 B로 넘어가고,
B에서 처리를 완료하고 next()를 호출하여 C로 넘어간 다음,
C에서 오류가 발생했을 때, next에 인자를 넣어 next(error)와 같은 형태로 실행한다.
인자는 원하는 형태로 넣어줘도 된다.

어쨌든 next가 호출될 때, 인자가 비어있지 않고 무언가 들어있다면 express는 오류가 발생했다고 인지하고 나머지 미들웨어를 모두 건너 뛰고 오류 처리 미들웨어로 넘어간다.

즉, A -> B -> C -> Error Middleware의 순서로 실행이 되는 것이다.
이를 코드로 작성해보자.


const express = require('express');

const app = express();

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

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

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

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

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

app.use((err, req, res, next) => {
    res.status(400).send(err);
});

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

GET /으로 요청을 보내면 상태 코드 400, error을 응답으로 받는다.

미들웨어는 C까지 실행되었음을 볼 수 있다.

실제로 오류가 발생하지 않았음에도 불구하고 next()에 단순 문자열을 인자를 전달했을 뿐인데 다음 미들웨어를 모두 건너 뛰고 오류 처리 미들웨어로 넘어갔다.

const express = require('express');

const app = express();

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

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

app.use((req, res, next) => {
    try {
        console.log('C');
        throw new Error('something went wrong');
    } catch (err) {
        next(err);
    }
    next('error');
});

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

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

app.use((err, req, res, next) => {
    res.status(400).send(err.toString());
});

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

이번에는 실제로 C 미들웨어에서 오류를 발생시키고, 오류 객체를 담아 next()로 전달했다.

오류 처리 미들웨어에서 err.toString()을 상태 코드 400과 함께 반환했다.
미들웨어 C에서 오류 객체를 생성할 때 넣었던 something went wrong이라는 메세지가 제대로 출력된다.

마찬가지로 미들웨어도 C까지 실행되고 이후 D, E는 실행되지 않았다.

예상대로 잘 동작한다.


3. next('route')

공식 문서에는 이렇게 기재되어 있다.

You can provide multiple callback functions that behave like middleware to handle a request. The only exception is that these callbacks might invoke next('route') to bypass the remaining route callbacks.

하나의 미들웨어에 여러 개의 콜백 함수를 전달할 수 있으며, next('route')를 실행하면 아직 실행되지 않은 나머지 콜백 함수들을 건너 뛴다고 적혀있다.

아래 코드를 보며 좀 더 자세히 알아보자.

app.get('/', (req, res, next) => {
  console.log('1');
  next('route');
}, (req, res) => {
	console.log('2');
}, (req, res) => {
	console.log('3');
});

app.get()에 3개의 콜백 함수가 인자로 전달된다.
원래는 모든 콜백 함수가 처리되어야 하지만, 첫 번째 콜백함수에서 next('route');와 같이 특수한 형태로 next()를 실행하기 때문에 나머지 콜백함수는 건너 뛰게 된다.

그래서 터미널에는 1만 출력될 것이다.

const express = require('express');

const app = express();

app.get('/', (req, res, next ) => {
    console.log('A');
    next('route');
}, (req, res, next) => {
    console.log('A-2');
    next();
}, (req, res, next) => {
    console.log('A-3');
    next();
});

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

app.get('/', (req, res, next ) => {
	console.log('C');
	res.send('ok');
});

app.use((err, req, res, next) => {
    console.log('error middleware');
    res.status(400).send(err.toString());
});

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

그럼 위 코드를 실행하고 GET /로 요청을 보내면 어떤 결과가 나올까.
app.get('/')에 해당되는 라우터가 3개나 있다.
그리고 첫번째 라우터에는 3개의 콜백 함수가 들어있기 때문에 원래라면 총 5개의 콜백 함수가 실행되어야 할 것이다.
그럼 아래와 같이 출력될 것이라고 예상할 수 있다.

A
A-2
A-3
B
C

그러나 첫 번째 라우터의 첫 번째 콜백 함수에서 next('route');를 호출했기 때문에, 첫 번째 라우터의 나머지 2개의 콜백 함수는 건너 뛰게 된다.

그래서 실제 실행 결과는 위와 같다.

주목해야 할 부분은 next() 함수에 인자를 전달하면 오류 처리 미들웨어로 바로 넘어간다고 직전에 배웠었는데, 오류 처리 미들웨어는 실행되지 않고 나머지 미들웨어가 정상적으로 실행된다는 것이다.

express에서 next('route');를 특별한 함수로 인식하기 때문에 이렇게 처리되는 것이다.

만약 오타가 나서 next('route2');로 인자를 잘못 입력한다면?

바로 오류 처리 미들웨어로 넘어가게 될 것이다.


4. app.route()

공식 문서에서는 chainable route handlers라고도 언급한 기능이다.
사용 방법은 매우 간단하다.

const express = require('express');

const app = express();

app.route('/book')
  .get((req, res) => {
    res.send('Get book');
  })
  .post((req, res) => {
    res.send('Post book');
  })
  .put((req, res) => {
    res.send('Put book');
  })
  .delete((req, res) => {
  	res.send('Delete book');
});

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

경로가 같고 여러 가지 Http Method에 대응하는 라우터를 만들고 싶을 때 위와 같이 사용할 수 있다.

GET /book, POST /book, DELETE /book 등, 어떤 Http Method로 요청을 보내더라도 작성한 그대로 응답을 받는 것을 볼 수 있다.


5. express.Router()

라우터가 많아지고 경로가 복잡해지면 이를 나누고 싶어질 때가 올 것이다.

예를 들어 경로가 /a/b/c/d/e와 같이 복잡해진다면, 모든 라우터가
app.get('/a/b/c/d/e')와 같은 형태가 될 것이다.

express.Router()로 라우터를 나눌 수 있다.
이를 서브 라우터라고 부르겠다.
예를 들어 const subRouter = express.Router();로 새로운 서브 라우터를 만들어보자.

const express = require('express');

const app = express();

const subRouter = express.Router();

app.use('/a/b/c', subRouter);

subRouter.get('/d/e', (req, res) => {
	res.send(req.url);
});

subRouter.get('/d/e/f', (req, res) => {
	res.send(req.url);
});

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

app.use()의 경로를 /a/b/c로 주고 subRouter를 미들웨어처럼 등록하게 되면, subRouter에 등록된 모든 라우터들의 기본 경로가 /a/b/c로 지정된다.

그래서 subRouter 아래의 라우터 경로를 /d/e로 주더라도 기본 경로가 /a/b/c이기 때문에, 실제 경로는 /a/b/c/d/e가 되는 것이다.

경로가 길어질 경우 매 번 /a/b/c를 입력할 필요 없이, 자동으로 입력해 주는 것이다.

잘 작동하는지 GET /a/b/c/d/e에 요청을 보내보자.


잘 작동한다.
다만, req.url에 들어있는 값은 전체 경로가 아닌, 해당 라우터에 등록된 경로만 추출된다는 점에 주의하자.(실제 경로는 /a/b/c/d/e지만 /d/e만 추출)

매우 단순한 예만 살펴봤는데, 라우터와 경로의 수가 수백, 수천 개로 늘어날 때 더욱 빛을 발할 기능인 것 같다.

0개의 댓글