DevInsight

개발하며 겪은 판단을 적어요

Tools
조회 3약 4분 읽기

2,731개의 타입 에러와 마주했던 한 주, strict 옵션을 끄고 켜는 사이의 현실

TypeScript strict 계열 옵션을 한 번에 켜면 수천 개의 타입 에러가 쏟아져 작업이 멈추기 십상이다. 실제 프로젝트 사례를 바탕으로 옵션별 변경 난이도와 영향 범위를 비교해 어떤 순서로 켜는 것이 안전한지, 어디까지 자동으로 고치고 어디를 수동으로 봐야 하는지 단계적 로드맵으로 정리했다.

DevInsight에서 발행해요

#TypeScript#strict 모드#타입 안정성#레거시 코드#리팩토링#tsconfig#strictNullChecks#noImplicitAny#코드 품질#마이그레이션

TSConfig의 strict: true를 처음 켜는 순간의 감각은 단순하다. 그때까지 잘 돌아가던 프로젝트가 빨간 줄로 뒤덮이고, 컴파일이 멈춘다. 2,731개라는 숫자가 꼭 극단적인 사례는 아니다. 몇 년 동안 성장해온 서비스라면 이만한 규모의 타입 에러는 오히려 평범하다.

문제는 개수가 아니라 그 뒤에 이어진 질문이다. 이걸 전부 손으로 고치자니 몇 주가 걸릴 것 같고, 무작정 끄자니 다시 느슨한 상태로 되돌아간다. 그래서 현실적인 선택은 "단계적으로 켜는 것"이고, 단계를 정하려면 먼저 각 옵션이 실제로 얼마나 큰 폭의 코드를 건드리는지 알아야 한다.

숫자로 보이는 것과 실제로 바꿔야 하는 것

strict는 단일 스위치지만 사실 다섯 개의 다른 옵션 묶음이다. noImplicitAny, strictNullChecks, strictFunctionTypes, noImplicitThis, strictPropertyInitialization 같은 것들이 함께 켜진다. 한 번에 전부 켜서 생기는 2,731개 에러를 보고 있으면 구분이 안 되지만, 실제론 세 종류로 나뉜다.

첫째는 타입 추론만으로 감당할 수 있는 것. 둘째는 API 응답이나 저장소에서 온 데이터처럼 구조적으로 any가 침투해 들어온 것이고, 셋째는 로직 자체가 null을 무시하고 짜여져 있어서 손을 대야 하는 것이다.

이 셋은 고치는 시간이 아예 다르다. 이걸 미리 구분하지 않으면 단계 계획이 세워지지 않는다.

one by one, 그러니까 옵션을 하나씩

제일 안전한 시작점은 noImplicitAny다. 여기서 쏟아지는 에러는 대부분 특정 함수 인자나 변수가 명시적인 타입 없이 돌아다니는 경우인데, 해결책이 비교적 기계적이다. 빈 함수에 매개변수 타입을 붙여주고, 인터페이스를 도입하면 끝나는 경우가 많다.

여기에 습관 하나를 미리 정해두면 좋다. 그때그때 추론되는 타입에 기대지 말고, 되도록 명시적으로 적는 것이다. const data = await fetchData()에서 await의 반환을 any로 받기보다 인터페이스로 받는 식으로. 이 작업은 에러 개수를 줄이는 것 이상으로 다음 단계를 쉽게 만든다.

가장 큰 난관은 그다음에 온다. strictNullChecks다.

strictNullChecks가 진짜 판가름한다

이 옵션은 에러 개수로는 한눈에 판단이 안 된다. 숫자가 적을 수도 있다. 문제는 "에러가 없는 코드"가 아니라 "에러가 나지 않는 걸로 보이던 코드"가 드러난다는 점에 있다.

예를 들어 user.address.city 같은 체이닝 접근이 전부 대상이 된다. useraddess가 옵셔널일 가능성은 JS에선 에러를 내지 않지만, 실행 시점엔 undefined 접근으로 터진다. 이건 단순히 타입 주석을 붙이는 것으로 끝나지 않는다. 데이터 흐름 자체를 다시 봐야 한다. null이 실제로 들어올 수 있는 경로인지, 아니면 호출부에서 필터링 됐어야 하는지.

