Python–TypeScript 계약

발행: (2026년 2월 10일 오후 09:12 GMT+9)
8 분 소요
원문: Dev.to

Source: Dev.to

The Python–TypeScript 계약을 위한 표지 이미지

nicolas.vbgh

The Coercion Saga의 일부 — AI가 품질 코드를 작성하도록 만들기.

불편한 상황

Backend tests pass. Frontend tests pass. Each side works in isolation. But do they work together?

  • Backend changes API. Frontend breaks. Nobody notices until production.
  • You rename a field from userName to username. Backend tests pass. Frontend tests pass—they mock the API anyway. Everything is green. You deploy. Production breaks because the frontend expects userName but the backend sends username.

This happens more often than anyone wants to admit.

목업의 문제점

프론트엔드 테스트는 API를 목업합니다. 반드시 그래야 합니다—모든 테스트에서 실제 백엔드를 실행할 수 없기 때문입니다. 하지만 목업은 거짓말을 합니다. 목업은 당신이 지정한 값을 반환할 뿐, 실제 백엔드가 반환하는 값을 반영하지 않습니다.

  • 백엔드가 변경됩니다. 목업은 변하지 않죠. 테스트는 통과하지만, 프로덕션에서는 실패합니다.

해결책: 양쪽 모두가 따라야 하는 단일 진실의 원천.

계약으로서의 OpenAPI

FastAPI는 타입 힌트에서 OpenAPI 스펙을 자동으로 생성합니다:

@router.post("/users", response_model=UserResponse)
async def create_user(user: UserCreate) -> User:
    ...

추가 작업이 필요 없습니다. 여러분의 타입이 스펙이 되고, 스펙이 계약이 됩니다.

{
  "paths": {
    "/users": {
      "post": {
        "requestBody": { "$ref": "#/components/schemas/UserCreate" },
        "responses": {
          "200": { "$ref": "#/components/schemas/UserResponse" }
        }
      }
    }
  }
}

이 파일은 저장소에 존재합니다. 양쪽 모두 이 파일과 일치해야 합니다.

Orval: 생성된 TypeScript 클라이언트

Orval은 OpenAPI 스펙을 읽어 TypeScript 코드를 생성합니다. 타입만이 아니라 HTTP 레이어가 내장된 전체 API 클라이언트를 만들어 줍니다.

설정 (orval.config.ts):

export default defineConfig({
  api: {
    input: '../shared/openapi.json',
    output: {
      target: './src/api/generated/endpoints.ts',
      schemas: './src/api/generated/models',
      client: 'react-query',
      mode: 'tags-split',
    },
  },
});

npx orval을 실행하면 다음과 같은 코드가 생성됩니다:

// Generated - don't edit
export const useCreateUser = (
  options?: UseMutationOptions
) => {
  return useMutation({
    mutationFn: (userCreate: UserCreate) => createUser(userCreate),
    ...options,
  });
};

export const createUser = (userCreate: UserCreate): Promise => {
  return customFetch({
    url: '/users',
    method: 'POST',
    data: userCreate,
  });
};

React Query 훅이 바로 사용 가능합니다. 뮤테이션, 쿼리, 캐시 무효화 패턴까지 모두 타입이 지정됩니다.

또한 다음과 같은 클라이언트도 생성할 수 있습니다:

  • Axios 클라이언트client: 'axios'
  • Fetch 클라이언트client: 'fetch'
  • SWR 훅client: 'swr'
  • Zod 스키마 – 컴파일 타임 타입 위에 런타임 검증을 제공

mode: 'tags-split' 옵션은 API 태그별로 파일을 하나씩 생성합니다. users.ts, products.ts, orders.ts와 같이 깔끔하게 구분되어 필요할 때만 임포트하면 됩니다.

수동으로 API 클라이언트를 만들 필요가 없습니다. 타입이 스펙과 항상 일치하므로 타입 드리프트가 발생하지 않습니다.

