우리는 타입스크립트를 신뢰할 수 있을까?

Typescript | 2024년 10월 02일

Typescript를 보다 신뢰할 수 있는 코드로 만드는 방법을 소개합니다.

Zod를 통해서 Api Response에 Type을 설정하는 방법을 소개합니다.

우리가 TypeScript를 사용하는 목적은 무엇일까요?

저는 작성한 코드를 안전하게 관리하고, 예상치 못한 오류를 방지하는 것이라고 생각해요. 우리는 작성한 타입 정의를 신뢰할 수 있어야 합니다. 그렇지 않으면, 타입 정의로 인해 코드에서 버그가 발생할 수 있으며, 이는 생산성을 크게 저하시킬 수 있으니까요.

이 문제를 해결하는 간단하면서도 효과적인 방법으로, Zod를 사용해 API 응답을 type-safe하게 관리할 수 있는 방안을 제안해볼게요.

왜 API Response를 검증해야 할까?

TypeScript로 프론트엔드 개발을 하면서 API Response에 타입을 설정하는 것은 흔한 일입니다. 하지만 실제로 서버에서 반환된 데이터의 타입이 우리가 정의한 타입과 일치하지 않는다면, 프론트엔드에서는 예상치 못한 오류가 발생할 수 있어요.

물론, 프론트엔드에서 에러 처리를 하거나 공통된 에러 처리 로직이 있다면 큰 문제는 방지할 수 있지만, 여전히 보다 체계적이고 안전한 방식으로 API 응답을 검증하고, 오류를 쉽게 핸들링할 수 있는 방법이 필요하다고 생각해요.

이번 포스팅에서는 Zod를 사용해 API Response 타입을 검증하고, 오류가 발생했을 때 간단하게 처리할 수 있는 방법을 소개해볼게요.

Zod를 사용한 타입 검증

아래 코드는 API 응답 타입을 검증하는 safeFactory 함수입니다. 이 함수는 API 요청의 응답 타입이 우리가 기대한 타입과 일치하는지 검증하고, 일치하지 않으면 에러를 발생시킵니다.

safeFactory
import { z, ZodType } from "zod";
import { del, get, patch, post, put } from "../instance";

type Method = typeof get | typeof post | typeof put | typeof patch | typeof del;

const safeFactory =
  <A extends Parameters<Method>>(method: (...args: A) => ReturnType<Method>) =>
  <Z extends ZodType>(zodSchema: Z) =>
  async (...args: A): Promise<z.infer<Z>> => {
    const response = await method(...args);
    const parsed = zodSchema.safeParse(response);

    if (parsed.error) throw new Error("API_TYPE_NOT_MATCH");

    return parsed.data;
  };

export const safeGet = safeFactory(get);
export const safePost = safeFactory(post);
export const safePut = safeFactory(put);
export const safePatch = safeFactory(patch);
export const safeDel = safeFactory(del);

safeFactory는 API 요청 메서드와 Zod 스키마를 인자로 받아, 해당 메서드가 반환하는 응답이 스키마와 일치하는지 검증해요. 이때 safeParse를 사용하여 응답 데이터를 안전하게 파싱하고, 만약 스키마와 일치하지 않는다면 오류를 발생시킵니다.

이 과정의 이점

  1. 안전성: API 응답이 예상한 타입과 다를 경우, 프론트엔드에서 바로 오류를 감지하고 대응할 수 있어, 예상치 못한 오류로 인한 버그 발생 가능성을 줄여줍니다.
  2. 명확한 오류 처리: 스키마 검증에 실패할 경우, safeParse는 오류 객체를 반환하며, 이를 통해 오류의 원인을 정확히 파악할 수 있습니다. 이 예시에서는 "API_TYPE_NOT_MATCH" 오류 메시지를 던지지만, 실제 운영 환경에서는 구체적인 오류 메시지를 추가해 디버깅을 쉽게 할 수 있습니다.

즉, 응답이 스키마와 일치하지 않으면 오류를 명확하게 식별하고, 적절히 처리할 수 있다는 점에서 코드를 더 신뢰할 수 있게 만듭니다.

어떻게 사용할 수 있을까?

다음은 safeFactory를 실제로 사용하는 예시입니다:

