
API 응답이 바뀌었는데 프론트가 죽어버렸습니다 (Zod가 필요한 이유)
TypeScript만 믿고 있다가 런타임 에러로 앱이 터졌습니다. 컴파일 타임이 아닌 '런타임'에 데이터를 검증해야 하는 이유와 Zod 활용법.

TypeScript만 믿고 있다가 런타임 에러로 앱이 터졌습니다. 컴파일 타임이 아닌 '런타임'에 데이터를 검증해야 하는 이유와 Zod 활용법.
any를 쓰면 타입스크립트를 쓰는 의미가 없습니다. 제네릭(Generics)을 통해 유연하면서도 타입 안전성(Type Safety)을 모두 챙기는 방법을 정리합니다. infer, keyof 등 고급 기법 포함.

부모에서 전달한 props가 undefined로 나와서 앱이 크래시되는 문제 해결

TypeScript/JavaScript에서 절대 경로 import 설정이 안 될 때의 원인을 '지도와 택시 기사' 비유로 설명합니다. CJS vs ESM 역사적 배경과 모노레포 설정, 팀 컨벤션까지 총정리.

API 요청 상태를 관리할 때 불리언 플래그 여러 개를 쓰시나요? 'impossible state(불가능한 상태)'를 방지하고, if 문 도배를 없애는 Discriminated Unions 패턴.

