BE 응답 래퍼 언래핑 패턴 — API 200인데 왜 에러?
📚 React 프론트엔드 삽질기 시리즈 (9편)
API가 200 OK를 반환하는데 프론트엔드에서 에러 페이지가 뜬다면, BE 응답 래퍼 구조를 의심하세요. useCustom + ApiWrapper 패턴으로 2중 언래핑하는 실전 해결법을 정리합니다.
API가 200 OK를 반환한다. Network 탭을 보면 데이터도 잘 온다. 그런데 화면에는 에러 페이지가 뜬다.
이 괴현상의 범인은 BE 응답 래퍼였다. 래퍼 안의 래퍼, 2중 구조를 모르면 한 시간을 날린다.
🔍 증상: 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중 구조

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중 언래핑

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 탭은 거짓말을 하지 않는다 — 코드를 짜기 전에 실제 응답 구조부터 확인하자.
📚 React 프론트엔드 삽질기 시리즈 (9편)
- 1. Vite 6.x 프록시에서 PATCH만 CORS 에러? 소문자 메서드 함정과 해결법
- 2. React Admin DataProvider 커스터마이징 삽질기
- 3. BE 응답 래퍼 언래핑 패턴 — API 200인데 왜 에러?
- 4. React useEffect 비동기 cleanup이 GPU를 죽이는 과정 — Pixi.js RenderTexture 실종 사건
- 5. shadcn init 실행했더니 프라이머리 컬러가 검정으로 — CSS 변수 덮어쓰기 트러블슈팅
- 6. Framer Motion whileInView 애니메이션이 스크린샷에서 사라지는 이유와 해결법
- 7. react-hook-form + Zod 연동에서 겪는 실전 함정 6가지 — 에러가 안 뜨는 이유부터 타입 불일치까지
- 8. Refine useCustom config.query가 정수를 보장하지 않는 함정 — 타입은 number인데 왜 400이야?
- 9. 패키지 설치 후 Invalid hook call? Vite 캐시 무효화가 답이다