현재 나만의 Express.js를 만들어나가는 중이다. 목표는 다음과 같다.
현재의 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를 일단 출력해주면 되는게 아닐까?
디버깅 용으로는
이 6가지 방법 중 뭐가 가장 좋을까? 각각 출력된 6개의 에러는 다음과 같다.
일단 ESLint에 No console을 적어뒀기 때문에 console은 좋은 선택이 아닌 것 같다. 그리고 내부적으로 util.inspect 라이브러리를 사용한다고 들었다.
그렇다고 매번 log.error('%o', ...)를 사용하기에는 좀 귀찮고, 그렇다고 log.error만을 출력하자니 잃어버리는 에러의 정보가 아까웠다.
일단은 log.error('%o')로 합의봐야겠다.
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
아무래도 블로그글과 스택오버플로가 말한 대로 아직까지는 NodeJs.ErrnoException을 사용해야 할 것 같다.
if (err instanceof Error && 'code' in err && err.code === 'ENOENT') {
이 너저분한 코드를 이제 고칠 수 있다!
이런 식으로 type predicate을 설정해 준 뒤,
isNodeError를 조건문 안에 적용해주면 이런 식으로 node 에러들의 자동완성이 뜬다!
기나긴 여정이었다...
Error -> CustomNodeError -> CustomHttpError 를 상속하다가 문득 생각했다. 왜 static abstract 은 없는걸까?
윈스턴은 이렇게 알록달록하게 출력하는 방법들이 있다.
To be continued...