--- id: wiki-2026-0508-백엔드-프론트엔드-데이터-변환-data-transforma title: 백엔드-프론트엔드 데이터 변환(Data Transformation between Backend and Frontend) category: 10_Wiki/Topics status: verified canonical_id: self aliases: [DTO, Data Transformation, API 응답 변환, snake_case to camelCase] duplicate_of: none source_trust_level: A confidence_score: 0.9 verification_status: applied tags: [frontend, backend, api, data-modeling, typescript] raw_sources: [] last_reinforced: 2026-05-10 github_commit: pending tech_stack: language: typescript framework: react --- # 백엔드-프론트엔드 데이터 변환(Data Transformation between Backend and Frontend) ## 매 한 줄 > **"매 server payload 매 client model 의 일치 안함 — adapter 의 boundary"**. 매 DB schema (snake_case, nullable, denormalized) 매 UI model (camelCase, optional, normalized)의 mismatch 의 explicit 의 mapping. 매 2026 의 Zod + tRPC + GraphQL codegen 의 type-safe automation. ## 매 핵심 ### 매 mismatch dimensions - **Naming**: `user_name` (snake) ↔ `userName` (camel). - **Types**: `1693497600` (Unix epoch) ↔ `Date` object, `"2026-05-10"` ↔ `Date`. - **Shape**: nested vs flat, `{user: {profile: {...}}}` ↔ `{userId, userName, ...}`. - **Nullability**: `null` (DB) ↔ `undefined` (TS optional). - **Enums**: `0/1/2` ↔ `'pending' | 'active' | 'closed'`. ### 매 layers (Clean Architecture) - **DTO** (Data Transfer Object) — wire format, snake_case, raw. - **Domain Model** — business rules, computed props. - **View Model** — UI-specific (formatted strings, derived flags). ### 매 응용 1. REST API 의 camelCase 변환. 2. GraphQL → TypeScript codegen. 3. Date/Decimal 의 typed objects 변환. 4. Form 의 server payload 의 transform. ## 💻 패턴 ### 1. snake_case ↔ camelCase (humps library) ```typescript import { camelizeKeys, decamelizeKeys } from 'humps'; const apiResponse = { user_name: 'Alice', created_at: '2026-05-10' }; const camel = camelizeKeys(apiResponse) as { userName: string; createdAt: string }; // → { userName: 'Alice', createdAt: '2026-05-10' } const payload = decamelizeKeys({ userName: 'Bob' }); // → { user_name: 'Bob' } ``` ### 2. Zod schema (validate + transform) ```typescript import { z } from 'zod'; const UserDTO = z.object({ user_id: z.number(), user_name: z.string(), created_at: z.string(), is_active: z.union([z.literal(0), z.literal(1)]), }).transform(d => ({ userId: d.user_id, userName: d.user_name, createdAt: new Date(d.created_at), isActive: d.is_active === 1, })); type User = z.infer; // { userId: number; userName: string; createdAt: Date; isActive: boolean } async function fetchUser(id: number): Promise { const res = await fetch(`/api/users/${id}`); const json = await res.json(); return UserDTO.parse(json); // validates + transforms } ``` ### 3. TanStack Query select (boundary transform) ```typescript import { useQuery } from '@tanstack/react-query'; export function useUser(id: number) { return useQuery({ queryKey: ['user', id], queryFn: async () => { const res = await fetch(`/api/users/${id}`); return res.json(); }, select: (data) => UserDTO.parse(data), // transform per render }); } ``` ### 4. Date / Decimal serialization ```typescript // server JSON cannot carry Date/BigInt natively import Decimal from 'decimal.js'; const PaymentDTO = z.object({ amount: z.string().transform(s => new Decimal(s)), paid_at: z.string().datetime().transform(s => new Date(s)), }); // reverse: outgoing function toPayload(p: { amount: Decimal; paidAt: Date }) { return { amount: p.amount.toString(), paid_at: p.paidAt.toISOString(), }; } ``` ### 5. tRPC (no transform needed — type-safe RPC) ```typescript // server (router.ts) import { initTRPC } from '@trpc/server'; const t = initTRPC.create(); export const appRouter = t.router({ user: t.procedure .input(z.object({ id: z.number() })) .query(async ({ input }) => { const u = await db.user.findUnique({ where: { id: input.id } }); return u; // typed end-to-end }), }); // client — types inferred, no manual mapping const { data } = trpc.user.useQuery({ id: 1 }); ``` ### 6. GraphQL Codegen ```yaml # codegen.yml schema: http://localhost:4000/graphql documents: 'src/**/*.graphql' generates: src/gql/types.ts: plugins: - typescript - typescript-operations - typescript-react-query ``` ### 7. View Model layer (UI formatting) ```typescript type User = { firstName: string; lastName: string; createdAt: Date }; type UserVM = { fullName: string; joinedAgo: string }; function toVM(u: User): UserVM { return { fullName: `${u.firstName} ${u.lastName}`, joinedAgo: formatDistanceToNow(u.createdAt, { addSuffix: true }), }; } ``` ## 매 결정 기준 | 상황 | Approach | |---|---| | REST + TypeScript | Zod schema 의 validate + transform at boundary. | | Full-stack TypeScript monorepo | tRPC 의 zero transform overhead. | | GraphQL backend | GraphQL Codegen 의 auto types. | | Legacy snake_case API | humps 의 camelize at fetch boundary. | | BigInt / Decimal | string serialization + parse on client. | **기본값**: Zod schema 의 boundary validation + transform. tRPC 매 monorepo 의 ideal. ## 🔗 Graph - 부모: [[Clean Architecture]] - 변형: [[Zod]] · [[GraphQL Codegen]] - 응용: [[React Hook Form]] - Adjacent: [[Schema Validation]] ## 🤖 LLM 활용 **언제**: API integration, form submission, type-safe full-stack, validation at trust boundary. **언제 X**: 매 internal tool 의 untyped — 매 still validate 매 trust boundary. ## ❌ 안티패턴 - **Trust the API blindly**: validate 매 trust boundary — server bug, version mismatch breaks UI silently. - **Transform 의 component-level**: scatter mapping logic — centralize at fetch layer. - **`any` types**: defeats TS — Zod infer 의 type derivation. - **Date as string in app code**: lose type safety — parse to Date at boundary. - **Manual mapping 의 100 fields**: codegen 의 use. ## 🧪 검증 / 중복 - Verified (Zod docs, tRPC docs, Martin Fowler "Patterns of Enterprise Application Architecture"). - 신뢰도 A. ## 🕓 Changelog | 날짜 | 변경 | |---|---| | 2026-05-08 | Phase 1 | | 2026-05-10 | Manual cleanup — full content with Zod/tRPC/Codegen patterns |