[TYPESCRIPT] 백엔드 인터페이스 설계 기준: 프론트엔드와 계약을 안정적으로 유지하는 방법

 

인증, 권한, 에러 처리, 데이터 접근까지 하나씩 정리하고 나면 결국 모든 흐름이 만나는 지점이 드러납니다. 바로 “프론트엔드와 백엔드가 어떻게 약속하고 통신하는가”입니다.

 

실무에서는 이런 문제가 반복됩니다.

  • 백엔드 수정 하나로 프론트 화면 여러 곳이 깨진다
  • 응답 형식이 바뀌었는데, 누가 책임져야 하는지 애매하다
  • 에러 코드가 제각각이라 공통 처리 로직을 만들 수 없다
  • 문서는 있는데, 실제 동작과 맞지 않는다

 

이 문제의 본질은 기술 선택이 아니라 인터페이스를 계약으로 다루지 않았다는 점에 있습니다. 백엔드 인터페이스를 “구현 결과”가 아니라 “운영 계약”으로 설계하는 기준을 정리합니다.

 

개념/배경 설명: 인터페이스는 왜 깨지는가

많은 팀에서 인터페이스는 이런 식으로 관리됩니다.

  • 일단 동작하게 만든다
  • 프론트에서 맞춰서 쓴다
  • 문제가 생기면 그때 고친다

 

이 방식은 초기에는 빠르지만, 운영 단계에 들어가면 비용이 급격히 증가합니다. 특히 다음 상황에서 문제가 커집니다.

  • 프론트와 백엔드 배포 주기가 다른 경우
  • 웹/모바일/어드민 등 클라이언트가 여러 개인 경우
  • 기능 확장보다 안정성이 중요한 시점

 

그래서 실무에서는 인터페이스를 “내부 구현”이 아니라 깨지면 안 되는 경계로 다룹니다.

 

핵심 설계 1: 응답 형식을 반드시 고정한다

백엔드 인터페이스에서 가장 먼저 고정해야 할 것은 “응답의 모양”입니다. 성공과 실패가 섞여 있으면, 프론트에서는 항상 예외 처리를 놓치게 됩니다.

 


// 공통 응답 계약
export type ApiResponse<T> =
  | { ok: true; data: T }
  | { ok: false; error: { code: string; message?: string } };

 

이 구조의 핵심은 단순합니다.

  • 성공과 실패를 구조적으로 분리한다
  • 프론트에서 분기 처리를 강제한다
  • HTTP status와 비즈니스 에러를 구분한다

 

실무 포인트 정리

  • 응답 구조는 절대 화면별로 다르게 만들지 않는다
  • 에러는 항상 같은 위치에서 내려온다
  • 성공/실패 판단은 ok 하나로 끝난다

 

핵심 설계 2: 에러 코드는 “의미 단위”로 정의한다

에러 메시지를 문자열로만 내려보내면, 프론트에서는 조건 분기가 불가능해집니다. 그래서 에러 코드는 반드시 의미 중심으로 정의해야 합니다.

 


export type ErrorCode =
  | 'UNAUTHORIZED'
  | 'FORBIDDEN'
  | 'SESSION_EXPIRED'
  | 'VALIDATION_FAILED'
  | 'RESOURCE_NOT_FOUND';

 

이렇게 하면 프론트에서는 문자열 비교가 아니라 정책 기반 분기가 가능합니다.

 

실무 포인트 정리

  • 에러 코드는 늘리기 쉽고, 줄이기 어렵다
  • 처음부터 과도하게 만들 필요는 없다
  • “분기 기준으로 쓸 수 있는가”를 기준으로 정한다

 

핵심 설계 3: 백엔드 인터페이스는 TypeScript로 계약을 고정한다

문서만으로 인터페이스를 관리하면 언젠가 코드와 어긋나게 됩니다. 그래서 실무에서는 타입을 계약의 기준으로 삼습니다.

 


// shared/types/api.ts
export type GetProfileResponse = ApiResponse<{
  userId: string;
  name: string;
  email: string;
}>;

 

이 타입을 기준으로

  • 백엔드는 이 형태로만 응답해야 하고
  • 프론트는 이 형태만 신뢰한다

 

이렇게 하면 인터페이스 변경은 곧바로 컴파일 에러로 드러납니다. 운영 사고 대신 개발 단계에서 깨지게 만드는 구조입니다.

 

핵심 설계 4: 변경 가능성은 “확장 필드”로 흡수한다

모든 변경을 breaking change 없이 처리할 수는 없습니다. 하지만 자주 바뀌는 영역은 처음부터 여지를 두는 것이 좋습니다.

 


type ListMeta = {
  total?: number;
  cursor?: string;
};

type ListResponse<T> = ApiResponse<{
  items: T[];
  meta?: ListMeta;
}>;

 

이 구조를 쓰면

  • 초기에는 meta 없이 단순하게 시작하고
  • 나중에 페이징/커서가 필요해져도 계약을 유지할 수 있습니다

 

운영/실무에서 자주 겪는 문제

  • 응답 구조가 API마다 달라 프론트 공통 로직이 없는 경우
  • 에러 메시지 변경이 곧 기능 수정이 되는 구조
  • 백엔드 수정 후 프론트가 조용히 깨지는 문제
  • 문서와 실제 응답이 어긋난 상태로 방치되는 경우

 

실무 권장 체크리스트

  • 모든 API 응답이 동일한 기본 구조를 사용하는가
  • 에러 코드를 분기 기준으로 사용할 수 있는가
  • 인터페이스 타입이 코드로 관리되고 있는가
  • 자주 바뀌는 영역에 확장 여지가 있는가
  • 프론트/백엔드가 같은 계약을 기준으로 개발하는가

 

백엔드 인터페이스 설계의 핵심은 “지금 편한 구현”이 아니라 나중에 깨지지 않는 계약입니다.

 

응답 구조를 고정하고, 에러를 의미로 정의하고, 타입을 계약으로 사용하면 프론트와 백엔드는 더 이상 눈치 싸움을 하지 않아도 됩니다. 결국 운영이 편해지는 시스템은 인터페이스부터 다릅니다.