π‘οΈ λ°±μλλ₯Ό λ―Ώμ§ λ§μΈμ: Zod, React, TypeScriptλ‘ νμ μμ ν λ°μ΄ν° νμΉ μλ²½ κ°μ΄λ (2026)
π μλ²½ 3μμ μ λͺ½
μ¬λ¬λΆλ κ²ͺμ΄λ³΄μ
¨μ κ²λλ€. μλ²½ 3μ, Slackμμ "P0 Critical" μλ¦Όμ΄ μΈλ¦¬κ³ , νλ‘λμ
λ‘κ·Έλ Cannot read properties of undefined (reading 'map') μλ¬λ‘ κ°λ μ°¨ μμ΅λλ€.
μ½λλ₯Ό νμΈν΄λ΄
λλ€. μλ²½ν΄ λ³΄μ
λλ€. TypeScriptλ μκ³ ! interfaceλ μ μνκ³ ! κ·μ¬μ΄ λ‘λ© μ€μΌλ ν€κΉμ§ λ£μμ΅λλ€.
κ·Έλ°λ° λ°±μλ νμ΄ β κ·Έλ€μ μ μλ₯Ό μμ¬νμ§ μμ§λ§ β μ무 λ§ μμ΄ API μλ΅μμ user_idλ₯Ό idλ‘ λ°κΏλ²λ Έμ΅λλ€. π±
μ¬λ¬λΆμ μ μ νμ μ μ΄κ±Έ μ‘μ§ λͺ»νμ΅λλ€. μλνλ©΄ TypeScriptλ μ»΄νμΌ νμ μνΌνμ΄λ‘μ§λ§ λ°νμμμλ μ λ Ήμ΄κΈ° λλ¬Έμ λλ€. π»
2026λ μ "κ·Έλ₯ APIλ₯Ό λ―Ώμ"λ 건 "μλμ΄ μΈνλΌ μ²μλΆ"λ‘ μΉμ§νλ μ§λ¦κΈΈμ λλ€. Zodμ React, κ·Έλ¦¬κ³ κ±΄κ°ν μμ¬μΌλ‘ μ΄ λ¬Έμ λ₯Ό ν΄κ²°ν΄λ΄ μλ€!
1οΈβ£ "TypeScriptλ κ±°μ§λ§μμ΄" λ¬Έμ π€₯
λ§€μΌ PRμμ 보λ μ½λκ° μμ΅λλ€. λ³Ό λλ§λ€ λμ΄ λ¨λ¦½λλ€:
// π© μ λ μ΄λ κ² νμ§ λ§μΈμ
const user = await response.json() as User;
μ as User 보μ΄μλμ? μ΄κ±΄ TypeScriptλ₯Ό λλ°λ‘ μ³λ€λ³΄λ©΄μ κ±°μ§λ§νλ κ²λλ€.
"κ±±μ λ§, μΈν°λ·μ΄ λ΄κ° κΈ°λνλ κ² μ νν 보λ΄μ€ κ±°μΌ."
μ€ν¬μΌλ¬: μΈν°λ·μ νΌλμ μΉ΄μ€μ€μ λλ€. πͺοΈ
asλ₯Ό μ¬μ©νλ©΄ νμ
체컀λ₯Ό μ°νν©λλ€. APIκ° number λμ stringμ 보λ΄λ©΄? TypeScriptλ ν볡νκ² μ»΄νμΌνμ§λ§, React μ±μ λ² μ΄λ§μΌμ μνΈνν μ€ννΈμ
λ³΄λ€ λΉ λ₯΄κ² ν¬λμν©λλ€. π
μ°λ¦¬μκ² νμν κ²:
- β λ°νμ κ²μ¦ (Runtime Validation)
- β Zod
2οΈβ£ Zod λ±μ₯: λ°μ΄ν°μ κ²½λΉμ π‘οΈ
Zodλ₯Ό ν΄λ½(μ¬λ¬λΆμ νλ‘ νΈμλ)μ κ²½λΉμμ΄λΌκ³ μκ°νμΈμ.
| μν | κ²μ¬ μμ |
|---|---|
| TypeScript | μ¬λλ€μ΄ μ§μ λ λκΈ° μ 볡μ₯ μ²΄ν¬ |
| Zod | λ¬Έ μμμ μ λΆμ¦ νμΈ |
λͺ λ¨μ μμΌλ©΄? μ μ₯ λΆκ°! π«
π¦ μ€μΉ
npm install zod @tanstack/react-query
π μ€ν€λ§ μ μ
TypeScript interface λμ Zod μ€ν€λ§λ₯Ό μμ±ν©λλ€. μ΄κ²μ΄ λ¨μΌ μ§μ€ 곡κΈμ(Single Source of Truth)μ
λλ€.
import { z } from "zod";
// 1οΈβ£ μ€ν€λ§ μ μ (λ°νμ κ·μΉ)
const UserSchema = z.object({
id: z.string().uuid(),
username: z.string().min(3),
email: z.string().email(),
// 2026 νμ€: APIλ μ§μ λΆν΄μ, μ΅μ
λ νλλ μ°μνκ²!
avatarUrl: z.string().url().optional(),
role: z.enum(["admin", "user", "intern_who_broke_prod"]),
});
// 2οΈβ£ νμ
μΆλ‘ (μ»΄νμΌ νμ κ·μΉ)
// μΈν°νμ΄μ€ μλ μμ± νμ μμ! Zodκ° ν΄μ€λλ€.
type User = z.infer<typeof UserSchema>;
π― ν΅μ¬ ν¬μΈνΈ
- μ€ν€λ§ νλλ‘ λ°νμ κ²μ¦ + νμ μ μ λ λ€ ν΄κ²°
z.inferλ‘ νμ μλ μΆμΆ- μ€λ³΅ μ½λ μ κ±°! β¨
3οΈβ£ νμΉ ν¨ν΄ (feat. TanStack Query) π£
2026λ
μ
λλ€. μ°λ¦¬λ λ¬Έλͺ
μ¬νμ μ΄κ³ μμ΄μ. λ°μ΄ν° νμΉμ useEffectλ₯Ό μ°μ§ μμ΅λλ€. TanStack Query (React Query)λ₯Ό μ¬μ©ν©λλ€.
κ²¬κ³ νκ³ ν¬λμ λ°©μ§λ νμ² ν¨μ
const fetchUser = async (userId: string): Promise<User> => {
const res = await fetch(`/api/users/${userId}`);
if (!res.ok) {
throw new Error(`API exploded with status: ${res.status}`);
}
const rawData = await res.json();
// πͺ λ§λ²μ μκ°!
// rawDataκ° μ€ν€λ§μ λ§μ§ μμΌλ©΄ μ¬κΈ°μ μλ¬λ₯Ό λμ§λλ€.
// "undefined" μλ¬κ° UIκΉμ§ μ νλλ κ²μ ν¨κ³Όμ μΌλ‘ μ°¨λ¨!
return UserSchema.parse(rawData);
};
π€ parse vs as λΉκ΅
| λ°©μ | λμ |
|---|---|
as User | π© νμ 체컀 μ°ν, λ°νμ λ³΄νΈ μμ |
UserSchema.parse() | β μ€ν€λ§ λΆμΌμΉ μ μ¦μ μμΈ μλ¬ |
λ°±μλκ° role: "super_admin" (μ°λ¦¬ enumμ μλ κ°)μ 보λ΄λ©΄?
parse()κ° λ€νΈμν¬ λ μ΄μ΄μμ μ¦μ μλ¬λ₯Ό λμ§λλ€Avatarμ»΄ν¬λνΈκ° λ λλ§ν λκ° μλλΌ! π―
4οΈβ£ μλμ΄ μμ§λμ΄ κΈ°μ : λ³ν λ μ΄μ΄ π§
μ¬κΈ° μ κ° μ½μν κ³ κΈ μΈμ¬μ΄νΈκ° μμ΅λλ€.
λλΆλΆμ μ¬λλ€μ κ²μ¦λ§ ν©λλ€. νλ‘ μμ§λμ΄λ κ²μ¦ + λ³νμ ν©λλ€.
λ¬Έμ μν©
APIλ μ’ μ’ λ°μ΄ν°λ² μ΄μ€κ° μ μ₯νλ λ°©μμΌλ‘ μ€κ³λ©λλ€. νλ‘ νΈμλκ° μ¬μ©νλ λ°©μμ΄ μλλΌμ.
- λ μ§ λ¬Έμμ΄ β
Dateκ°μ²΄ νμ - μΌνΈ λ¨μ κ°κ²© β λ¬λ¬ λ¨μ νμ
UI μ»΄ν¬λνΈ μ¬κΈ°μ κΈ°μ new Date(user.createdAt)λ user.price / 100μ ν©λΏλ¦¬μ§ λ§μΈμ! π€
β¨ Zod μ€ν€λ§μμ ν΄κ²°
const TransactionSchema = z.object({
id: z.string(),
// λ°±μλλ ISO λ¬Έμμ΄μ 보λ: "2026-05-20T10:00:00Z"
timestamp: z.string().datetime(),
// λ°±μλλ μΌνΈ(μ μ)λ₯Ό 보λ: 1500
amountInCents: z.number(),
}).transform((data) => ({
...data,
// β‘ μ¬κΈ°μ λ³ν λ°μ!
// UIλ μ§μ§ Date κ°μ²΄λ₯Ό λ°μ
date: new Date(data.timestamp),
// UIλ ν¬λ§·ν
λ λ¬Έμμ΄μ λ°μ: "$15.00"
formattedAmount: new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'USD'
}).format(data.amountInCents / 100),
}));
type Transaction = z.infer<typeof TransactionSchema>;
// μ΄μ μ»΄ν¬λνΈλ 'date'λ₯Ό Date κ°μ²΄λ‘ μ·¨κΈν©λλ€.
// JSX μμμ λ μ§ νμ± νμ μμ! π
π° Anti-Corruption Layer
μ΄κ²μ λΆν¨ λ°©μ§ λ μ΄μ΄λ‘ μλν©λλ€.
UI μ½λλ κΉλνκ³ μμκ² μ μ§λλ©°, λ°±μλμ μ§μ λΆν ν¬λ§·ν μ μ ν λͺ¨λ¦ λλ€.
5οΈβ£ νν μνλ μμ
: z.catch() π¦ΈββοΈ
μλ리μ€
50κ°μ μν λͺ©λ‘μ νμΉν©λλ€. λ¨ νλμ μνμ΄ μλͺ»λ priceλ₯Ό κ°μ§κ³ μμ΅λλ€.
z.array(ProductSchema).parse(data)λ§ μ¬μ©νλ©΄?
π¨ κ·Έ νλμ μ©μ μ¬κ³Όκ° μ 체 λ°°μ΄ κ²μ¦μ μ€ν¨μν΅λλ€.
μ¬μ©μλ 47λ² μνμ μ΄λ―Έμ§ URLμ΄ μλ€λ μ΄μ λ§μΌλ‘ "λ¬Έμ κ° λ°μνμ΅λλ€" μλ¬ νμ΄μ§λ₯Ό λ΄ λλ€.
그건 λμ UXμ λλ€. π’
π§ ν΄κ²°μ±
: z.catch() λλ μμ ν νμ±
const SafeProductList = z.array(
ProductSchema.catch((ctx) => {
console.error("Failed to parse product:", ctx.error);
// ν΄λ°± λ°ν λλ λμ€μ νν°λ§ν 'invalid' μ¬λ³Ό λ°ν
return { id: "invalid", name: "Corrupted Data", price: 0 };
})
);
// λ μ’μ λ°©λ²: νμ΄νλΌμΈμΌλ‘ μλ νν°λ§!
const ResilientList = z.array(z.any())
.transform((items) => {
return items
.map(item => ProductSchema.safeParse(item))
.filter(result => result.success) // ποΈ μ°λ κΈ°λ λ²λ¦¬κ³
.map(result => result.data); // π κΈλ§ λ¨κΈ°κΈ°
});
πͺ κ²°κ³Ό
μ΄ ν¨ν΄μ νμ΄μ§κ° μ¬μ ν 49κ°μ μ’μ μνμ λ λλ§νλλ‘ λ³΄μ₯ν©λλ€.
ν볡λ ₯μ΄ ν΅μ¬μ λλ€! π
6οΈβ£ λͺ¨λ κ²μ 컀μ€ν ν μΌλ‘ μ 리 πͺ
μ΄ μλ¦λ€μμ λ¨Ήμ΄λ λ λ§νΌ κΉλν ν μΌλ‘ κ°μΈλ΄ μλ€.
import { useQuery } from "@tanstack/react-query";
export const useUser = (userId: string) => {
return useQuery({
queryKey: ["user", userId],
queryFn: () => fetchUser(userId),
// 2026 λͺ¨λ² μ¬λ‘: Zod μλ¬μμλ μ¬μλνμ§ λ§μΈμ!
// μ€ν€λ§κ° ν리면, μ¬μλν΄λ λ°μ΄ν° κ΅¬μ‘°κ° κ³ μ³μ§μ§ μμ΅λλ€.
retry: (failureCount, error) => {
if (error instanceof z.ZodError) return false;
return failureCount < 3;
},
});
};
π― ν¬μΈνΈ
| μν© | μ¬μλ μ¬λΆ |
|---|---|
| λ€νΈμν¬ μλ¬ | β μ΅λ 3λ² |
| Zod κ²μ¦ μλ¬ | β μ¬μλ 무μλ―Έ |
π κ²°λ‘ : νΈν 주무μΈμ
Zodλ₯Ό TypeScriptμ ν¨κ» μ¬μ©νλ©΄, ν¬λμ μμΉκ° λ°λλλ€:
| Before | After |
|---|---|
| π² "μ€μ²©λ μ»΄ν¬λνΈ μ΄λκ°μμ λλ€νκ²" | π― "API κ²½κ³μμ λͺ μμ μΌλ‘" |
μ¬λ¬λΆμ λ¨μν μ½λλ₯Ό μμ±νλ κ² μλλλ€. μμλ₯Ό ꡬμΆνλ κ²μ λλ€. π°
β 체ν¬λ¦¬μ€νΈ
- μ λ ₯κ°μ κ²μ¦νλ€
- μ§μ λΆν λ°μ΄ν°λ₯Ό UI-ready ννλ‘ λ³ννλ€
- λΆλΆ μ€ν¨λ₯Ό μ°μνκ² μ²λ¦¬νλ€
μ΄μ κ°μ μμ§λ anyλ₯Ό μ°λ λλ£λ₯Ό μ°Ύμ μ΄ κΈ λ§ν¬λ₯Ό 보λ΄μ£ΌμΈμ. π
π ν΅μ¬ μμ½
| κ°λ | μ€λͺ |
|---|---|
as ν€μλ | π© νμ 체컀λ₯Ό μμ΄λ κ±°μ§λ§ |
| Zod μ€ν€λ§ | β λ°νμ + μ»΄νμΌνμ λ¨μΌ μ§μ€ 곡κΈμ |
.parse() | μ€ν€λ§ λΆμΌμΉ μ μ¦μ μλ¬ |
.transform() | λ°±μλ β νλ‘ νΈμλ λ°μ΄ν° λ³ν |
.catch() | λΆλΆ μ€ν¨ μ 볡ꡬ |
safeParse() | μλ¬ λμ§μ§ μκ³ κ²°κ³Ό κ°μ²΄ λ°ν |
π μΆμ²: Stop Trusting Your Backend: The Ultimate Guide to Type-Safe Fetching with Zod, React & TypeScript (2026) - Nesan | JavaScript in Plain English



