본문으로 건너뛰기

API 계약 소비

본 문서는 서버 API의 타입을 얻는 방법, 그 타입을 어느 계층까지 노출하는지, 계약이 변경될 때 어떻게 대응하는지를 정의합니다.

1. 타입은 생성물입니다

서버 응답과 요청의 타입은 OpenAPI 명세에서 생성하며 손으로 작성하는 것을 금지합니다.

손으로 작성하면 서버가 계약을 바꿔도 컴파일이 통과합니다. 불일치가 런타임에, 그것도 해당 화면을 열어 본 사람에게만 드러납니다. 생성 방식은 이 실패를 빌드 시점으로 옮깁니다.

도구 openapi-typescript
생성 대상 타입만 생성합니다
생성물 위치 shared/api/generated/
저장소 포함 커밋에 포함합니다

HTTP 호출 코드를 생성하는 도구(orval, ng-openapi-gen)는 사용하지 않습니다. HttpClient 기반 서비스를 만들어 내는데, 조회 수단으로 httpResource를 사용하기로 한 결정과 역할이 겹칩니다.

1.1 생성물을 커밋하는 이유

항목 커밋 포함 빌드 시 생성
계약 변경의 가시성 PR diff에 그대로 드러납니다 드러나지 않습니다
빌드 재현성 백엔드 서버 없이 빌드됩니다 CI가 백엔드 가용성에 종속됩니다
갱신 시점 명시적입니다 암묵적이며 어느 빌드에서 바뀌었는지 추적이 어렵습니다

계약 변경이 코드 리뷰에 노출되는 것이 가장 큰 이득입니다. 필드가 사라진 것을 병합 전에 발견합니다.

1.2 생성물 수정 금지

shared/api/generated/ 아래 파일의 직접 수정은 금지합니다. 다음 생성에서 소실되며, 소실된 사실을 아무도 알아채지 못합니다.

린터는 파일 편집 자체를 막을 수 없으므로 강제 수단은 CI의 재생성 검사입니다. 파이프라인에서 타입을 다시 생성한 뒤 git diff --exit-code로 차이를 확인하고, 차이가 있으면 실패시킵니다. 손으로 고친 내용은 재생성 결과와 달라지므로 검출됩니다.

eslint.config.jsignores에도 이 경로를 등재합니다. 생성물에 대한 린트 지적은 고칠 수 없는 지적이라 잡음이 됩니다.

생성 결과가 잘못되었다면 고칠 대상은 파일이 아니라 OpenAPI 명세이며, 백엔드에 수정을 요청합니다.

2. 생성 타입의 노출 범위

생성된 타입을 모든 상위 계층에서 그대로 사용합니다. 화면 전용 모델로 변환하는 계층을 두지 않습니다.

// 허용 — pages가 생성 타입을 직접 사용합니다
import type { AssessmentResponse } from '@/shared/api';

// 금지 — 필드 대응만 하는 변환 계층
function toAssessmentView(dto: AssessmentResponse): AssessmentView { ... }

2.1 선택 근거와 대가

변환 계층을 두면 백엔드 변경이 매핑 함수에서 멈추고 화면 어휘를 독립적으로 유지할 수 있습니다. 그럼에도 두지 않는 이유는 백엔드가 필드를 자주 추가한다는 전제아키텍처 2절에 기재되어 있기 때문입니다. 필드 하나가 추가될 때마다 매핑을 고쳐야 하며, 그 작업은 값을 옮겨 담기만 할 뿐 판단을 담지 않습니다.

대가는 명확합니다. 백엔드 리팩터링이 화면 코드까지 전파되고, 서버 어휘로 된 타입명이 화면 코드에 그대로 드러납니다. 이 대가를 감수하는 대신 매핑 유지 비용을 없앱니다.

2.2 예외

화면에서 계산이 필요한 파생 값은 변환이 아니라 계산이며 금지 대상이 아닙니다. 해당 슬라이스의 model 세그먼트에 둡니다.

