← 모든 글

type-safe 를 어디까지 강제할 것인가

strict: true로 847개 오류가 터진 결제 모듈 실화. HEDVION이 타입 강제 범위를 4단계로 나누고 외부 API 경계에 Zod를 전면 도입한 1년간의 판단 기록.

결제·정산 코드에서 타입 오류는 돈 문제다

TypeScript를 쓰는 팀이라면 strict: true를 켜는 순간의 충격을 기억할 것이다. 코드베이스 전체가 빨간 줄로 뒤덮이고, 팀원 모두 "이걸 다 고쳐야 해?"라는 표정을 짓는다. HEDVION도 예외가 아니었다. 하지만 우리가 타입 안전성 문제를 바라보는 시각은 "깔끔한 코드"가 아니다. 결제, 정산, 자동화를 직접 운영하는 팀에게 타입 오류는 잘못 처리된 금액, 누락된 정산 레코드, 자동화 파이프라인 중단으로 이어지는 실물 리스크다.

결제 파이프라인에서 가장 흔한 시나리오를 들어보자. PG사 웹훅 핸들러에서 amount 필드를 number로 타입 선언해 놓았어도, 실제 수신된 JSON이 "1500" (문자열)이면 계산 로직은 조용히 오작동한다. JavaScript의 + 연산자는 타입이 섞이면 문자열 연결로 동작하고, 이 결과가 DB에 그대로 기록되면 정산 레코드에 이상한 값이 남는다. 타입 선언만 믿고 런타임 검증을 생략했을 때 발생하는 "거짓 안전감"의 전형이다. 이런 버그는 개발 환경에서 재현하기 어렵고, 특정 PG사의 엣지케이스 응답에서만 발현되기 때문에 스테이징을 통과한 채 프로덕션에서 처음 터지는 경우가 많다.

strict: true가 꺼내놓은 847개의 빨간 줄

1년 전 우리가 레거시 결제 모듈에 처음으로 strict: true를 활성화했을 때 컴파일 오류 847개가 터졌다. 단일 모듈 기준이다. "대부분은 의미 없는 노이즈겠지"라고 예상했지만, 실제로 분류해 보니 달랐다. 847개 중 약 210개(약 25%)는 실제로 런타임에 undefined 역참조나 암묵적 any 전파로 이어질 수 있는 진짜 위험 코드였다. 나머지 637개는 타입 미선언, 미사용 변수, 과도한 any 등 정리가 필요하지만 즉각적 버그는 아닌 것들이었다.

비용-편익을 계산했다. 결제 실패 1건당 고객 CS 응대, 재처리, 정산 정정 작업이 평균 40~80분 소요된다. 반면 우리 팀 내에서 실제로 측정한 컴파일 오류 1개 수정 시간은 평균 약 8분이다. 잠재적 위험 코드 10개만 컴파일 단계에서 사전에 막아도 이미 본전을 넘는다. 637개의 "정리형" 오류는 기능 개발 사이클과 분리해 별도 리팩터링 PR로 3주에 걸쳐 소화했다. 한 번에 전부 고치려는 시도는 반드시 실패한다 — 팀이 번아웃되거나 PR이 너무 커서 리뷰가 무력화된다. 이것도 이 과정에서 배운 교훈이다.

거짓 안전감이 가장 위험한 구간: 외부 API 응답

결제·정산 도메인에서 타입 안전성의 허점이 가장 크게 드러나는 곳은 외부 시스템과의 경계다. PG사 API, 은행 정산 파일, 환율 데이터 피드, 카드 네트워크 이벤트 — 이 모든 것은 우리가 타입을 통제할 수 없는 영역이다. interface PGWebhookPayload { amount: number; orderId: string; } 라고 선언해도 실제 JSON이 그것을 따른다는 보장은 없다. TypeScript의 타입은 컴파일 타임에만 존재하고 런타임에는 완전히 사라진다. 이 사실을 처음엔 쉽게 잊는다.

우리 팀은 이 구간에 Zod를 전면 도입했다. 핵심 패턴은 단순하다: 외부 입력이 시스템에 진입하는 모든 지점(웹훅 핸들러, 배치 파일 파서, 외부 REST 응답 처리)에 Zod 스키마를 배치하고, TypeScript 타입은 z.infer<typeof schema>로만 생성한다. 타입 정의와 런타임 검증이 분리되는 순간을 구조적으로 차단하는 방식이다. 도입 전후 차이는 명확했다. 특정 PG사가 간헐적으로 orderId 필드를 누락한 채 웹훅을 보내는 케이스가 있었는데, Zod 도입 전에는 이것이 프로세스 중간에 undefined로 조용히 전파되어 정산 레코드에 null 값이 기록됐다. 도입 후에는 파싱 단계에서 즉시 오류가 발생하고 해당 이벤트가 dead-letter queue로 격리된다. 같은 문제지만 디버깅 시간이 수 시간에서 5분 내로 줄었다.

타입 강제의 역효과 — 프로토타입과 테스트 픽스처

타입을 강하게 쓰는 것이 항상 옳다는 주장은 실무에서 버티지 못한다. 우리가 의식적으로 완화하는 구간이 두 곳 있다.

