TypeScript strict mode 켜고 벌어진 일들
레거시 프로젝트에서 strict mode를 켰을 때 쏟아진 에러와 그 해결 과정.
1,847개의 에러가 떴다
어느 금요일, tsconfig.json에서 "strict": true를 켰다. 빌드를 돌렸다. 1,847개.
금요일 오후에 할 일이 아니었다. 바로 되돌렸다.
2년 된 프로젝트였다. TypeScript를 쓰고는 있었지만 strict: false였다. any가 곳곳에 박혀 있었고, null 체크는 optional chaining으로 대충 넘겼다. 타입 에러가 나면 as any를 붙여서 해결했다.
솔직히 편했다. 빠르게 기능을 만들 수 있었고, 타입 때문에 막히는 일이 없었다. 근데 프로젝트가 커지면서 런타임 에러가 점점 늘었다. "Cannot read property of undefined"가 Sentry에서 하루 20건씩 찍혔다.
strictNullChecks부터 시작했다
1,847개를 한 번에 고치는 건 불가능했다. strict mode의 하위 옵션을 하나씩 켜기로 했다.
strictNullChecks부터. 이게 에러의 60%를 차지하고 있었다. 1,100개의 에러. 많지만 대부분 비슷한 패턴이었다. null일 수 있는 값을 체크 없이 사용하는 경우.
가장 흔한 패턴은 이거였다. const user = getUser(id); 다음에 바로 user.name을 접근하는 코드. getUser가 User | undefined를 반환하는데 undefined 체크를 안 하고 있었다.
고치면서 실제 버그를 발견했다
이걸 고치면서 실제 버그를 여러 개 발견했다. 존재하지 않는 사용자 ID로 조회했을 때 서버가 500 에러를 내는 곳이 있었다. strictNullChecks가 켜져 있었다면 애초에 컴파일이 안 됐을 코드다.
3주에 걸쳐 1,100개를 다 고쳤다. 하루 평균 60-70개. 기계적인 작업이 많아서 지루했지만, 고칠 때마다 코드가 안전해지는 느낌이 들었다.
(하루에 70개씩 고치면 꿈에서도 null 체크를 한다.)
noImplicitAny와의 전쟁
다음은 noImplicitAny. 450개의 에러. 이벤트 핸들러의 파라미터가 대부분이었다. onChange={(e) => ...}에서 e의 타입을 명시하지 않은 경우. React.ChangeEvent<HTMLInputElement> 같은 타입을 다 붙여야 했다.
유틸리티 함수들도 많았다. function formatData(data) 같은 함수에 data의 타입이 없었다. 이걸 고치면서 함수의 입출력이 명확해졌고, 잘못된 데이터를 넣는 실수를 방지할 수 있게 됐다.
진짜 어려운 건 서드파티 라이브러리였다
우리가 작성한 코드는 고치면 그만이었다. 문제는 타입 정의가 불완전한 서드파티 라이브러리. 특히 오래된 라이브러리 중 @types 패키지가 최신 버전을 반영하지 않는 경우가 있었다.
declare module로 타입을 직접 선언하거나, 어쩔 수 없이 // @ts-expect-error를 사용했다. as any보다는 @ts-expect-error가 낫다. 왜 무시하는지 주석으로 설명할 수 있고, 나중에 타입이 수정되면 불필요한 ignore를 알려주니까.
6주 후 Sentry를 열었다
strict mode 완전 적용에 6주가 걸렸다.
다음 달 Sentry 리포트. "Cannot read property of undefined" 에러가 하루 20건에서 3건으로 줄었다. 80% 이상 감소.
개발 속도도 역설적으로 빨라졌다. 코드를 수정할 때 타입 시스템이 영향 범위를 알려주니까, "이거 고치면 저기 깨지지 않을까?" 하는 불안감이 사라졌다.
strict는 프로젝트 시작할 때 켜야 한다
나중에 켜면 6주간의 고통이 기다리고 있다. tsconfig의 strict를 false로 시작하는 건 미래의 자신에게 6주치 숙제를 남기는 것과 같다.
지금 strict가 꺼져 있다면 하위 옵션 하나씩 켜보길. 한 번에 다 하려면 좌절하지만, 하나씩 하면 감당할 수 있다.