레이블이 Zod인 게시물을 표시합니다. 모든 게시물 표시
레이블이 Zod인 게시물을 표시합니다. 모든 게시물 표시

금요일

tRPC 타입 에러: Input Validation 실패와 해결방법

🔍 검색 키워드: tRPC 타입 에러, tRPC input validation 실패, tRPC ZodError, tRPC 클라이언트 타입 추론, tRPC 런타임 검증

tRPC 타입 에러: Input Validation 실패와 해결방법

증상

tRPC 클라이언트에서 정확하게 타입을 맞춰 데이터를 전송했는데도 서버에서 타입 검증 에러가 발생합니다.

Error: [UNPROCESSABLE_CONTENT]: Input validation failed
    ├─ expected number, received string (code: invalid_type)
      └─ at "age"

또는 클라이언트에서 데이터를 보낼 때 타입스크립트 컴파일 에러:

Argument of type '{ name: string; age: string }' is not assignable
  to parameter of type '{ name: string; age: number }'

원인

tRPC는 타입스크립트의 타입 검사런타임 검증(보통 Zod 스키마)을 별도로 수행합니다. 세 가지 주요 원인이 있습니다:

  1. Zod 스키마와 TS 타입이 불일치
    서버에서 z.number()로 정의했지만, 클라이언트 데이터는 문자열로 전달
  2. 타입 강제(coerce)가 없음
    API 요청에서 쿼리 파라미터나 폼 데이터는 항상 문자열이지만, 스키마에서 변환하지 않음
  3. 선택적 필드와 기본값 처리 오류
    .optional() 또는 .default() 누락으로 undefined/null 검증 실패

해결방법

해결책 1: Zod 스키마에서 타입 강제(Coerce)

import { z } from 'zod';
  import { router, publicProcedure } from '@trpc/server';
  
  const userSchema = z.object({
    name: z.string().min(1, "이름은 필수입니다"),
      age: z.coerce.number().min(0, "나이는 0 이상이어야 합니다"),
        email: z.string().email().optional(),
        });
        
        export const appRouter = router({
          createUser: publicProcedure
              .input(userSchema)
                  .mutation(async ({ input }) => {
                        // input.age는 이제 자동으로 number 타입 보장
                              return { success: true, age: input.age };
                                  }),
                                  });

해결책 2: 쿼리 파라미터 검증 시 .default() 사용

export const appRouter = router({
    listUsers: publicProcedure
        .input(
              z.object({
                      limit: z.coerce.number().default(10).min(1).max(100),
                              skip: z.coerce.number().default(0).min(0),
                                      sortBy: z.enum(['name', 'age', 'createdAt']).default('createdAt'),
                                            })
                                                )
                                                    .query(async ({ input }) => {
                                                          // input.limit, skip, sortBy 모두 안전하게 기본값 적용됨
                                                                return { data: [], total: 0 };
                                                                    }),
                                                                    });

해결책 3: 클라이언트에서 타입 안전성 확보

// 클라이언트
  const trpc = createTRPCReact();
  
  export function UserForm() {
    const createUser = trpc.createUser.useMutation();
    
      const handleSubmit = (e: React.FormEvent) => {
          e.preventDefault();
              const formData = new FormData(e.currentTarget);
              
                  // 명시적 타입 변환
                      const payload = {
                            name: formData.get('name') as string,
                                  age: parseInt(formData.get('age') as string), // 문자열 → 숫자
                                        email: (formData.get('email') as string) || undefined,
                                            };
                                            
                                                // 이제 tRPC이 타입 검증 & 런타임 검증 수행
                                                    createUser.mutate(payload);
                                                      };
                                                      
                                                        return (
                                                            
); }

정리표

문제 상황 원인 해결방법
쿼리 파라미터가 문자열인데 number 검증 실패 Zod에서 강제 변환 안 함 z.coerce.number() 사용
폼 데이터 전송 시 "필드 없음" 에러 선택적 필드에 .optional() 미적용 스키마에 .optional() 또는 .default() 추가
TS 컴파일은 통과했는데 런타임 에러 타입과 Zod 스키마 불일치 스키마에서 satisfies z.ZodType<YourType> 검증
API 응답 타입이 예상과 다름 뮤테이션 반환값 타입 정의 누락 .mutation() 뒤에 .output(schema) 체인 추가

tRPC는 타입스크립트 안전성과 런타임 검증을 결합한 강력한 도구입니다. Zod 스키마를 "소스 오브 트루스"로 생각하고, 클라이언트 타입은 자동으로 유추되도록 하면 대부분의 타입 에러를 사전에 방지할 수 있습니다.