나만의 Express.js 구현하기 (1) - 노드 Error에 관한 고민

0


현재 나만의 Express.js를 만들어나가는 중이다. 목표는 다음과 같다.

  1. Middleware pattern을 완벽하게 지원해야 한다.
  2. 에러 핸들링을 지원해야 한다. Async middleware (request handler)의 에러까지 잘 잡아줘야 한다.
  3. Router를 구현해야 한다.
  4. 진짜 Express용 middleware를 npm을 통해 깔아서 적용시켜도 잘 동작해야 한다. (예시: serve-favicon, cookie-parser 등)

현재의 1번의 진행상황은 나름 만족스럽다. 다음 코드를 살펴보자.

myExpressApp.use((req: MyRequest, res: MyResponse, next: MyNextFunction) => {
  log.info('middleware 1');
  next();
  log.info('middleware 1 after next');
});

myExpressApp.use((req: MyRequest, res: MyResponse, next: MyNextFunction) => {
  log.info('middleware 2');
  next();
  log.info('middleware 2 after next');
  res.send('Hello World');
});

위와 같은 코드를 실행시키면 어떤 결과가 나와야 할까?

위와 같이 원하는대로 잘 동작한다!

middleware 1 start
middleware 2 start
middleware 2 end
middleware 1 end

형식으로 스택 구조가 유지된다. 여기까지가 에러 없는 진행상황이다.

이제 2번을 지원해야 할 시간이다. 버그의 늪으로 빠져보자.

처음 빨간 색 줄글은 내가 의도한 에러라서 괜찮다. global error handler가 called되는 모습을 보고 싶었다.

그러나 그 이후 socket error가 문제다.
직접 만든 MyExpress에서 write after end 에러가 났다.

글로벌 에러 핸들러에서 에러를 처리한 이후에도 무언가 write 과정이 있었던 것 같다.

myFabicon에서 에러가 나면 이를 글로벌 에러 핸들러에 넘기고 실행을 정지하기를 바랐는데, 어디서 잘못된 것일까?

직접 작성한 글로벌 에러 핸들러를 살펴보았다.

이런... return이 없었다. 그러니 error handler가 call 된 이후에도 계속 middleware를 실행했지...

바로 return을 적어서 재실행보았다.

그러니까 다른 에러가 발생했다..!

뭔가 에러 발생 이후에도 계속 middleware를 실행해 나가는 기존 문제가 해결된 것 같긴 한데, 이 에러는 왜 발생한 것일까?

우선, 빈 줄로 구분되어 있는 에러들은 서로 다른 에러들이다. 즉, global error handler가 두 번 call 된 것이라고 볼 수 있다. 왜 이런 현상이 일어나는 것일까?

일단 NotFoundError는 내가 직접 만든 custom error인데, stack trace를 출력해주지 않는 부분이 마음에 걸렸다.

다음은 myFabicon, 즉 내가 직접 만든 serve-fabicon 미들웨어인데, 이런... 반성해야 할 부분이다.

에러를 try catch로 잡고 에러의 타입을 확인해서 next로 던지는 건 좋았는데, new NotFoundError를 만들면서 기존의 err는 버려졌다... 이러니 스택트레이스며 모든 정보가 다 날라갔다. 우선 이것부터 고쳐야겠다.

Node.js의 에러 관련 소스코드를 직접 살펴보면서 공부했다.
assertion_error.js

그런데 생각해보니, 디버깅을 하고 싶으면 NotFoundError에다가 err를 넣거나 할 필요 없이 그냥 err를 일단 출력해주면 되는게 아닐까?


error 에서 최대한 많은 정보 출력하기

디버깅 용으로는

이 6가지 방법 중 뭐가 가장 좋을까? 각각 출력된 6개의 에러는 다음과 같다.

일단 ESLint에 No console을 적어뒀기 때문에 console은 좋은 선택이 아닌 것 같다. 그리고 내부적으로 util.inspect 라이브러리를 사용한다고 들었다.

그렇다고 매번 log.error('%o', ...)를 사용하기에는 좀 귀찮고, 그렇다고 log.error만을 출력하자니 잃어버리는 에러의 정보가 아까웠다.

일단은 log.error('%o')로 합의봐야겠다.


isNodeError type predicate 만들기

Error는 파고들면 파고들수록 심연에 다가가는 느낌이다.

          console.log(util.inspect(err, { showHidden: true }));

이걸 통해 에러 메세지의 모든 것에 겨우 다가간 느낌이었지만, name은 출력되지 않는다...

겨우 수소문해가면서 모든 properties를 출력하는 함수를 만든 후에야 error의 모든 정보를 출력할 수 있었다.

도대체 name과 stack, message는 뭐가 다르길래 하나는 후자는 util.inspect에 보이고 전자는 안 보이는 것일까?

나는 바보다. name은 Error.prototype.name 이었고, message는 그냥 instance property였다.

그런데 mdn에서는 Error.prototype.stack이다. stack도 안 보여야 하는 것 아닐까? 하지만 non-standard이니 node에는 다르게 구현되어 있을 수도 있다.

Node.js에서는 error.stack이 Error.prototype.stack과 다른 것 같다!

그런데 내 에디터에서는 Error를 es5 기준으로 타입 체킹해주는 것 같다... 이걸 어떻게 고치지? tsconfig.json를 고쳐봐야겠다.

이 문제에 대해 다룬 블로그 글도 있다.

The Problem with Handling Node.js Errors in TypeScript (and the workaround) - DEV Community

node.js - In Typescript how do you make a distinction between Node and vanilla Javascript Error types? - Stack Overflow

아무래도 블로그글과 스택오버플로가 말한 대로 아직까지는 NodeJs.ErrnoException을 사용해야 할 것 같다.

        if (err instanceof Error && 'code' in err && err.code === 'ENOENT') {

이 너저분한 코드를 이제 고칠 수 있다!

이런 식으로 type predicate을 설정해 준 뒤,

isNodeError를 조건문 안에 적용해주면 이런 식으로 node 에러들의 자동완성이 뜬다!

기나긴 여정이었다...


static abstract...

Error -> CustomNodeError -> CustomHttpError 를 상속하다가 문득 생각했다. 왜 static abstract 은 없는걸까?


winston coloring

윈스턴은 이렇게 알록달록하게 출력하는 방법들이 있다.


To be continued...

0개의 댓글