서버에서 받아온 타입은 어떻게 타입안정성을 보장할까? 에 대한 생각기록

---
생각

서론

최근 B사의 과제 전형을 진행하며 다양한 질문을 받았다. 그 중 아직 정답을 못찾은 API 호출과 타입 안정성에 대한 내용을 정리해보려고한다.

과제는 단순히 API를 호출해서 설문지를 제출하는 기능을 구현하는 것이었지만, 면접관의 질문은 내가 작성한 코드에 대해서 매우 디테일한 부분까지 질문했다.

import type { QuestionForm } from "../types/form";

export const fetchQuestions = async (): Promise<QuestionForm> => {
  const res = await fetch(BASE_URL);
  if (!res.ok) throw new Error("데이터를 불러오는 도중 에러가 발생했습니다.");
  return res.json();
};

우선 내 전체 코드가 되게 직관적이고 구조도 잘 짜여있어서 칭찬 받았다. 나름 기분이 좋았음. 본론으로 돌아가 API 호출 관련해서나온 질문인데

"이렇게 API를 호출하면, 백엔드의 응답 타입이 바뀌었을 때 어떻게 검증할 건가요? res.json() 은 any 타입으로 추론될 텐데, 어떻게 클라이언트에서 안전한 타입 검증을 할 수 있을까요?"

라는 질문을 받았다.

첫 번째 질문. 컴파일 타임 검증의 한계

내 코드에서 fetchQuestions 함수는 Promise<QuestionForm> 타입을 반환한다고 명시하고 있다. 하지만 타입스크립트는 컴파일 타임에만 작동한다. 즉, 빌드 시점에는 타입 에러를 잡아줄 수 있지만, 실제 런타임에 API에서 QuestionForm과 다른 데이터가 넘어오면 타입스크립트는 이를 감지하지 못한다 .그래서 res.jsonany타입으로 추론된다. 결과적으로 의도하지 않은 undefinednull 등의 값이 들어와 애플리케이션이 예상치 못한 오류로 멈출 수 있다.

이런 디테일을 고려하지 않고 기능 구현에만 집중했기 때문에 좀 뼈맞은 질문을 받았다. 그럼에도 내가 했던 답변은 "타입 가드를 활용해 런타임에 타입을 한 번 더 검증하겠습니다."라고 답변했던 거같다.


개발 피로도에 관한 질문.

"매 API 호출마다 타입 가드를 쓰는 건 너무 개발 피로도가 높지 않을까요? 더 좋은 방법은 없을까요?"

한 단계 더 깊은 질문을 받았다. 매번 수동으로 타입 가드 함수를 만드는 것은 반복적이고 비효율적이긴 하다. 하지만 실제로 내가 프로젝트를 할땐 하나하나 다 타입가드를 꼼꼼히 만들었어서 이런 고민은 하지 못했다. 과연 타입가드를 하나씩 만드는게 피로도가 높아지는걸까?? .. 그럴수도 있겠다. 그래서 내 답변은

zod나 yup같은 스키마기반 런타임 유효성 도구를 사용해 런타임 검증을 자동화하는 방법을 제시했다.

스키마를 한 번 정의해 두면, 이 스키마를 통해 런타임에 데이터의 유효성을 검사할 수 있고, 스키마로부터 타입스크립트 타입을 자동으로 추론할 수도 있다고 생각했다.

// Zod 스키마 정의
import { z } from 'zod';

const QuestionFormSchema = z.object({
  id: z.number(),
  title: z.string(),
  content: z.string(),
});

// fetch 함수에 적용
export const fetchQuestions = async (): Promise<z.infer<typeof QuestionFormSchema>> => {
  const res = await fetch(BASE_URL);
  if (!res.ok) throw new Error("데이터를 불러오는 도중 에러가 발생했습니다.");

  const data = await res.json();
  // Zod를 사용하여 런타임 유효성 검사
  return QuestionFormSchema.parse(data);
};

이 방식은 수동 타입 가드보다 훨씬 간결하고 안전하다. QuestionFormSchema.parse(data) 코드가 런타임에 데이터가 스키마에 맞지 않으면 오류를 발생시켜, 애플리케이션이 잘못된 데이터로 인해 망가지는 것을 방지해준다.


Zod의 성능 문제와 최적화 관련된 질문.

"매 런타임마다 Zod로 검사하는 게 과연 괜찮은 방식일까요?"

면접관의 마지막 질문은 Zod의 성능 오버헤드에 대한 것이었던 것 같다. 사실 zod를 많이 써보지 않아서 저 질문에 대한 의도가 정확히 뭔지 모르겠다. 그래서 지금 되짚어보면 저 질문의 의미는

“모든 API 호출마다, 특히 대량의 데이터를 처리할 때마다 유효성 검사를 수행하는 것이 성능에 미치는 영향을 고민해야 한다는 것” 인거같다.

물론 대부분의 경우 Zod의 성능 저하는 미미하다고 한다. 하지만 면접관은 "최적의 상황"을 가정하는 것이 아니라, "문제가 될 수 있는 상황"에 대해서 나의 깊은 이해를 확인하고 싶었던 것같다. 이에 대해서 정확히 답변을 하지 못했는데 지금 시간이 지나고 나서 나름 정리된 나의 최종답변을 정리해보자.

  1. 상황에 맞는 유연한 전략: 만약 백엔드와 프론트엔드가 긴밀하게 협업하고, API 스펙이 안정적이라면 Zod의 런타임 검사를 생략하고 컴파일 타임 타입스크립트만으로도 충분할 수 있습니다. 반대로, API 스펙이 자주 변경되거나 불안정하다면 Zod를 사용하는 것이 더 안전하다고 생각합니다.
  2. OpenAPI Generator 활용: 더 나아가, OpenAPI/Swagger 스펙을 사용해 백엔드 API 스펙을 자동으로 연동하는 방법도 있습니다. 백엔드에서 스펙을 정의하면, 이를 기반으로 프론트엔드 API 클라이언트 코드와 타입 정의를 자동으로 생성해주는 도구입니다. 이 방식은 휴먼 에러를 최소화하고, 백엔드 스펙 변경에 즉각적으로 대응할 수 있는 가장 이상적인 솔루션이라고 생각합니다.

결론

이번 면접을 통해 단순한 API 호출에도 깊은 고민이 필요하다는 것을 깨달았다. 그리고 어떻게 클라이언트 단에서 타입을 안정하게 검증할 것이냐는 사실 정답이 없었던것 같다. 단순히 fetch 로 API를 호출하는 것을 넘어, 컴파일 타임과 런타임의 차이, 효율적인 검증 방법, 그리고 협업 환경에서의 최적화까지 고려하는 것이 중요하다고 생각한다.

목차