백엔드에서 userNameusername으로 바꾸면? 생성된 타입이 자동으로 변경됩니다. 프론트엔드에서 아직 옛 이름을 사용하고 있는 모든 곳에서 TypeScript가 오류를 표시하므로, 배포 전에 바로 수정할 수 있습니다.

워크플로우

  1. 백엔드가 API를 변경합니다.
  2. OpenAPI 스펙이 업데이트됩니다 (FastAPI가 자동으로 수행합니다).
  3. 프론트엔드에서 npm run codegen을 실행합니다.
  4. 생성된 타입이 업데이트됩니다.
  5. TypeScript가 파괴적인 변경을 감지합니다.
  6. 프론트엔드를 수정합니다.
  7. CI가 모든 것이 일치하는지 검증합니다.

조정 회의가 없습니다. “프론트엔드 업데이트했나요?” 같은 질문도 없습니다. 타입이 계약을 강제합니다.

Source:

게이트

두 가지 검증. 백엔드 스펙은 실제와 일치해야 합니다. 프론트엔드 타입은 스펙과 일치해야 합니다.

validate:openapi:
  stage: contract
  image: python:3.12-slim
  services:
    - name: postgres:16
      alias: db
  variables:
    DATABASE_URL: postgresql://postgres:postgres@db:5432/test
  before_script:
    - pip install uv && cd backend && uv sync --frozen
  script:
    - cd backend && uv run uvicorn app.main:app --host 0.0.0.0 --port 8000 &
    - sleep 5
    - curl -s http://localhost:8000/openapi.json > /tmp/live-spec.json
    - diff shared/openapi.json /tmp/live-spec.json
  allow_failure: false

validate:codegen:
  stage: contract
  image: node:lts-slim
  before_script:
    - cd frontend && npm ci --prefer-offline
  script:
    - npm run codegen
    - git diff --exit-code src/api/generated
  allow_failure: false
  • validate:openapi – 백엔드를 시작하고, 현재 스펙을 가져와 커밋된 스펙과 비교합니다. 차이가 있으면 CI가 실패합니다. 누군가 스펙을 업데이트하지 않은 채 API를 변경한 경우입니다.
  • validate:codegen – TypeScript 클라이언트를 재생성하고 커밋된 파일과 차이가 있는지 확인합니다. 차이가 있으면 CI가 실패합니다. 누군가 스펙을 업데이트했지만 클라이언트를 재생성하지 않은 경우입니다.

복사·붙여넣기·수정만 하면 됩니다. 작동합니다.

요점

백엔드와 프론트엔드는 서로 다른 언어를 사용합니다—여기서는 Python, 저쪽에서는 TypeScript. 계약이 없으면 서로 멀어지고, 작은 변경이 쌓여서 프로덕션이 깨집니다.

OpenAPI가 바로 그 계약입니다. 양쪽을 동기화하고, 파괴적인 변경을 조기에 포착하며, “내 환경에서는 동작한다”는 놀라움을 없애줍니다.

rval generates the types. CI validates the match.

Breaking changes are impossible to miss. Not "unlikely." Impossible. The pipeline catches them before they ship.

That's the deal.

**Next up:** [E2E Tests] – coming soon – The contract is enforced. Now test the real thing: a browser hitting the full stack.
0 조회
Back to Blog

관련 글

더 보기 »

Savior: 저수준 설계

Grinding Go: Low‑Level Design 인터뷰 준비와 문제 해결 능력 강화를 위해 다시 설계 단계로 돌아갔습니다. 소프트웨어 개발은 …

왜 0.1 + 0.2가 코드에서 0.3이 되지 않을까

markdown Floating‑Point Surprise python print 0.1 + 0.2 당신은 0.3이 나오길 기대합니다. 하지만 실제로는 0.30000000000000004가 나옵니다. 당신의 계산기는 0.3이라고 말하고, Excel도 0.3이라고 말합니다. Yo...