// pages/risk-assessment-detail/model/grade.ts
export function calculateGrade(a: AssessmentResponse): 'high' | 'medium' | 'low' { ... }

원본 타입을 그대로 두고 필요한 값을 계산하는 것과, 타입 전체를 다른 모양으로 옮겨 담는 것은 다릅니다. 전자는 허용하고 후자는 금지합니다.

3. 요청 함수

생성된 것은 타입뿐이므로 요청 함수는 직접 작성합니다.

대상 위치
한 화면 전용 조회 pages/<화면>/api/
여러 화면 공용 조회 entities/<도메인>/api/
도메인 무관 CRUD 헬퍼 shared/api/
HTTP 클라이언트 설정과 인터셉터 shared/api/

엔드포인트 경로 문자열은 호출부에 직접 기재하지 않고 shared/api/endpoints.ts에 모읍니다. 경로가 흩어지면 API 버전이 올라갈 때 검색으로 찾아야 합니다.

4. 계약 변경 대응

4.1 갱신 절차

  1. 백엔드가 배포한 OpenAPI 명세를 받아 타입을 재생성합니다.
  2. 컴파일 오류가 발생한 지점을 확인합니다. 이것이 영향 범위입니다.
  3. 필드 삭제나 타입 변경이면 화면 코드를 수정합니다.
  4. 필드 추가면 대개 수정할 것이 없습니다.
  5. 재생성 결과와 화면 수정을 같은 커밋에 담습니다.

생성물과 그 소비 코드를 분리해 커밋하면 중간 커밋이 컴파일되지 않습니다.

4.2 변경 유형별 비용

변경 유형 프론트엔드 영향 대응
응답 필드 추가 없음 재생성만
선택 필드 추가 없음 재생성만
응답 필드 삭제 컴파일 오류 사용처 전부 수정. 백엔드와 사전 협의 필수
필드 타입 변경 컴파일 오류 사용처 수정
필드명 변경 컴파일 오류 삭제와 추가가 동시에 일어난 것으로 취급
엔드포인트 경로 변경 런타임 실패 endpoints.ts 수정. 컴파일로 검출되지 않으므로 주의

마지막 항목이 유일하게 컴파일로 잡히지 않는 변경입니다. 경로가 문자열이기 때문이며, 이 때문에 경로를 한 곳에 모읍니다.

5. 에러 응답

에러 응답의 타입도 OpenAPI 명세에 포함되어 있으면 생성 대상입니다. 없으면 shared/api/에 공통 에러 타입을 정의합니다.

에러 응답의 해석과 표시는 본 문서의 범위가 아니며 예외 · 에러 표시 · 로깅이 원본입니다.

6. 명세가 없는 백엔드

OpenAPI 명세를 제공하지 않는 백엔드와 연동해야 하는 경우, 명세 발행을 먼저 요청합니다. Spring Boot는 springdoc으로, NestJS는 @nestjs/swagger로 코드에서 명세를 생성할 수 있으므로 백엔드 측 작업량이 크지 않습니다.

요청이 수용되지 않아 타입을 손으로 작성하게 되는 경우, 그 프로젝트는 본 표준에서 이탈한 것이므로 프로젝트 저장소의 ADR로 사유와 범위를 기록합니다. 표준 문서를 조건부로 완화하지 않습니다.

7. 금지 사항

금지 사유
서버 응답 타입을 손으로 작성 계약 위반이 컴파일을 통과합니다
shared/api/generated/ 직접 수정 다음 생성에서 소실됩니다
필드 대응만 하는 변환 계층 작성 판단을 담지 않는 코드를 유지하게 됩니다
엔드포인트 경로를 호출부에 직접 기재 변경 시 검색으로 찾아야 합니다
any로 응답 타입 회피 생성 방식을 택한 이유가 사라집니다
© 2026 dev.goraebap