TypeScript(타입스크립트) 개발자들이 흔히 겪는 문제 중 하나는 `if (user.email)`처럼 이미 확인한 조건을 코드 곳곳에서 반복적으로 검증하는 것입니다. 이는 타입 시스템이 검증 사실을 기억하지 못해 발생하며, 불필요한 코드 중복과 잠재적 버그로 이어질 수 있습니다. 이러한 문제를 해결하기 위해 '검증 대신 파싱(Parse, don't validate)'이라는 원칙이 TypeScript 환경에 맞춰 재조명되고 있습니다.
이 원칙의 핵심은 단순히 값의 유효성을 `boolean`으로 확인하는 '검증기' 대신, 원시 입력을 받아 더 정밀한 타입(예: `string` 대신 `EmailAddress`)으로 변환하거나 실패 정보를 명확히 반환하는 '파서'를 사용하는 것입니다. 예를 들어, `sendWelcome(user: ValidUser)`와 같이 파싱된 유효한 값만 받을 수 있는 함수 시그니처를 사용하면, 함수 내부에서 별도의 재검증 로직이 필요 없어집니다. TypeScript의 구조적 타입 시스템(structural type system) 한계로 인해 `string`과 `Email`을 명확히 구분하기 어렵지만, `unique symbol` 기반의 브랜디드 타입(branded type)을 활용해 컴파일 시점에 다른 타입으로 취급하도록 흉내 낼 수 있습니다. 또한, `Parsed<T>`와 같은 구별된 유니언(discriminated union)을 사용해 파싱의 성공과 실패를 타입 서명에 명시적으로 드러내어 잠재적 오류를 미리 방지합니다.
이러한 접근 방식은 외부에서 들어오는 `unknown` 타입의 원시 입력과 애플리케이션 내부에서 신뢰할 수 있는 도메인 타입을 명확히 분리하는 데 도움을 줍니다. `UnvalidatedUser`와 `ValidUser`를 구분하고, `parseUser(raw: unknown): Parsed<ValidUser>`와 같은 파서를 통해 단계적으로 타입을 좁혀나가면, 코드의 안정성과 가독성이 크게 향상됩니다. `Zod`, `io-ts`, `valibot`과 같은 라이브러리들은 이러한 파서와 TypeScript 타입을 스키마 정의 하나로 함께 생성할 수 있게 하여 개발자의 수고를 덜어줍니다. 이 방식은 F#(F#)나 Elm(엘름) 같은 언어에 비해 다소 장황할 수 있지만, TypeScript 코드베이스의 견고함을 높이는 효과적인 전략입니다. 결국, '타입 시스템이 증거를 들고 있게 하고, 사람의 기억에 맡기지 말라'는 작은 원칙이 반복적인 방어적 코드를 줄이고 더 신뢰할 수 있는 소프트웨어를 만드는 데 기여합니다.