Zod에서 스키마 정의가 필요한 이유

정성엽·2025년 7월 21일
0

이것저것

목록 보기
3/4

INTRO

최근에 커스텀 로깅 패키지를 만들면서 apiValidation 이라는 유틸리티를 구현하려고 했다.

아이디어는 간단했다. 제네릭으로 타입을 받고 매개변수로 response를 받아서 타입 체크를 통해 문제가 있는 경우에만 콘솔로그를 찍도록 하는 것이었다.

하지만 구현 과정에서 TypeScript의 아주 기본적인 동작을 놓치고 있었기에 포스팅을 통해 회고해보려고 한다.


1. 초기 아이디어

개발하다 보면 이런 상황들이 자주 발생한다

API 응답이 예상한 타입과 다른데 런타임에서야 발견됨
타입스크립트는 컴파일 타임에만 체크하고 런타임에서는 알 수 없음
매번 수동으로 응답 데이터를 검증하기 번거로움

export const postA = async (params: ApiRequest) => {
  const response = await api.post<ApiResponse>(
    "/sample",
    params
  );
  return response.data;
};

주로 이러한 방식으로 제네릭을 통해 타입 추론이 진행되도록 프론트에서 코드를 작성하는게 일반적이다.

하지만, 만약 백엔드에서 ApiResponse 타입에 맞지 않게 실수로 데이터를 넘겨주는 경우에는 postA 데이터를 사용하는 쪽에서 오류가 발생하게 된다.

(주로 사용하는쪽에서 undefined 가 발생하면 API를 잘못 넘겨주는 경우가 대부분이었다.)

경험상 네트워크 탭으로 API 응답 포맷을 명세와 비교하면서 찾았던 기억이 있다.

💡 원하는 기능

그래서 필자는 다음과 같은 기능을 구현해보고 싶었다.

간단한 제네릭 타입 검증

  • Logger.apiValidation<User>(response) 이런 식으로 사용

자동 타입 체크

  • 런타임에서 실제 응답이 타입과 일치하는지 확인

문제 발생시에만 로깅

  • 타입이 맞으면 동작 X / 틀리면 상세한 에러 로그

2. 구현 아이디어 - 불가능

처음에는 정말 단순하게 생각했던거 같다.

위에도 적었지만 제네릭으로 타입을 받아서 런타임에 검증하면 되지 않을까? 라는 아이디어였다.

💡 이상적인 구현 (불가능)

class Logger {
 static apiValidation<T>(response: any): response is T {
   // T 타입의 구조를 알아서 response와 비교하고 싶었음
   const typeInfo = this.extractTypeInfo<T>();
   if(!Logger.validate(response, typeInfo)) {
     console.log("Api Response와 정의된 타입이 일치하지 않습니다.");
   };
 }
}

// 사용
const response = await api.get<ApiResponse>("/endpoint");
Logger.apiValidation<ApiResponse>(response)

하지만, 여기서 가장 중요한건 타입 스크립트는 컴파일시 타입 내용이 사라진다 이다.

즉, 타입 자체가 값이 아니기 때문에 컴파일 시점에서는 이 타입이 사라지게 된다.

따라서, 타입을 이용한 값 검증 자체가 불가능하다.


3. 그래서 스키마 정의가 필요한 이유

이 시점에서 Zod를 다시 살펴보게 되었다.

왜 이렇게 복잡하게 스키마를 정의해야 하는 걸까?

import { z } from 'zod';

// 런타임에 존재하는 실제 객체로 스키마 정의
const UserSchema = z.object({
  id: z.number(),
  name: z.string(),
  email: z.string().email()
});

// 타입은 스키마에서 추출
type User = z.infer<typeof UserSchema>;

// 런타임 검증
const result = UserSchema.safeParse(response);

여기서 직접 구현해보니 핵심을 이해할 수 있었다.

Zod는 내가 생각했던 아이디어와 정반대 방향으로 접근한다.

즉, 타입에서 스키마를 만드는 것이 아니라, 스키마에서 타입을 만드는 것이다.

이렇게 하면 UserSchema는 런타임에 실제로 존재하는 JavaScript 객체이기 때문에 실제 데이터와 비교할 수 있게 된다.

💡 왜 스키마 정의가 필요한가?

결국 정리해보면 다음과 같다.

TypeScript 타입 !== 런타임 객체

  • 컴파일 후에는 타입 정보가 완전히 사라진다
  • 런타임 검증을 위해서는 실제 JavaScript 객체가 필요하다!

OUTRO

이번에 API 검증 유틸리티를 만들어보면서 TypeScript의 타입 시스템과 런타임의 차이점을 명확히 이해할 수 있었다.

타입스크립트는 개발 시점의 안전성을 보장해주지만, 런타임 검증은 별개의 문제였다.

결국 두 영역을 모두 커버하려면 스키마 정의는 필수적이었다...

profile
코린이

0개의 댓글