Tori의 개발 공부

[Next.js] HTTP 응답 구조를 표준화한 이유와 설계 전략 본문

WEB/Next.js

[Next.js] HTTP 응답 구조를 표준화한 이유와 설계 전략

B_Tori 2026. 2. 19. 14:14

API 응답은 단순한 데이터 반환이 아니라 프론트와의 계약이다.

 

1. 왜 응답 구조를 표준화해야 할까?

초기에는 Route Handler에서 다음처럼 직접 응답을 반환했습니다.

return NextResponse.json({ error: '에러 발생' }, { status: 500 })

return NextResponse.json(
  { success: true, data: groupData },
  { status: 201 },
)

겉보기엔 문제 없어 보이지만, 프로젝트가 커질수록 다음 문제가 발생할 위험이 있었습니다:

  • 응답 구조가 API마다 달라질 위험
  • 에러 필드 구조가 통일되지 않음
  • status 코드가 분산 관리됨
  • 추후 meta, requestId 추가 시 전체 수정 필요
  • 프론트 처리 로직이 복잡해짐

 

2. 설계 목표

응답 구조화를 위해 다음과 같은 목표를 가지고 설계했습니다: 

  • 모든 API 응답을 동일한 형태로 유지
  • 성공/실패 분기 명확화
  • 상태코드 관리 중앙화
  • 향후 확장 가능성 확보

 

3. 해결 전략: Envelope 패턴 도입

응답 타입 정의

export type ApiSuccess<T> = {
  success: true
  data: T
}

export type ApiError = {
  success: false
  error: {
    message: string
    code?: string
    details?: unknown
  }
}

export type ApiResponse<T> = ApiSuccess<T> | ApiError

 

모든 API는 반드시 이 두 구조 중 하나만 반환합니다.

 

이 구조의 핵심은 다음과 같습니다.

 

  1. 성공과 실패를 명확히 분리한다.
    success: true | false를 기준으로 항상 분기할 수 있기 때문에 프론트엔드 로직 단순화 가능
  2. 응답을 “데이터”가 아니라 “계약”으로 정의한다.
    API는 단순히 값을 반환하는 함수가 아니라, 클라이언트와 약속된 형식을 전달하는 인터페이스 역할
  3. 확장 가능성을 확보한다.
    meta, requestId, timestamp 등의 필드를 추가해야 할 경우, 이 타입 정의만 수정하면 전체 API 계약이 일관되게 유지 가능
  4. 클라이언트 타입 안정성을 확보한다.
    프론트에서는 ApiResponse<T>를 기준으로 응답을 처리할 수 있으며, 잘못된 접근(예: 실패 응답에서 data 접근)을 컴파일 단계에서 방지 가능

 

4. Response Helper 추상화

export function ok<T>(data: T, status = 200) {
  return NextResponse.json({ success: true, data }, { status })
}

export function fail(
  message: string,
  status = 500,
  opts?: { code?: string; details?: unknown },
) {
  return NextResponse.json(
    {
      success: false,
      error: {
        message,
        code: opts?.code,
        details: opts?.details,
      },
    },
    { status },
  )
}

 

그리고 자주 쓰는 상태코드는 별칭으로 제공했습니다.

export const created = <T,>(data: T) => ok(data, 201)
export const badRequest = (message: string) => fail(message, 400)
export const unauthorized = (message = '인증이 필요합니다.') => fail(message, 401)
export const forbidden = (message = '권한이 없습니다.') => fail(message, 403)
export const notFound = (message = '대상을 찾을 수 없습니다.') => fail(message, 404)

 

5. 결과

1️⃣  코드 가독성 개선

return created(groupData)

 

API 라우터 반환에서 반환값의 의도가 명확해졌습니다.

2️⃣  유지보수성 향상

  • 응답 구조 변경 시 한 파일만 수정
  • 에러 정책 일괄 적용 가능
  • meta, requestId 확장 용이

3️⃣  프론트 로직 단순화

if (!res.success) {
  showToast(res.error.message)
}

이후 UX 처리 로직이 통일시킬 수 있습니다.

 

마무리

작은 추상화처럼 보이지만, API 응답 표준화는 프로젝트의 확장성과 일관성을 지탱하는 중요한 설계 요소입니다.

응답 구조를 정리하는 일은 단순한 리팩토링이 아니라, 시스템의 계약을 정립하는 작업이라는 생각이 들었습니다.