BE 응답 래퍼 언래핑 패턴 — API 200인데 왜 에러?

API가 200 OK를 반환하는데 프론트엔드에서 에러 페이지가 뜬다면, BE 응답 래퍼 구조를 의심하세요. useCustom + ApiWrapper 패턴으로 2중 언래핑하는 실전 해결법을 정리합니다.


API가 200 OK를 반환한다. Network 탭을 보면 데이터도 잘 온다. 그런데 화면에는 에러 페이지가 뜬다.

이 괴현상의 범인은 BE 응답 래퍼였다. 래퍼 안의 래퍼, 2중 구조를 모르면 한 시간을 날린다.


🔍 증상: 200 OK인데 undefined

🔍 증상: 200 OK인데 undefined 하다가 버그를 마주친 순간
🔍 증상: 200 OK인데 undefined 하다가 버그를 마주친 순간

리포트 페이지를 만들고 있었다. 토큰 기반으로 리포트 데이터를 조회하는 간단한 GET API다.

API를 호출하면 응답이 잘 온다. 상태 코드 200, body도 있다. 그런데 컴포넌트에서 데이터를 꺼내면 undefined다.

const { data: apiResponse } = useCustom({
  url: `/report/${token}`,
  method: "get",
});

// 데이터 접근
const valid = apiResponse?.data?.valid;
console.log(valid); // undefined 😱

분명히 API 응답에 valid: true가 있는 걸 Network 탭에서 확인했는데, 코드에서는 잡히지 않는다.


🔎 원인: BE 응답 래퍼의 2중 구조

🔎 원인: BE 응답 래퍼의 2중 구조 코드를 한 줄씩 따라가는 중
🔎 원인: BE 응답 래퍼의 2중 구조 코드를 한 줄씩 따라가는 중

Chrome DevTools Network 탭에서 실제 응답을 자세히 들여다봤다.

{
  "success": true,
  "data": {
    "valid": true,
    "reportData": {
      "score": 85,
      "summary": "..."
    }
  },
  "message": "OK"
}

여기서 함정이 있다. BE는 모든 응답을 { success, data, message } 래퍼로 감싼다. 이건 일반적인 NestJS 인터셉터 패턴이다.

그런데 useCustom의 반환값도 { data } 구조다. 즉:

apiResponse.data     → BE 응답 전체 { success, data, message }
apiResponse.data.data → 실제 데이터 { valid, reportData }

2중 .data가 필요한 것이었다. apiResponse?.data?.valid로 접근하면 래퍼의 data 안에서 valid를 찾는 게 아니라, 래퍼 자체에서 valid를 찾게 된다. 당연히 undefined다.


✅ 해결: ApiWrapper 타입 + 2중 언래핑

✅ 해결: ApiWrapper 타입 + 2중 언래핑 수정 완료, 이제 좀 살 것 같다
✅ 해결: ApiWrapper 타입 + 2중 언래핑 수정 완료, 이제 좀 살 것 같다

1단계: 응답 래퍼 타입 정의

interface ApiWrapper<T> {
  success: boolean;
  data: T;
  message: string;
}

BE의 표준 응답 구조를 타입으로 정의한다. 모든 API가 이 래퍼를 쓰므로, 한 번 만들어두면 재사용된다.

2단계: useCustom에 제네릭 적용

interface ReportData {
  valid: boolean;
  reportData: {
    score: number;
    summary: string;
  };
}

const { data: apiResponse } = useCustom<ApiWrapper<ReportData>>({
  url: `/report/${token}`,
  method: "get",
});

3단계: 2중 언래핑으로 데이터 접근

// ❌ 잘못된 접근 (래퍼를 무시)
const valid = apiResponse?.data?.valid; // undefined

// ✅ 올바른 접근 (2중 언래핑)
const reportData = apiResponse?.data?.data;
const valid = reportData?.valid; // true ✅
const score = reportData?.reportData?.score; // 85 ✅

🛡️ 예방: 래퍼 구조를 팀/프로젝트 표준으로

🛡️ 예방: 래퍼 구조를 팀/프로젝트 표준으로 재발 방지를 위한 안전장치
🛡️ 예방: 래퍼 구조를 팀/프로젝트 표준으로 재발 방지를 위한 안전장치

DataProvider에서 래퍼 자동 해제

매번 .data.data를 쓰는 건 실수를 부른다. DataProvider 레벨에서 래퍼를 자동으로 벗겨주는 게 좋다.

// dataProvider 커스터마이징
const customDataProvider = {
  ...defaultDataProvider,
  custom: async ({ url, method, payload }) => {
    const response = await httpClient[method](url, payload);
    // 래퍼 자동 해제
    return {
      data: response.data.data, // BE 래퍼의 data를 바로 꺼냄
    };
  },
};

이렇게 하면 컴포넌트에서는 apiResponse?.data만으로 실제 데이터에 접근할 수 있다.

BE 응답 구조 문서화

## API 응답 표준
모든 API는 아래 래퍼로 감싸서 반환합니다.
{ success: boolean, data: T, message: string }

FE에서 접근 시:
- useCustom: apiResponse.data.data
- DataProvider 커스텀 후: apiResponse.data

이런 문서가 있고 없고의 차이는 크다. 새 팀원이 합류했을 때, 혹은 3개월 뒤의 내가 다시 볼 때.


📝 정리

드디어 📝 정리 문제를 잡았다
드디어 📝 정리 문제를 잡았다

항목내용
증상API 200인데 FE에서 데이터 undefined
원인BE 응답 래퍼 { success, data, message } 구조 미파악
해결ApiWrapper<T> 타입 + apiResponse.data.data 2중 언래핑
예방DataProvider 레벨 래퍼 자동 해제 + BE 응답 구조 문서화
소요약 1시간 (Network 탭에서 구조 파악 후 5분 만에 해결)

BE 응답 래퍼는 일관성을 위한 좋은 패턴이다. 다만 FE에서 그 구조를 정확히 알고 있어야 한다. Chrome DevTools Network 탭은 거짓말을 하지 않는다 — 코드를 짜기 전에 실제 응답 구조부터 확인하자.