여기서 자동 수정 도구가 한계를 보인다. TS의 자동 fix는 에러를 "치우는" 방향으로 동작하는데, 이건 오류가 사라지는 것과 고쳐지는 것은 다른 일이다. 와일드하게 타입을 열어주는 코드 코딩 스타일로 고쳐주면 오류는 안 나겠지만, 안전성 목표와는 거리가 멀다.

그래서 이 옵션은 반쯤 자동, 반쯤 수동으로 다뤄야 한다. 자동으로 후보를 뽑고, 사람이 해당 도메인 로직을 볼 수 있는 만큼만 고친다. 여기서 시간을 아끼려면 의존성을 줄여야 한다. 타입이 곳곳에 퍼져 있는데 여기저기서 sync를 끊을 수는 없다.

순서를 거꾸로 뒤집는 실수

옵션을 켜는 순서에서 흔한 실수는 "쉬운 것부터"가 아니라 "적은 것부터" 해치우는 것이다. 에러 수가 적다고 해서 쉬운 게 아니다. strictPropertyInitialization은 에러가 적은 편인데, 이걸 먼저 켜면 문제가 생긴다. 이 옵션은 생성자에서 초기화되지 않은 속성을 잡아내는데, 클래스가 이미 널 병합이나 접근자로 우회하고 있었다면 이걸 켰을 때 오히려 더 많은 것을 건드리게 된다.

오히려 공격적인 순서는 이렇다. 먼저 noImplicitAnystrict의 일부를 켜고, 그다음 strictNullChecks, 마지막으로 나머지를 켠다.

strictFunctionTypes는 특이하게 동작한다. 함수의 반환형은 공변적이지만 매개변수는 반공변적이라는 것, 일반 TS 개발자에게 낯선 개념이라 이 옵션 하나로 의외로 많은 곳에서 함수 시그니처가 깨진다. 콜백을 인자로 받는 패턴이 많은 코드베이스라면 이 에러가 몰려 나온다. 여기서는 수동 판단이 필요한 경우가 많다. 콜백의 매개변수를 넓히면 컴파일은 되지만 실행 시점의 계약이 어긋날 수 있기 때문이다. 이 옵션은 "남겨뒀다가 마지막에 처리하라"가 거의 정답에 가깝다.

기술적인 파이프라인, 그리고 절대 하면 안 되는 것

숫자로 보면 작아 보이는 데도 실제로는 난관이 숨어 있다. 딱 하나, 이 프로세스에서 절대 지켜야 할 원칙이 있다. strict: true를 켠 상태에서 lint를 돌리지 않는 것이다.

no-unused-vars@typescript-eslint/no-explicit-any 같은 규칙은 strict를 켜기 전에 이미 있을 가능성이 높은데, 이 몹들이 strict 옵션을 켠 직후의 코드에서 터져 나오는 건 전혀 다른 종류의 에러다. 이걸 동시에 끄고 켜는 날은 통제가 무너진다.

반대로 활용할 수 있는 건 // @ts-nocheck 와 함께 진행하는 방식이다. 한 파일이나 모듈에 일시적으로 이 주석을 달아두고, strict를 전체로 켠 다음 점진적으로 오류가 줄어드는 파일만 주석을 풀어나가는 방식이 실무에서 가장 잘 동작한다.

문제는 이 주석이 쌓이면 무의미해지는 것. @ts-nocheck 파일이 몇 개 없고 오래 방치되면, 그것들은 사실상 strict 밖의 코드가 된 것과 같다. 그래서 추적해야 한다. CI에서 주석 개수를 세는 스크립트 하나쯤은 만들어두는 게 좋다.

본격적인 실행, 그리고 측정

여기까지 왔으면 실제 적용 순간을 정한다. 첫째 날에 strictNullChecks까지 켜고, 남은 에러 리스트를 뽑는다. 그다음 에러를 리팩토링 추상화 단위로 묶는다. 이틀에 걸쳐 티켓 단위로 처리하고, 하루 처리가 끝나면 저장소의 타입 안정성 그래프를 그려본다. 에러 수가 감소하는 곡선이 아니라, 타입 오류가 실제로 배포에 미치는 영향의 범위가 줄어드는지 보는 것이다.

