Skip to content
Choi Jeongmin edited this page Dec 1, 2024 · 1 revision

📄 Zod

프론트엔드에서 백엔드와의 데이터 송수신 시 약속된 타입의 런타임 검증을 위해 Zod 라이브러리를 도입한 과정을 기술합니다.

🧩 배경 및 필요성

저희 팀에서는 프론트엔드와 백엔드 간 데이터 송수신 시 타입 일관성이 중요하게 다뤄졌습니다.

백엔드는 class-validator를 활용해 클라이언트로부터 들어오는 데이터의 유효성을 검증하고 있었습니다.

하지만 프론트엔드는 백엔드에서 제공받는 데이터에 대한 검증이 부족해 다음과 같은 문제가 발생했습니다.

  1. 런타임 오류 증가
    • 예상치 못한 데이터 구조로 인해 UI가 깨지거나 로직이 실패함.
  2. 디버깅 비용 증가
    • 문제의 원인이 프론트엔드의 예상 타입 불일치임을 찾는 과정에서 시간 낭비.
  3. TypeScript의 한계
    • TypeScript가 컴파일 타임에만 작동해 런타임 검증 부재.

따라서, 백엔드와 송수신되는 데이터의 런타임 타입 검증을 통해 신뢰성을 확보하고 타입 일관성을 유지하는 것을 목표로 Zod를 도입하게 되었습니다.

🗺️ 문제 해결 과정

Zod 도입

Zod는 TypeScript에서 스키마 정의 및 검증을 위한 라이브러리로, 요구사항에 적합한 솔루션이었습니다.

  1. 런타임 검증 → 데이터를 런타임에서 검증해 잘못된 데이터 구조를 미리 차단.
  2. 타입 생성 자동화Zod 스키마를 기반으로 TypeScript 타입을 생성해 중복 정의 제거.
  3. 간편한 API → 직관적인 메서드와 풍부한 타입 지원.

구현 사례

Zod를 도입한 후, API 통신 시 송수신 데이터를 검증하는 로직을 추가했습니다. 아래는 실제로 적용한 코드입니다.

먼저, 스키마와 타입을 Zod를 통해 정의하고 생성합니다.

export const GetQuestionsRequestSchema = z.object({
  sessionId: z.string(),
  token: z.string().optional(),
});

export const GetQuestionsResponseSchema = z.object({
  questions: z.array(QuestionSchema),
  isHost: z.boolean(),
  expired: z.boolean(),
  sessionTitle: z.string(),
});

export type GetQuestionsRequestDTO = z.infer<typeof GetQuestionsRequestSchema>;

export type GetQuestionsResponseDTO = z.infer<typeof GetQuestionsResponseSchema>;

이후, API 요청부에서 요청 데이터에 대한 검증을 1차적으로 하고, 2차적으로 수신 데이터에 대해 검증을 진행하는 방식으로 사용하고 있습니다.

export const getQuestions = (params: GetQuestionsRequestDTO) =>
  axios
    .get<GetQuestionsResponseDTO>('/api/questions', {
      params: GetQuestionsRequestSchema.parse(params),
    })
    .then((res) => GetQuestionsResponseSchema.parse(res.data));

export const postQuestion = (body: PostQuestionRequestDTO) =>
  axios
    .post<PostQuestionResponseDTO>(
      '/api/questions',
      PostQuestionRequestSchema.parse(body),
    )
    .then((res) => PostQuestionResponseSchema.parse(res.data));

⚙️ 트러블슈팅

Zod 도입 후 로그인 문제

Zod를 도입한 후 시연 중 로그인이 실패했는데, 이에 대한 피드백이 없다는 내용을 들었습니다.

그 이유는 로그인 요청 실패에 대한 처리는 되어 있었으나, Zod를 통한 값에 대한 검증 실패 시 에러 처리가 누락되어 있었기 때문입니다.

사용자가 이메일 형식을 지키지 않는다거나, 스키마에서 허용되지 않는 입력을 하게 되면 발생하는 에러에 대한 처리가 추가로 필요한 상황이었습니다.

export const PostLoginRequestSchema = z.object({
  email: z.string().email(),
  password: z.string().min(8).max(20),
});

export type PostLoginRequestDTO = z.infer<typeof PostLoginRequestSchema>;
  • 로그인 시 클라이언트가 백엔드로 전송하기 전 데이터의 유효성을 검증하기 위해 사용하는 스키마와 타입입니다.
export const login = (body: PostLoginRequestDTO) =>
  axios
    .post<PostLoginResponseDTO>(
      `${AUTH_BASE_URL}/login`,
      PostLoginRequestSchema.parse(body),
    )
    .then((res) => PostLoginResponseSchema.parse(res.data));
  • 송수신 데이터에 대해 검증을 하고 있지만, API 호출 함수에서는 에러 처리에 대한 책임을 지고 있지 않습니다.
  • 호출부에서 에러 처리에 대한 책임을 져야 하는 상황입니다.

따라서 현재는 로그인 부분에 한해서 해당 에러에 대한 처리를 추가로 해주었지만, 아직 모든 부분에서 Zod로부터 발생한 에러 처리가 완전히 이루어지지 않아 아쉬움이 남습니다.

📈 결과 및 성과

Zod를 통해 런타임 검증을 함으로써 프론트엔드와 백엔드 간 타입 신뢰를 기반으로 보다 효율적인 작업을 할 수 있게 되었습니다.

  • 디버깅 효율성 향상
    • 데이터 불일치로 인한 런타임 오류를 사전에 방지할 수 있었습니다.
  • 타입 일관성 확보
    • 프론트엔드와 백엔드 간 타입 충돌 문제를 해결할 수 있었습니다.

이후 고려 사항

현재 백엔드는 class-validator를, 프론트엔드는 Zod를 활용하여 데이터 유효성 검사를 진행하고 있으나, 모노레포의 장점을 살려 공통 타입 패키지를 만드는 것을 고려하고 있습니다.

이를 통해 아래의 장점을 확보할 수 있을 것으로 생각합니다.

  1. 프론트엔드와 백엔드의 타입 및 검증 로직 공유
  2. 검증부 작업의 일관성 및 신뢰성 확보
Clone this wiki locally