API First 실전: 프론트엔드 Types를 예측 가능하고 안정적으로 만든 방법

발행: (2026년 1월 12일 오전 06:09 GMT+9)
10 min read
원문: Dev.to

I’m sorry, but I can’t retrieve the article from the link you provided. Could you please paste the text you’d like translated here? I’ll then translate it into Korean while preserving the source link and the original formatting.

문제: 프론트엔드–백엔드 비동기화

  1. 백엔드가 엔드포인트를 변경한다.
  2. 프론트엔드는 여전히 이전 가정을 사용한다.
  3. 수동으로 만든 TypeScript 타입이 오래된다.
  4. 버그가 늦게 나타난다 — 때로는 프로덕션에서만.

프론트엔드가 파괴적인 변경 사항을 알게 되는 시점이 항상 너무 늦다.

TypeScript는 도움이 되지만, 타입이 정확할 때만 그렇다. 이러한 타입을 수동으로 유지하는 것은 규모에 맞지 않는다.

우리의 솔루션: 계약으로서의 API‑First, 문서가 아니라

우리 팀에서 API‑First는 한 가지 간단한 규칙을 의미합니다:

프론트엔드는 백엔드 API에서 생성된 TypeScript 타입과 인터페이스만 사용합니다.

OpenAPI (Swagger) 스키마는 단순한 문서가 아닙니다. 이것은 단일 진실 원천입니다. 계약에 설명되지 않은 것이 있다면, 프론트엔드는 그것이 존재한다고 가정하지 않습니다.

Source:

실제 동작 방식 (CRUD 사용자 예시)

1. 백엔드가 OpenAPI 스키마 제공

백엔드 구현이 완료되기 전에, 백엔드 팀은 YAML 형식의 OpenAPI 스키마를 제공하여 향후 API를 설명합니다. 아래는 단순화된 실제 예시입니다:

openapi: 3.0.3
info:
  title: User Service API
  description: API for managing users
  version: "1.0"

paths:
  /users:
    get:
      tags:
        - users
      summary: Get list of users
      operationId: listUsers
      parameters:
        - name: page
          in: query
          schema:
            type: integer
            default: 1
        - name: limit
          in: query
          schema:
            type: integer
            default: 20
        - name: status
          in: query
          schema:
            type: string
            enum: [active, inactive]
      responses:
        "200":
          description: List of users
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/UserListResponse"

    post:
      tags:
        - users
      summary: Create new user
      operationId: createUser
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CreateUserRequest"
      responses:
        "201":
          description: User created
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/User"

  /users/{id}:
    get:
      tags:
        - users
      summary: Get user by ID
      operationId: getUser
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        "200":
          description: User data
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/User"

components:
  schemas:
    User:
      type: object
      properties:
        id:
          type: string
          format: uuid
        name:
          type: string
        email:
          type: string
          format: email
        status:
          type: string
          enum: [active, inactive]

    CreateUserRequest:
      type: object
      required:
        - name
        - email
      properties:
        name:
          type: string
        email:
          type: string
          format: email

    UserListResponse:
      type: object
      properties:
        data:
          type: array
          items:
            $ref: "#/components/schemas/User"
        total:
          type: integer

이 YAML만으로도 프론트엔드가 작업을 시작할 수 있습니다—백엔드가 아직 구축 중이더라도 말이죠.

2. TypeScript 타입 및 API 요청 함수 생성

프론트엔드에서는 openapi-generator(또는 유사 도구)를 사용해 스키마로부터 TypeScript 코드를 생성합니다. 생성된 내용은 다음과 같습니다:

  • TypeScript 타입 및 인터페이스 (User, CreateUserRequest, …)
  • 바로 사용할 수 있는 API 요청 함수 (listUsers, createUser, getUser)

이제 Axios 호출을 직접 작성하거나 요청 형태를 추측할 필요가 없습니다. 모든 import는 생성된 코드에서 직접 가져오므로, 예를 들어:

// Example usage
import { getUser, createUser, User, CreateUserRequest } from './api';

async function loadUser(id: string) {
  const user: User = await getUser({ id });
  // …
}

async function addUser(payload: CreateUserRequest) {
  const newUser: User = await createUser(payload);
  // …
}

Source:

프론트엔드 코드의 기반이 되는 생성된 타입

타입이 생성되면 프론트엔드 전반에 걸쳐 단일 진실의 원천이 됩니다:

  • API 레이어 – 모든 HTTP 호출이 생성된 함수를 사용합니다.
  • React 컴포넌트 – props와 state가 생성된 인터페이스로 타입이 지정됩니다.
  • 폼 및 검증 – 요청 페이로드 타입이 폼 스키마를 주도합니다.
  • UI 상태 및 셀렉터 – enum과 리터럴 유니온이 UI를 백엔드와 동기화합니다.

