TypeScript로 개발을 진행하던 중 발생한 에러가 생각지도 못한 원인에서 비롯된 경험을 이곳에 남겨봅니다. 나중에 또 실수할 저를 위해.
웹 개발 프로젝트를 진행하면서 REST API를 개발중이었습니다. 대략적인 구현은 거의 끝난 상태였지만 아직 테스트 코드가 작성중인 상태라 원하는대로 동작하는지는 확인해보질 못했었습니다. 그래서 테스트 코드는 시간 날때 따로 작성하는 것으로 하고, 프론트 개발을 하면서 API 테스트로 함께 하면 되겠다 싶어 하나씩 해보는 중에 문제를 발견하게 됩니다.
먼저 백엔드의 대략적인 아키텍처를 설명드리자면 다음과 같습니다. ()는 해당 단계를 구현한 클래스명입니다.
또한 각 과정에서 발생하는 DB 연산과 에러 처리를 위한 객체를 각 클래스 생성자를 통해 연쇄적으로 넘겨주는 형태로 설계했습니다.
문제는 사용자 로그인을 처리하는 함수에서 처음 발견되었습니다.
다음과 같은 로그인 요청에
POST /api/v1/auth/login
로그인 프로세스를 처리하고 결과를 응답으로 받을 수 있어야 하는데 다음과 같은 예외가 발생합니다.
(node:14676) UnhandledPromiseRejectionWarning: Unhandled promise rejection (rejection id: 2): TypeError: Cannot read property 'db' of undefined
db
는 요청을 처리하는 클래스에서 사용하도록 생성자를 통해 넘겨준 DB 연산을 추상화한 객체입니다. 대략 다음과 같은 형태로 구현돼있다고 보시면 되겠습니다.
// 사용자 인증과 관련된 API
public AuthAPI {
public constructor(
private db: IDatabase,
private eh: IErrorHandler) { /* ... */ }
private loginUser(req: express.Request, res: express.Response) {
// ...
this.db.findUserByID(); // DB 연산
}
}
실질적으로 요청이 처리되는 부분은 loginUser
메서드인데, 이곳에서 db
의 함수를 호출하는 과정에서 예외가 발생한 것으로 보였습니다.
예외 메시지를 해석하면 db
프로퍼티가 있어야 할 객체에 없다는, 즉 this
객체가 db
필드를 가지고 있지 않다는 것으로 해석이 되었습니다.
저는 여기서 가능한 원인을 크게 다음과 같이 두 가지로 생각했습니다.
AuthAPI
의 생성자의 db
파라미터에 undefined가 전달됐다.loginUser
에서 참조하는 this
가 어떤 이유로 인해 undefined
가 되어 있다.하지만 TypeScript의 강점 중 하나가 함수에 전달되는 타입을 일정 수준 강제함으로써 원치 않는 값이 전달되는 상황을 방지한다는 것이고, 위의 코드 처럼 undefined
나 null
이 아닌 IDatabase
객체만 넘길 수 있도록 작성된 코드에서 다른 타입이 전달되는 코드가 있었다면 분명 컴파일 과정에서 찾을 수 있을 것으로 생각했습니다.
결국 가능한 원인은 두 번째 뿐인 것으로 결론 짓고 구글링하다가 다음과 같은 글을 발견했습니다. 원문
특히, ‘Typical Symptoms and Risk Factors’ 항목의 ‘The value this points undefined instead of the class instance (strict mode)‘가 이 문제를 정확히 표현하고 있다고 생각했습니다.
해당 문서에서 제안한 방법은 3가지 입니다.
Instance Function 사용하기
private doSomething = () => { console.log("Hello!"); }
Fat Arrow Function 사용하기
doOtherThing(() => { console.log("Hello!"); });
Bind 함수 사용하기
const doSome = new Something();
doOtherThing(doSome.doSomthing.bind(doSome));
각 방법마다 장단점이 있는데, 이번 문제에서는 1번 방법을 적용해봤습니다.
다음과 같이 코드를 수정한 결과 정상적으로 동작하는 것을 확인하였습니다.
private loginUser = (req: express.Request, res: expresss.response) => {
// ...
this.db.findUserById();
}
참고한 문서에 따르면, TypeScript에서 this
는 다음과 같이 정해진다고 합니다. (상위 항목부터 우선순위를 가짐)
function#bind
함수의 결과로 반환된 함수인 경우, this
는 bind
에 전달된 인자를 가리킵니다.x.doSomething()
과 같은 형태로 호출된 경우, 함수는 x
를 가리킵니다.this
는 undefined
입니다.this
는 전역 객체를 가리킵니다(브라우저에서는 window
).저의 경우에는 3번 항목이 원인이 된 것으로 보입니다. TypeScript의 strict 모드는 컴파일러 설정인 tsconfig.json
에서 strict
에 true
를 지정하는 것으로 활성화되는데, 이로 인해 this
가 undefined
가 된 것이었습니다.
함수를 정의하는 방법에 큰 차이가 없어서 아무 생각 없이 사용한 문법이 수 시간의 삽질을 불러오게 될 줄은 몰랐습니다. 물론 하나 배웠다고 생각하니 마음은 뿌듯하지만, 원인을 찾는 데에 시간을 많이 쓰지 않았나 싶습니다. 역시 이런 부분은 경험으로 깨달아야 할 것 같습니다.