이 측정은 도구가 해주지 않는다. Lint 커멘드에서 나온 숫자를 신뢰하지 말고, 배포 후 릴리즈 노트에서 실제로 발생한 타입 관련 버그의 개수를 세다 보면 이 작업이 실질적으로 가치를 만들었는지 알 수 있다.

만약 이게 처음 2~3주 안에 배포에 아무런 변화를 만들지 않는다면, 그 프로젝트는 strict를 켜는 것으로 타입 안정성을 얻을 수 있는 상태가 아닐 수 있다. 그 경우 오히려 신규 파일에만 strict를 강제하는 전략, 신규 코드에 대한 사전 검사(lint, docgen, CI gate)가 더 나은 선택일 수 있다. 이 기준을 명확히 두면 실패를 실패로 안 끝나게 된다.

마지막으로 조언 하나. 이 과정에서 만난 에러가 2,731개라고 해서 그것만 고치면 끝인 게 아니다. 그 수치는 어디까지나 "코드 이곳저곳에 타입이 어디론가 흘러다니고 있다"는 신호다. 이 로드맵을 다 밟고 나서도 이 숫자가 나오는 원인, 즉 any를 주입하는 실질적인 통로가 남아 있지 않은지 다시 확인하라. 그 통로가 바로 다음 단계가 필요해지는 지점이다.

댓글

댓글을 읽어오는 중입니다.

같이 읽으면 좋은 글

방금 읽은 주제와 이어지는 글을 골랐습니다.

Tools 전체 보기
Tools

any 지옥에서 strict로 승급한 어느 프로젝트의 사투기

느슨하게 태어난 TypeScript 프로젝트에서 strict를 하루아침에 켜면 수백 개의 에러가 쏟아진다. 이 글은 신규 기능 개발과 버그 수정 틈새에서 strict 계열 옵션을 하나씩 켠 실제 진행 기록으로, 옵션별 난이도와 효과를 따져 최적의 적용 순서를 정리했다. any 소탕 순서부터 strictNullChecks, noImplicitAny까지 실무에서 겪은 장애와 회귀를 함께 담았다.

#TypeScript#strict#strictNullChecks#noImplicitAny
Tools

ESLint Flat Config 마이그레이션 실패 일지와 살아남는 체크리스트

ESLint 9의 flat config로 넘어가면서 extends가 사라지고, 플러그인 호환성 문제, VS Code ESLint 확장과의 설정 불일치, 글로벌 변수 선언 방식 변화 등 현장에서 마주치는 장애물을 해결 순서대로 정리한다. 삽질을 줄이는 실전 체크리스트. 2025년 4월, ESLint 9가 정식 릴리스되면서 파일은 deprecated 경고를 넘어 아예 무시되기 시작했다.

#ESLint#flat config#eslintrc#마이그레이션
Tools

eslintrc를 버리고 flat config로 갈아탈 때 조용히 무너지는 지점들

eslintrc에서 flat config로 전환할 때 가장 많이 무너지는 지점은 규칙이 아니라 플러그인 호환성과 설정 병합 순서다. 문자열 기반 extends가 사라지고 객체 배열로 바뀌면서 플러그인 등록 방식, ignores 처리, 에디터 연동이 모두 달라진다. 이 글은 실제 마이그레이션에서 조용히 실패하는 지점을 짚고, 무엇을 먼저 옮기고 무엇을 버려야 하는지 판단 기준을 제시한다.

#ESLint#flat config#마이그레이션#린트

이전 글

작은 서비스도 버틸 수 있는가, 폴링 한계와 큐 도입 시점의 판단 기준

다음 글

use client 한 줄로 갈리는 서버와 클라이언트, 번들 크기부터 상태 관리까지 흔들린다

DevInsight Digest

새 글이 쌓이면, 피드에서 바로 이어 읽으세요.

과장된 알림 대신 발행한 글 전체를 RSS로 제공합니다.

RSS 구독