본문 바로가기

Schema Validation Layer

by mugglim 2022. 12. 5.

프롤로그

타입스크립트 덕분에 자바스크립트 개발이 편해지고 있습니다. 컴파일 시점에서 타입을 검사하니 코드의 품질을 높일 수 있고, VS Code와 같은 IDE에서 자동완성을 제공해 개발 생산성도 높아지게 됩니다.

하지만 타입스크립트를 통해 개발한다고 해서 완전히 안전한것은 또 아닙니다. 타입스크립트의 컴파일 된 결과물은 결국 자바스크립트 코드입니다. 컴파일 시점에 에러가 발생하지 않더라도, 컴파일 된 자바스크립트 코드에서 런타임 에러가 발생할 수 있습니다. 이번 글에서는 런타임 환경에서도 타입 안정성을 보장할 수 있는 방법을 소개하고자 합니다.

코드를 살펴보고 싶으시다면, 하단 링크를 참고부탁드립니다.

 

GitHub - mugglim/zod-with-react-error-boundary: Boilerplate with zod and ErrorBoundary of react

Boilerplate with zod and ErrorBoundary of react. Contribute to mugglim/zod-with-react-error-boundary development by creating an account on GitHub.

github.com


1. 타입스크립트는 런타임 환경에서 안전하지 않다.

(Type Safe을 보장하지 않는다.)

앞서 말씀드린것과 같이, 타입스크립트는 런타임 환경에서 안전하다고 말할 수 없습니다.

간단한 예를 들어보겠습니다. 아래의 코드는 미리 정의된 Todo 타입을 기반으로 API 응답을 받습니다.

interface Todo {
  id: number;
  userId: number;
  title: string;
  completed: boolean;
}

const BASE_URL = "<https://jsonplaceholder.typicode.com>";

const getTodoList = async () => {
  const response = await fetch(`${BASE_URL}/todos`);
  const todoList: Todo[] = await response.json();

  // todoList의 타입이 항상 Todo[]일까요? 
  return todoList;
};

그런데, 만약 Todo 타입이 변경되면 어떨까요? 혹은 특정 Todo 값이 비어있으면 어떨까요?

Todo를 사용하는 코드에서 런타임 에러가 발생하겠지요. 런타임 에러가 발생한 것은 알 수 있지만, 원인을 정확히 파악하기는 어렵습니다.
만약 런타임 에러가 발생한 환경이 프로덕션 환경이라면 더욱 끔직합니다. 이제 이러한 문제점을 해결하기 위한 Schema Validation 방법을 알아봅시다.

2. Schema Validation

Schema Validation은 컴파일 시점의 타입과 런타임 시점의 값을 비교하는 절차입니다. Schema Validation 절차를 통해 런타임 환경에서도 타입 안정성을 확보할 수 있게 됩니다.

예로 들어보겠습니다. 현재, View 코드는 API를 호출 후, Reponse 값을 기반으로 렌더링하고 있습니다.

만약 HTTP Reponse 타입이 타입스크립트 타입과 일치하지 않으면 어떻게 할까요?

에러가 발생한 UI를 렌더링 해야 합니다. 또한, 에러가 발생한 원인을 로깅해야 합니다. 현재 상황에서는 런타임에러가 발생하더라도 원인을 파악하기 어려워, 로깅의 의미가 없습니다.

해결을 위해 Schema Validation Layer을 배치한 흐름을 고려 해봅시다. 물론 API Controller 앞에 있어도 됩니다. 만약 React를 사용한다면 ErrorBoundary를 통해 에러 로깅 및 fallback UI 코드를 우아하게 처리할 수 있을 것 입니다.

// Foo.tsx
const Foo = () => {
  return (
    <ErrorBoundary>
        <ThrowErrorComponent />
    </ErrorBoundary>;
    )
};

// ErrorBoundary.tsx
componentDidCatch(error){
    if(error instanceof ServerError){
        // 서버의 응답이 실패한 경우
    }

    if(error instanceof ZodError){
        // 스키마 에러가 발생한 경우
    }
}

3. zod

zod는 Schema validation을 위한 라이브러리입니다. 물론, 직접 schema validation 코드를 작성해볼 수 있습니다. 우선 zod의 동작원리와 매력을 한번 느껴보시죠.

zod를 사용하기 위해서는 우선 스키마를 정의해야 합니다. 위에서 언급한 Todo 타입을 기반으로 작성해보겠습니다. 아래 코드를 살펴보면, z.object를 통해 타입이 아닌 실제 자바스크립트 코드임을 확인할 수 있습니다. 런타임 환경에서도 타입 검증을 위한 조건이라고 생각하시면 될 것 같습니다.

참고로, zod를 사용하시면 별도로 타입을 선언하기 보다는 z.infer을 활용하길 추천드립니다.

// lib/schemas/todo.ts
import { z } from 'zod';

export const TodoSchema = z.object({
  id: z.number(),
  userId: z.number(),
  title: z.string(),
  completed: z.boolean(),
});

export const TodoListSchema = z.array(TodoSchema);

// 별도로 type을 선언할 필요가 없음
export type STodo = z.infer<typeof TodoSchema>;
export type STodoList = z.infer<typeof TodoListSchema>;

아래 코드는 Todo 스키마를 기반으로 간단한 HTTP 요청을 수행합니다. 만약 런타임 환경에서 스키마 관련 에러가 발생하면 ZodError 런타임 에러가 발생하게 됩니다.

// lib/apis/todo.ts

import { TodoListSchema } from '../schemas/todo';

const getTodoList = async () => {
  const response = await fetch(`${import.meta.env.VITE_BASE_URL}/todos`);
  const todoList = await response.json();

  // Schema validation이 실패한 경우, "ZodError"을 throw 함.
  return TodoListSchema.parse(todoList);
};

export { getTodoList };

글을 마무리 하며

1. zod가 유용한가요?

네. 유용하다고 생각합니다.

간단한 타입가드를 통해 런타임에서도 Type Safe를 보장할 순 있다. 하지만, Type Safe을 위한 로직이 추가되면 함수의 역할이 많아지게 된다. 또한, 디버깅/로깅 과정에도 꽤나 불편하다. Type Safe를 검증하는 로직을 분리하는건 코드의 확장성에 도움이 된다고 생각합니다.

또한 FE에서 장애가 발생했을 떄, 장애 대응을 위해 로깅이 필요하다. 그런데, 로깅을 할 때 원인이 명확하지 않다면 로깅을 하는 의미가 없어집니다. zod를 사용하면 에러의 원인을 좁힐 수 있기 때문에 원인 파악에 시간을 줄일 수 있습니다.

단, 오버엔지니어링 일 수 있습니다.

백엔드 스키마가 변경 될 일은 사실 크게 많지는 않습니다. 그리고 스키마와 다른 데이터가 오는 경우도 많지 않습니다. 예를 들어, 협의 되지 않는 스키마 변경을 대응하기 위해, 프론트에서 방어 로직을 작성하는게 맞을지는 고민이 필요할 것 같습니다.

Reference

댓글