금요일 오후 5시, 평화로운 퇴근 직전이었습니다. 갑자기 슬랙 알림이 울렸습니다. "사용자 목록 페이지가 안 켜져요. 흰 화면만 나와요." "어? 제가 로컬에서 돌렸을 땐 멀쩡한데요?"
부랴부랴 로그를 확인해 보니 이런 에러가 찍혀 있었습니다.
Uncaught TypeError: Cannot read properties of undefined (reading 'map')
코드 상으로는 문제가 없었습니다. TypeScript 컴파일러도 초록색(성공)이었고요.
원인을 파헤쳐 보니, 백엔드 개발자분이 API 응답 포맷을 살짝 바꾸셨더군요.
users: [] 배열이 와야 할 자리에, 데이터가 없으면 users: null을 보내도록 변경된 것이었습니다.
제 프론트엔드 코드는 users가 무조건 배열이라고 믿고(Type Inference) users.map()을 돌렸는데, 런타임에 null이 들어오니 폭발해버린(Map on null) 것이죠.
저는 "TypeScript를 쓰면 이런 타입 에러는 다 막아주는 거 아니었나?"라고 오해했습니다.
interface UserResponse { users: User[] }라고 타입을 정의해 뒀으니까, 당연히 배열이 올 거라고 믿었습니다.
하지만 깨달았습니다. TypeScript는 컴파일 타임(Compile Time)에만 존재합니다.
브라우저에서 코드가 실행되는 순간(Runtime), 타입스크립트는 전부 사라지고 순수 자바스크립트만 남습니다.
서버에서 실제로 무슨 데이터(null인지 undefined인지)가 날아오는지는 TypeScript가 알 방법이 없습니다.
그저 제가 정의한 인터페이스를 "믿어줄" 뿐이죠.
즉, 엔드포인트(API, DB, 사용자 입력)의 경계에서는 TypeScript가 무용지물이 됩니다.
이걸 "입국 심사대(Immigration Check)"에 비유하니 이해가 됐습니다.
우리는 "외부 데이터는 모두 오염되었다(Tainted)"고 가정해야 합니다. 무조건 검증하고, 검증된 데이터만 앱 내부로 들여보내야 합니다.
처음엔 if (data.users && Array.isArray(data.users)) 같은 방어 코드를 덕지덕지 발랐습니다.
하지만 필드가 100개라면? 코드가 너무 지저분해집니다.
그래서 Zod(Schema Validation Library)를 도입했습니다.
TypeScript 인터페이스 대신 Zod 스키마를 먼저 정의합니다. 이것이 우리의 '데이터 헌법'이 됩니다.
import { z } from "zod";
// 1. 스키마 정의 (Runtime Validation Logic)
const UserSchema = z.object({
id: z.number(),
name: z.string().min(2, "이름은 2글자 이상이어야 합니다"),
email: z.string().email(),
// 백엔드가 가끔 null을 보낸다고? nullable()로 처리!
role: z.enum(["admin", "user"]).nullable(),
});
// 2. 타입 추출 (Compile Time Type)
// interface User extends... 할 필요 없이 Zod가 알아서 타입을 만들어줍니다.
type User = z.infer<typeof UserSchema>;
이 User 타입은 TypeScript 컴파일러가 쓰고, UserSchema 객체는 자바스크립트 런타임이 씁니다.
일석이조(One Source of Truth)입니다.
API로부터 데이터를 받았을 때, 바로 쓰지 말고 Zod에게 검사를 맡깁니다.
/* 기존 방식 (위험) */
// const user: User = await response.json() as User; // 'as'는 거짓말쟁이
/* Zod 방식 (안전) */
const json = await response.json();
try {
// parse: 데이터가 스키마와 다르면 즉시 에러(Throw)를 던집니다.
const user = UserSchema.parse(json);
console.log(user.name); // 여기까지 왔다면 100% 안전한 데이터임이 보장됨.
} catch (error) {
// 스키마 불일치! 프론트가 죽는 대신, 우아하게 에러 처리
console.error("서버 형식이 변경되었습니다:", error);
toast.error("데이터 형식이 올바르지 않습니다.");
}
이제 서버가 email 필드를 빠뜨리거나 id를 문자열로 보내면, Zod가 즉시 "잠깐! id는 숫자여야 하는데 문자열이 왔어"라고 막아섭니다.
앱이 undefined 참조 에러로 흰 화면이 되는 대신, 우리가 제어 가능한 에러 상태가 됩니다.
에러를 throw 하는 게 부담스럽다면 safeParse를 씁니다.
const result = UserSchema.safeParse(json);
if (!result.success) {
// result.error에 상세한 에러 내용이 담겨있음
console.log(result.error.format());
return null;
}
// result.data는 검증된 User 타입
return result.data;
Zod를 쓰면서 얻은 의외의 수확은 "백엔드 API 문서가 거짓말을 해도 내가 알 수 있다"는 것입니다.
스웨거(Swagger) 문서에는 '필수(Required)'라고 되어 있는데, 실제로는 null이 오는 경우가 허다합니다.
Zod 없이 개발할 땐 "왜 안 되지?" 하고 제 코드를 의심하며 3시간을 디버깅했습니다. Zod를 쓴 뒤로는 3초 만에 알 수 있습니다.
ZodError: Expected string, received null at "address.city"
이 로그를 캡처해서 백엔드 개발자분께 보내드리면 됩니다. "서버 응답이 문서랑 다르네요." 책임 소재가 명확해지고, 디버깅 시간이 획기적으로 줄어듭니다.
데이터를 검증하면서 동시에 변환할 수도 있습니다.
const PriceSchema = z.string()
// "1,000원" -> 1000 (숫자)으로 변환
.transform((val) => parseInt(val.replace(/,/g, ""), 10));
const result = PriceSchema.parse("1,000"); // result는 숫자 1000
서버에서 오는 Date 문자열을 JS Date 객체로 자동 변환하거나, 빈 문자열을 null로 바꾸는 등의 전처리를 깔끔하게 처리할 수 있습니다.
API 응답뿐만 아니라, 사용자 입력(Form) 검증에도 Zod가 최고입니다.
react-hook-form과 zod-resolver를 함께 쓰면 환상의 짝꿍입니다.
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
const schema = z.object({
username: z.string().min(2, "너무 짧아요"),
age: z.number().min(18, "미성년자는 가입 불가"),
});
const { register, handleSubmit } = useForm({
resolver: zodResolver(schema), // Zod 스키마를 폼 검증 규칙으로 사용!
});
복잡한 if (value.length < 2) 로직을 짤 필요 없이, 스키마 한 줄로 폼 유효성 검사가 끝납니다.
Zod로 검문검색해라. '그냥 타입 단언(as)'을 쓰는 건 시한폭탄을 안고 코딩하는 것과 같다.