user-api
const UserSchema = z.object({
  userId: z.number(),
  userName: z.string(),
  image: z.string().url(),
  tags: z.array(z.string()),
});

type User = z.infer<typeof UserSchema>;

const user = await safeGet(UserSchema)("/user");

우리는 기존에 axios.get을 사용하던 방식과 매우 유사하게 API 요청을 작성할 수 있으며, 동시에 응답 타입을 안전하게 검증할 수 있어요. 이를 통해 API 응답에 대한 타입 신뢰도를 크게 높일 수 있었구요.

발생할 수 있는 문제

위 코드를 실제 서비스에 적용했을 때 어떤 문제점이 생길 수 있는지 그리고 어떻게 대응할 수 있는지 알아볼게요.

위 코드 예시대로 그대로 사용한다면 아마 배포 타이밍 관련해서 에러가 발생할 수 있어요.

API 버전 관리

가장 명확한 방법 중 하나는 API 버전 관리를 통해, 프론트엔드와 백엔드가 같은 버전을 참조하도록 하는 것 입니다.

이 방법에 대해서는 이 글에서 집중하고 있는 부분은 아니라서 참고하실 수 있는 링크를 첨부하겠습니다.

Zod 스키마의 유연성

Zod에서는 기본적으로 스키마 검증이 엄격하지만, 선택적 필드나 추가 필드를 허용하는 기능도 제공하고 있어요. 예를 들어서, 백엔드에서 새로운 필드가 추가되더라도 기존 필드에만 의존하는 프론트엔드는 문제가 발생하지 않도록 할 수 있습니다.

user-api-partial
const UserSchema = z.object({
  userId: z.number(),
  userName: z.string(),
  image: z.string().url(),
  tags: z.array(z.string()),
}).partial();

partial 기능은 TypeScript의 optional chaining과 비슷해 보일 수 있지만, 사실은 다릅니다. partial은 입력된 스키마에서 모든 필드를 선택적(optional)으로 만들어, 해당 필드가 없어도 유효한 데이터로 간주하도록 바꿔주는 메서드입니다. 반면 optional chaining은 런타임에 객체의 특정 필드가 존재하지 않을 때 안전하게 접근할 수 있도록 도와주는 기능입니다.

즉, partial은 스키마를 느슨하게 만드는 것이고, optional chaining은 데이터 접근 방식을 안전하게 만드는 기능입니다.

자세한 내용은 링크를 참고해주세요!

또는 새로운 필드가 추가되어도 스키마에서 무시할 수 있도록 처리할 수도 있습니다.

user-api-passthrouth
const UserSchema = z.object({
  userId: z.number(),
  userName: z.string(),
  image: z.string().url(),
  tags: z.array(z.string()),
}).passthrough();  // 예상하지 못한 추가 필드를 허용

passthrough는 스키마를 검증하는 과정에서 알지 못하는 key가 들어오면 해당 필드를 허용하는 기능을 가지고 있습니다.

자세한 내용은 링크를 참고해주세요.

이렇게 하면 백엔드에서 새로운 필드가 추가되더라도 프론트엔드에서 오류가 발생하지 않고 안전하게 처리될 수 있습니다.

두 번째로 소개한 Zod 스키마의 유연한 활용은, 엄격한 타입 검증을 통해 코드의 신뢰성을 높이는 본래 목적과는 조금 다를 수 있어요.

하지만 실제 서비스 환경에서는 이상과 현실 사이에서 적절한 타협이 필요할 때가 많죠. 엄격한 검증을 고수하면서도 운영에서의 이슈를 최소화하는 방안을 고민하는 것도 중요하니까요.

마치며

프론트엔드에서 중요한 과제중 하나는 데이터 검증이라고 생각해요. 우리가 작성한 타입 정의가 실제로도 신뢰할 수 있는지, 그리고 예상치 못한 오류를 미리 방지할 수 있는지를 점검하는 과정은 필수적이라고 생각합니다. Zod와 같은 도구를 통해 API 응답 타입을 검증하고, 코드의 안전성을 한층 강화할 수 있습니다.

타입 안정성에 대해 다시 한번 생각해보고, 코드가 더 신뢰할 수 있도록 개선해보는 건 어떨까요?

참고자료