첫째, 새 정산 정책을 검토하는 프로토타입 단계다. 새로운 수수료 구조나 분배 로직을 실험할 때, 데이터 모델이 확정되지 않은 상태에서 엄격한 타입을 붙이면 로직보다 타입을 고치는 데 더 많은 시간을 쓰게 된다. 이 단계에서는 핵심 계산 로직에만 집중하고 나머지는 unknown 또는 // TODO: type this 주석으로 표시해 둔다. 단, 이 코드가 프로덕션으로 병합되는 시점에는 타입 강화 작업이 반드시 같은 PR에 포함되어야 한다는 규칙을 명문화했다. "나중에 타입 붙이자"는 실제로는 영원히 미뤄지기 때문이다.

둘째, 테스트 픽스처다. 결제 트랜잭션 객체는 필드가 40개를 넘는 경우도 있다. 특정 필드 2개만 검증하는 테스트에서 나머지 38개 필드를 모두 채워야 한다면, 테스트 코드가 불필요하게 길어지고 유지보수 부담이 커진다. Partial<Transaction> 또는 테스트 전용 빌더 패턴을 쓰는 것이 현실적이다. 다만 이 완화는 테스트 코드에만 적용하고, 프로덕션 코드에서 Partial을 남용하지 않도록 ESLint 커스텀 룰로 경계를 긋는다.

HEDVION이 실제로 쓰는 4단계 기준

1년간의 시행착오 끝에 지금 우리 팀이 따르는 기준을 공개한다. PR 리뷰와 온보딩에서 실제로 참조하는 내용이다.

프로덕션 비즈니스 로직: strict: true, noImplicitAny: true 강제. 타입 단언(as)은 원칙적으로 금지하며, 불가피한 경우 해당 줄에 왜 안전한지 주석으로 설명해야 한다. as unknown as X 패턴은 PR 리뷰에서 반려 사유가 된다.

외부 입력 처리 레이어: 모든 타입은 Zod 스키마에서 z.infer로 추론. 타입 단언으로 외부 데이터를 억지로 맞추는 코드는 작성 자체를 금지한다. 파싱 실패는 오류로 명시적으로 처리하고 dead-letter queue나 알람으로 연결한다.

내부 이벤트·메시지 큐: 프로듀서와 컨슈머가 동일 모노레포 내에 있으면 공유 타입 패키지를 통해 타입을 공유한다. 외부 서비스가 소비하는 이벤트는 JSON Schema로 문서화하고, 컨슈머 측에 Zod 검증을 추가한다.

테스트 코드: strict는 유지하되 픽스처에만 Partial<T> 허용. 팀 공통 빌더 유틸을 제공해 반복 작성을 줄인다.

지금 바로 써먹을 수 있는 실행 지침

이론은 충분하다. 결제·정산·자동화를 운영하는 팀이 내일 당장 적용할 수 있는 것들만 추렸다.

① strict: true 전환은 모듈 단위로, 오류 분류부터 시작하라. 전체 코드베이스에 한 번에 켜지 말고, 새로 만드는 파일과 모듈부터 적용한다. 기존 모듈은 오류가 뜨면 "즉각 위험"(undefined 역참조, any 전파)과 "정리 필요"(미사용 변수, 타입 미선언)로 분류해 전자만 먼저 처리한다. 후자는 기능 사이클과 분리된 별도 PR로 소화한다.

② 외부 API 응답에 타입 단언이 있으면 지금 당장 grep 하라. response.data as PaymentResult 패턴을 코드베이스에서 찾아라. 이 패턴이 결제 금액, 주문 ID, 정산 상태를 다루는 경로에 존재한다면 다음 스프린트 안에 Zod 파서로 교체하는 것을 강력히 권장한다.

③ PR 리뷰 체크리스트에 타입 단언 항목을 추가하라. "이 PR에 as 또는 any가 새로 추가됐는가? 이유가 있는가?" 한 줄만 추가해도 팀의 의식이 달라진다. 자동화하려면 no-explicit-any ESLint 룰과 @typescript-eslint/no-unsafe-* 룰셋을 CI에 넣으면 된다.

④ 결제 객체처럼 필드가 많은 타입은 팀 공통 빌더 팩토리를 만들어라. createMockTransaction({ amount: 1000 }) 형태의 팩토리 함수 하나로 팀 전체가 재사용하면 테스트 코드가 절반으로 줄고, 타입 변경 시 픽스처 수정 부담도 한 곳에 모인다.

타입 안전성은 목표가 아니라 도구다. 도구도 어디에 어떻게 쓸지를 의식적으로 정하지 않으면 효과가 반감된다. 규칙을 명문화하고, 외부 경계에는 런타임 검증을 반드시 병행하고, 예외를 허용하되 이유를 기록하는 것 — 이 세 가지가 HEDVION이 1년간 부딪히고 정착시킨 실천이다.

— by mings

* 위 링크는 인프런 affiliate 활동의 일환이며, 일정액의 수수료를 제공받을 수 있습니다.

📚 추천 강의
한 입 크기로 잘라먹는 바이브코딩 (with Claude Code)
Claude Code로 바이브코딩, 개발자라면 꼭 들어야 할 필수 강의
강의 보러가기 →

* 위 추천 링크는 쿠팡파트너스 활동의 일환이며, 일정액의 수수료를 제공받을 수 있습니다.