Express.js의 next 함수는 무엇일까?

1


https://expressjs.com/en/guide/writing-middleware.html

공식 홈페이지의 설명을 보면 미들웨어 함수는

function(req, res, next) {
	do something...
    next(); // call next middleware function
}

의 형태를 가지고 있다고 합니다. 이때 next 함수를 콜 해주어야만 express가 다음 미들웨어를 실행시킵니다. next를 call 하지 않으면 영원히 멈춰있다고 합니다.

it must call next() to pass control to the next middleware function. Otherwise, the request will be left hanging.

그런데 이렇게 설명만 들으면 생기는 의문점이 한두 개가 아닙니다. (괄호 안에 빠른 정답을 넣어뒀습니다.)

  • next는 도대체 어떤 함수인 걸까요?
  • next에 인자를 넣을 수 있을까요? (yes)
  • next는 여러 번 콜 할 수 있는 걸까요? (no)
  • next는 단지 express에게 다음 미들웨어를 실행시키라고 signaling의 역할을 하는 함수인 걸까요?
  • 미들웨어 여러 개를 연결하면 콜 스택이 계속 쌓일까요? 즉, Maximum Call Stack Size Exceeded 문제가 생기지는 않을까요?

위 질문들에 답하기 위해 여러가지 실험을 해서 블로그에 정리해 보겠습니다.


콜 스택 실험

우선 express js의 작동 방식에 대해서 간단하게 살펴보겠습니다.

아주 아주 간단한 헬퍼 함수 fn1 부터 fn4까지 만들어보겠습니다.

fn1 함수는 그냥 함수를 시작할 때 fn1 start를 출력하고, next를 콜한 뒤, 함수가 끝날 때 fn1 end를 출력하는 아주 아주 간단한 함수입니다.

const fn1 = (req, res, next) => {
  console.log("fn1 start");
  next();
  console.log("fn1 end");
}

const fn2 = (req, res, next) => {
  console.log("fn2 start");
  next();
  console.log("fn2 end");
}
...
// fn3, fn4도 위와 비슷한 형식으로 선언합니다.

이렇게 4개의 함수를 만들었다면, 그 다음에는 express를 설치한 후 실행합니다.

const app = express();
// fn1 ~ fn5는 함수입니다.
app.get("/hello", fn1, fn2, fn3, fn4);

app.listen(1234, () => {
  console.log('app running!');
}

위 코드를 실행시킨 후 localhost:1234/hello로 접속하면, 함수 fn1, fn2, fn3, fn4가 순서대로 실행됩니다. 아래는 그 결괏값입니다.

app running!
fn1 start
fn2 start
fn3 start
fn4 start
fn4 end
fn3 end
fn2 end
fn1 end

surprise

매우 놀라운 결과!

fn1에서 next를 부르면 fn2, fn3를 부르고, 기다렸다가 next가 끝난 뒤에 fn1 end를 출력하는 것을 볼 수 있습니다! 즉, express는 next를 단순히 다음 미들웨어를 부르는 시그널링 함수로 구현한 것이 아니라, 나름의 미들웨어들의 콜 스택을 유지한다는 것을 알 수 있습니다! 즉, fn1은 fn2, fn3... 와 같은 다음 미들웨어들이 끝날 때까지 기다렸다가, 남은 작업을 처리합니다.

아래와 같은 결과가 나왔다면 그냥 순서대로 미들웨어가 실행되고 끝이 났다고 생각했을 겁니다.

app running! 
fn1 start
fn1 end
fn2 start
fn2 end
fn3 start
fn3 end
fn4 start
fn4 end // 제가 예상했던 결과 (실제로는 x)

즉, next 함수 다음에 무언가를 실행하는 것은 일종의 defer 연산의 역할을 한다고 볼 수 있습니다.

그런데, 특별한 경우가 아니라면 한 미들웨어에서 next를 콜해서 다음 미들웨어를 실행했는데, 여전히 이전 미들웨어의 동작이 남아있는 것을 비직관적이라고 여길 수도 있습니다. 예를 들어 next 함수 다음 줄에 실수로 추가 연산을 적어서 생기는 Unexpected behavior 때문에 디버깅에 시간을 낭비할 여지가 있다고 합니다.

그럴 때 이를 방지하기 위해서, next 연산 다음에는 아무것도 없다는 것을 강조하기 위해

return next();

의 형태로 next를 콜 하는 방식도 자주 쓰인다고 합니다. 별 건 아니고, 다음 미들웨어의 실행이 끝나자마자 현재 미들웨어를 바로 종료한다는 것을 명시하는 행위입니다. 아주 꿀팁입니다! 이렇게 해두면 next 함수 콜 다음에 실수로 그 어떤 연산을 적는다고 하더라도 unreachable statement이기 때문에 실행이 되지 않고 warning을 띄워 줄 겁니다.


next("route")는 무엇일까?

위 예시 코드를 다음과 같이 두 개의 app.get으로 분리해도 똑같은 출력값이 나옵니다!

const app = express();

app.get("/hello", fn1, fn2);

app.get("/hello", fn3, fn4);

app.listen(1234, () => {
  console.log('app running!');
}

신기하죠? app.get을 두 개 정의해도 위에서부터 아래로 순차적으로 실행이 됩니다.

그런데, 저렇게 두 개의 app.get을 따로 따로 선언한 상태에서 fn1의 next() 함수에 "route"라는 string literal을 인자로 넘기면 신기한 일이 벌어집니다.

"rout"도 안되고, "route123"도 안됩니다. 무조건 "route"여야 합니다! option도 아니고 enum도 아니고 string 형태로 행동을 제어하는 게 생소하네요. design을 hate한다는 스택오버플로 댓글들도 꽤 있습니다.

const fn1 = (req: Request, res: Response, next: NextFunction) => {
  console.log('fn1 start');
  next("route");
  console.log('fn1 end');
};

// fn2, fn3, fn4는 바꾸지 마세요!

fn1을 이렇게 바꾸었더니, 결과값이 글쎄,

app running!
fn1 start
fn3 start
fn4 start
fn4 end
fn3 end
fn1 end

이렇게 바뀌었어요!

fn2가 사라졌습니다.

사실 순차적으로 실행되었다고 하더라도, 같은 app.get 함수 안에 정의된 함수들을 같은 라우터 미들웨어 스택 안에 있는 미들웨어 함수(middleware functions from the same router middleware stack)라고 합니다. fn1, fn2는 같은 라우터 미들웨어 스택에 정의된 거고, fn2, fn3는 서로 다른 app.get에 정의되었기 때문에 다른 router middleware stack에 들어가 있는 거라고 합니다.

next("route") 함수는 같은 라우터 미들웨어 스택에 정의된 미들웨어 함수들을 모두 스킵하고, 다음 라우터 미들웨어 스택으로 강제로 넘어가는 역할을 합니다! 다시 말해, 미들웨어 실행 순서를 제어할 수 있게 해줍니다. 따라서 fn2가 스킵되고 바로 fn3가 실행된 것이라고 해석할 수 있습니다.

이렇듯, next 함수는 단순히 미들웨어 함수들을 순차적으로 실행하는 것을 도와주는 보조적인 flag나 signal 역할만 하는 것이 아니라, express가 어떤 middleware 함수를 언제 실행할 건지를 알려주는 중추적인 역할을 담당하고 있습니다.


다음 포스트에는 next 함수의 추가적인 역할에 대해서 알아보겠습니다.

next("router") // 라우터 레벨 스킵 (route가 아니라 router)
next(abcdefg) // 에러 핸들링 

끝.

0개의 댓글

관련 채용 정보