타입 가드를 어느 정도 쓰다 보면, 비슷한 코드가 계속 늘어나는 시점이 옵니다. 외부에서 들어오는 값을 하나하나 확인하고, 필드 타입을 검사하고, 조건이 조금만 달라져도 또 다른 가드를 만듭니다.
이쯤 되면 자연스럽게 스키마 검증 도구를 보게 됩니다. zod, io-ts 같은 라이브러리가 대표적입니다. 처음에는 “타입 가드 대신 쓰면 편하겠다” 정도의 기대에서 시작합니다.
막상 붙이고 나면, 편해지는 부분도 있고, 생각보다 불편해지는 부분도 같이 따라옵니다. 이 글에서는 스키마 검증을 도입하면서 코드가 어떻게 바뀌는지, 그리고 어떤 변화가 오래 남는지 위주로 정리해 봅니다.
타입 가드만 쓰던 시기의 코드
스키마 검증을 붙이기 전에는 보통 이런 형태로 시작합니다.
type LoginBody = {
email: string;
password: string;
};
function isLoginBody(value: unknown): value is LoginBody {
if (typeof value !== 'object' || value === null) return false;
const v = value as any;
return typeof v.email === 'string' && typeof v.password === 'string';
}
이 정도만 있어도 웬만한 실수는 걸러집니다. 문제는 요구사항이 조금만 늘어나도 바로 드러납니다.
- 이메일 형식을 확인해야 하는 경우
- 비밀번호 길이 제한이 생긴 경우
- 선택 필드가 하나씩 추가되는 경우
이 시점부터 가드는 길어지고, 어디까지 검사해야 하는지 애매해집니다.
스키마 검증을 붙였을 때 가장 먼저 바뀌는 점
zod 같은 스키마 라이브러리를 붙이면, 검사 로직이 한 덩어리로 모입니다.
import { z } from 'zod';
const LoginSchema = z.object({
email: z.string().email(),
password: z.string().min(8),
});
type LoginBody = z.infer<typeof LoginSchema>;
타입 정의와 검사 규칙이 같이 움직이기 시작합니다. 이 자체는 분명 편합니다. 다만 여기서부터 코드 성격이 조금 달라집니다.
검증 실패가 값이 아니라 사건이 된다
타입 가드는 보통 boolean을 반환합니다. 통과하면 다음 단계로 넘어가고, 실패하면 바로 에러를 던지거나 종료합니다.
스키마 검증을 쓰기 시작하면, 실패는 단순한 false가 아니라 “어떤 필드가 왜 실패했는지”라는 정보가 됩니다.
const result = LoginSchema.safeParse(body);
if (!result.success) {
console.log(result.error.issues);
}
이 정보는 디버깅이나 로그에서는 꽤 도움이 됩니다. 문제는 이 정보를 어디까지 흘려보낼지 결정해야 한다는 점입니다.
에러 응답 구조가 바뀌기 시작한다
처음에는 대충 이런 식으로 끝내기 쉽습니다.
throw new BadRequestException('invalid request');
하지만 스키마 에러를 한 번 보기 시작하면,
- 필드 단위 에러를 내려줄지
- 하나의 메시지로 감출지
- 로그에만 남길지
같은 선택을 계속 하게 됩니다. 여기서부터는 타입의 문제가 아니라 API 설계와 운영 정책 쪽으로 넘어갑니다.
타입 가드와 스키마 검증의 경계가 흐려진다
스키마를 붙이고 나면, 기존에 있던 타입 가드들이 애매해집니다.
어떤 코드는 스키마로 검사하고, 어떤 코드는 여전히 is 함수를 씁니다. 둘을 섞다 보면 기준이 흐려지기 쉽습니다.
보통은 이런 식으로 역할이 나뉩니다.
- 외부 입력: 스키마 검증
- 내부 흐름: 타입 가드 또는 타입 자체 신뢰
이 경계를 명확히 두지 않으면, 같은 값을 두 번 검사하거나, 반대로 아무 검사도 안 하고 넘기는 경우가 생깁니다.
검증 로직이 점점 비즈니스에 가까워질 때
스키마는 처음에는 형태만 검사합니다. 하지만 시간이 지나면 이런 요구가 들어옵니다.
- 이 값은 다른 필드와 같이 봐야 한다
- 상태에 따라 허용되는 값이 달라진다
- DB 조회 결과에 따라 검증이 달라진다
이 지점부터는 스키마 안에 어디까지 넣을지 고민하게 됩니다. 전부 넣기 시작하면, 스키마가 비즈니스 로직을 품기 시작합니다.
경험상 이 단계까지 가면, 스키마는 “검증 도구”라기보다 또 하나의 레이어가 됩니다. 이 레이어를 유지할 준비가 되어 있는지가 중요해집니다.
운영에서 체감되는 변화
스키마 검증을 붙이고 나서 가장 크게 달라지는 건, 문제가 생겼을 때 원인을 찾는 속도입니다.
어디서 잘못된 값이 들어왔는지, 형태 문제인지, 값 문제인지가 로그에서 바로 보입니다. 대신 코드 양은 분명히 늘어납니다.
편해졌다고 느끼는 시점과 귀찮아졌다고 느끼는 시점이 같이 옵니다. 그래서 팀마다 이 선택에 대한 인식 차이가 생기기도 합니다.
스키마 검증을 붙인다는 건, 단순히 타입 가드를 대체하는 선택이 아닙니다. 검증 실패를 어떻게 다루고, 그 결과를 어디까지 노출할지까지 함께 결정하게 됩니다.
코드가 단순해지는 구간도 있고, 오히려 복잡해지는 구간도 생깁니다. 이 변화가 감당 가능한지, 그리고 팀이 그 구조를 유지할 준비가 되어 있는지가 결국 가장 중요한 판단 기준이 됩니다.
'개발 > Typescript' 카테고리의 다른 글
| [TYPESCRIPT] DTO를 믿기 시작했을 때 놓치기 쉬운 지점들 (0) | 2026.02.09 |
|---|---|
| [TYPESCRIPT] 에러 타입을 나누기 시작하면 코드에서 달라지는 것들 (0) | 2026.02.08 |
| [TYPESCRIPT] 타입 가드를 쓰다 보면 결국 마주치게 되는 문제들 (0) | 2026.02.06 |
| [TYPESCRIPT] Decorator를 쓰기 전에 먼저 고민하게 되는 것들 (0) | 2026.02.05 |
| [TYPESCRIPT] Variadic Tuple Types를 실무에서 쓰는 경우 (0) | 2026.02.04 |
