본문 바로가기
개발

openapi-typescript + Zod로 API 타입 빈틈 메우기

by 현명5079 2026. 2. 1.

❓ 기존의 코드 : TypeScript로만 API타입 정의

 기존 사내에서 API타입은 다른 라이브러리 없이 TypeScript로만 정의되어있었다. 다음과 비슷하게 말이다. 

type MemberV1 = {
  name: string;
  email: string;
  phone: string; //1. 휴먼에러 => 백엔드에서는 phoneNumber에 담아주는 값
};

async function fetchMemberV1(memberId: number): Promise<MemberV1> {
  const response = await apiRequester.get<ServiceApiResponse<MemberV1>>(
    `/api/members/${memberId}`,
  );
  return response.data.data;
}

export function Page() {
  const [member, setMember] = useState<MemberV1 | null>(null);

  useEffect(() => {
    fetchMemberV1(1).then(setMember);
  }, []);

  if (!member) return <div>로딩중...</div>;

  return (
    <div>
      <p>이름: {member.name}</p>
      <p>이메일: {member.email}</p>
      <p>전화번호: {member.phone}</p> //2. 렌더링 안됨
    </div>
  );
}

 

회원데이터를 백엔드에서 조회하여 렌더링하는 페이지다. API 스펙을 보고 MemberV1을 정의한 뒤, 화면에 바인딩해준다. 하지만 이 화면은 제대로 렌더링되지 않는다. 프론트엔드에서는 전화번호를 phone 필드로 받고 있지만, 백엔드에서는 phoneNumber에 담아주고 있기 때문이다.

 

이때 TypeScript는 컴파일 에러를 띄워주지 않는다. 개발자가 직접 정의한 MemberV1 타입 자체에는 문법적 오류가 없기 때문이다.
TypeScript는 코드 내부의 타입만 검증할 뿐, 서버에서 내려오는 실제 JSON의 구조까지는 보장하지 못한다.

실제 사내에서도 이렇게 API 스펙을 잘못 옮겨 적어원인을 찾는 데 꽤 오래 걸린 적이 있다. 이 문제를 어떻게 해결할 수 있을까?

우리 팀은 이러한 생산성 저하를 막기 위해 openapi-typescriptZod를 도입하기로 결정했다. 


1️⃣  openapi-typescript로 타입 자동 생성하기

openapi-typescript는 Swagger 기반으로 타입을 자동 생성해 주는 라이브러리다. 이를 활용하면 개발자가 타입을 직접 정의하지 않고도 Swagger와 일치시킬 수가 있다.

 

openapi-typescript 이외에도 swagger-typescript-api, orval가 있는데, 특히 orval을 쓰면 API 타입뿐 아니라 API 호출 함수, react-query 훅까지 자동 생성할 수 있어 자동화 범위가 넓다. 하지만 자동 생성되는 함수, 변수명의 커스터마이징이 어려웠고, orval이 react-query의 버전 변경을 제때 반영해주지 못할 수 있다는 점이 걸렸다. 그래서 타입 생성만 담당하는 openapi-typescript를 도입하기로 결정했다. 

 

📌사용법

라이브러리를 설치한 후 package.json에 다음과 같은 script만 입력해주면 된다. /docs/json은 Swagger를 JSON으로 제공해주는 페이지 URL을 적어주면 된다.      

//pacakge.json
"scripts": {
  "generate:types": "openapi-typescript https://서버주소/docs/json -o src/shared/api/openapi/generated.ts"
},

 

이제 npm run generate:types 를 해주면 openapi/generated.ts파일에 타입이 생성된다. 그리고 아래처럼 사용할 수 있다. 

//openapi-typescript가 적용된 코드

type MemberV2 =
  paths["/api/members/{memberId}"]["get"]["responses"]["200"]["content"]["application/json"];

async function fetchMemberV2(memberId: number): Promise<MemberV2> {
  const response = await apiRequester.get<ServiceApiResponse<MemberV2>>(
    `/api/members/${memberId}`,
  );
  return response.data.data;
}
export function Page() {
  const [member, setMember] = useState<MemberV2 | null>(null);

  useEffect(() => {
    fetchMemberV2(1).then(setMember);
  }, []);

  if (!member) return <div>로딩중...</div>;

  return (
    <div>
      <p>이름: {member.name}</p>
      <p>이메일: {member.email}</p>
      <p>전화번호: {member.phone}</p> //컴파일 에러 발생해서 개발자가 수정가능!
    </div>
  );
}

 

이제는 백엔드 Swagger에 정의된 타입을 그대로 쓰기 때문에 member.phone에서 컴파일 에러가 발생한다. 이를 통해 타입 에러를 개발단계에서 잡을 수가 있게 됐다. 

 

❓그러나 아직도 문제!

openapi-typescript를 쓰면 생산성도 향상되고, 타입에러도 줄어들지만 여전히 버그 가능성이 숨어있다.

- 백엔드에서 실수로 실제 API Spec과 다르게 Swagger를 생성

- Swagger가 수정됐는데 프론트엔드에서 generated.ts를 업데이트하지 않음

 

이 상태로 배포되면, TypeScript는 런타임 오류를 감지하지 못한다.
실제 API 응답이 달라도, 에러 없이 서비스가 동작해 버릴 수 있다.

 

이를 보완하기 위해 런타임 검증 도구가 필요했고, 그 대안으로 Zod를 도입했다.


2️⃣ Zod로 API 런타임검증하기

Zod는 런타임에서 데이터의 유효성을 검증하는 라이브러리다. 스키마를 정의하고, .parse()를 호출하면 런타임에 실제 데이터가 스키마와 일치하는지 검증해준다. 일치하지 않으면 ZodError를 throw 해준다. 이를 openapi-typescript와 조합하면 다음처럼 사용할 수 있다. 

//openapi-typescript로 정의
type MemberV3 =
  paths["/api/members/{memberId}"]["get"]["responses"]["200"]["content"]["application/json"];

//Zod 스키마 생성
const MemberV3Schema = z.object({
  memberId: z.number(),
  name: z.string(),
  email: z.string(),
  phoneNumber: z.string(),
}) satisfies z.ZodType<MemberV3>;

async function fetchMemberV3(memberId: number): Promise<MemberV3> {
  const response = await apiRequester.get<ServiceApiResponse<MemberV3>>(
    `/api/members/${memberId}`,
  );
  return MemberV3Schema.parse(response.data.data);//스키마와 다르면 ZodError 발생!
}
export function Page() {
  const [member, setMember] = useState<MemberV3 | null>(null);

  useEffect(() => {
    fetchMemberV3(1).then(setMember);
  }, []);

  if (!member) return <div>로딩중...</div>;

  return (
    <div>
      <p>이름: {member.name}</p>
      <p>이메일: {member.email}</p>
      <p>전화번호: {member.phoneNumber}</p>
    </div>
  );
}

Swagger에서 생성된 타입과 동일한 구조의 Zod 스키마를 만든다. 이때 satisfies z.ZodType<MemberV3>를 붙이면, 스키마가    Swagger 타입과 일치하는지 컴파일 타임에 검증할 수 있다. 그리고 스키마와 API 응답이 다르다면 .parse에서 ZodError가 발생해서 런타임에서 타입이 틀렸는지를 감지할 수 있게 된다.