예시: Enum 생성

OpenAPI 스키마에서 사용자 상태에 대한 enum을 생성합니다:

export const UserStatusEnum = {
  Active: 'active',
  Inactive: 'inactive',
} as const;

export type UserStatusEnum =
  typeof UserStatusEnum[keyof typeof UserStatusEnum];

이제 프론트엔드에서는 User.status"active" 또는 "inactive" 중 하나만 될 수 있음을 알게 됩니다. 우리는 이 enum을 다음과 같이 사용합니다:

  • 드롭다운 옵션을 구성합니다.
  • 필터 리스트를 생성합니다.
  • 조건부 UI 로직을 구현합니다.

하드코딩된 문자열이 없고, 중복된 상수도 없으며, 불일치가 조용히 발생하지 않습니다.

CI를 조기 경고 시스템으로 활용

이 접근 방식의 진정한 힘은 CI 파이프라인의 일부가 되었을 때 발휘됩니다.

  1. 최신 Swagger 스키마 가져오기 (예: 공유 아티팩트 저장소에서).
  2. Pull‑request 빌드마다 TypeScript 타입 재생성.
  3. tsc와 프런트엔드 단위/통합 테스트 전체를 실행합니다.

백엔드가 예기치 않게 변경되면:

  • 생성된 타입이 바뀝니다.
  • TypeScript 컴파일이 실패하거나 테스트가 깨집니다.

이 실패가 코드가 병합되기 전에 발생하기 때문에, 프런트엔드 팀은 계약이 어긋났다는 즉각적인 신호를 받아 즉시 수정 작업을 조율할 수 있습니다.

TL;DR

OpenAPI 스키마를 유일한 진실의 출처로 간주합니다.
그 스키마에서 TypeScript 타입과 요청 함수를 생성합니다.
생성된 아티팩트만 프론트엔드가 사용하도록 합니다.
CI에서 재생성을 실행해 계약 위반을 조기에 감지합니다.

이 계약‑우선 워크플로우를 적용함으로써 우리는 고전적인 “프론트엔드‑백엔드 불일치” 문제를 없애고, 수동 타입 관리 부담을 줄였으며, UI와 API가 일관되게 동작한다는 확신을 팀에 제공했습니다.

발생한 문제

  • IPT 컴파일 실패
  • 단위 테스트 실패

중요한 부분은 언제 이런 일이 발생하는가:

  • 릴리스 후가 아니라
  • 프로덕션 환경이 아니라
  • CI 중에

이는 백엔드와 프론트엔드 개발자가 문제를 즉시 인지하게 함을 의미합니다. 프론트엔드 테스트는 백엔드 변경에 대한 추가적인 안전망이 됩니다.

이 접근 방식으로 얻은 것

API‑First를 채택한 후 명확한 개선을 확인했습니다:

  • 예측 가능하고 안정적인 프론트엔드 타입
  • 개발 속도 향상
  • 팀 간 질문 감소
  • 파괴적 변경 조기 감지
  • 리팩토링 시 훨씬 높은 자신감

프론트엔드는 추측을 멈추고 계약을 신뢰하기 시작했습니다.

다른 팀에 이 방법을 도입하는 방법

이 접근 방식을 시도하고 싶다면, 저의 조언은 다음과 같습니다:

  1. 하나의 서비스부터 시작하기
  2. 먼저 타입을 생성하고, 필요하면 나중에 요청을 생성하기
  3. 생성된 타입 사용을 강제하기
  4. CI에 타입 검사를 추가하기
  5. OpenAPI를 문서가 아닌 계약으로 다루기

이는 도구보다 규율에 더 가깝습니다.

Final Thoughts

API‑First는 Swagger 파일이나 생성기에 관한 것이 아닙니다.
예측 가능성에 관한 것입니다.

TypeScript 프론트엔드의 경우, 신뢰할 수 있는 계약이 있으면 데이터에 대한 사고 방식이 바뀝니다. API가 진실의 원천이 될 때, 프론트엔드는 더 단순하고, 안전하며, 자신감 있게 됩니다.

Back to Blog

관련 글

더 보기 »

React 컴포넌트에서 TypeScript Generics

소개 제네릭은 React 컴포넌트에서 매일 사용하는 것은 아니지만, 특정 경우에는 유연하고 타입‑안전한 컴포넌트를 작성할 수 있게 해줍니다.