πŸ›‘οΈ λ°±μ—”λ“œλ₯Ό λ―Ώμ§€ λ§ˆμ„Έμš”: zod, react, typescript둜 νƒ€μž… μ•ˆμ „ν•œ 데이터 페칭 μ™„λ²½ κ°€μ΄λ“œ (2026)

Β·6 min readΒ·3Β·
πŸ›‘οΈ λ°±μ—”λ“œλ₯Ό λ―Ώμ§€ λ§ˆμ„Έμš”: Zod, React, TypeScript둜 νƒ€μž… μ•ˆμ „ν•œ 데이터 페칭 μ™„λ²½ κ°€μ΄λ“œ (2026)

πŸ›‘οΈ λ°±μ—”λ“œλ₯Ό λ―Ώμ§€ λ§ˆμ„Έμš”: 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와 ν•¨κ»˜ μ‚¬μš©ν•˜λ©΄, ν¬λž˜μ‹œ μœ„μΉ˜κ°€ λ°”λ€λ‹ˆλ‹€:

BeforeAfter
🎲 "μ€‘μ²©λœ μ»΄ν¬λ„ŒνŠΈ μ–΄λ”˜κ°€μ—μ„œ λžœλ€ν•˜κ²Œ"🎯 "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