Develop

[fetchBaseQuery] RTK Query PATCH 요청 시 body wrapper를 피하는 올바른 방식

너드나무 2025. 11. 10. 11:16
반응형

서론

Redux Toolkit Query(RTK Query)를 활용해 API를 호출하다 보면, 종종 요청 본문 구조가 서버와 일치하지 않아 예상치 못한 오류가 발생하는 경우가 있습니다.

그중 자주 마주치는 이슈 중 하나가 바로 "body wrapper" 문제입니다 — 즉, 본문에 포함되면 안 되는 키가 들어가거나, 데이터 구조가 불필요하게 중첩되는 현상이죠.

이번 글에서는 그 원인 중 하나인 body 구조의 불일치 문제, 특히 JSON.stringify()의 오용으로 인해 서버가 기대하는 본문 구조와 달라지는 사례를 다룹니다.

서버 요구 사항

  • 서버는 다음과 같은 요청 구조를 기대합니다.
// 서버가 정의한 타입
interface ContentUpdateRequest {
  id: number; // URL 경로에 사용됨
  data: Record<string, any>; // 요청 본문(body)
}
  • 희망 JSON body (id는 URL 경로(/api/content/:id/)에만 사용되며, body에는 절대 포함되어선 안 됩니다.)
{
  "data": {
    "title": "콘텐츠 제목",
    "description": "설명",
    "fields": [...],
    "entries": [...]
  }
}

잘못된 구현: body를 수동으로 stringify

  1. 이 코드에서는 body를 문자열로 직접 직렬화(JSON.stringify)하고 있습니다. 이로 인해 다음과 같은 문제가 생깁니다
updateContent: builder.mutation<
  ContentUpdateResponse,
  ContentUpdateRequest
>({
  query: ({ id, ...body }) => ({
    url: `/api/content/${id}/`,
    method: "PATCH",
    body: JSON.stringify(body), // ❌ stringify는 필요 없음
  }),
}),
문제 설명
Content-Type 누락 자동 설정되지 않아 서버가 text/plain으로 인식
자동 직렬화 무력화 RTK Query의 fetchBaseQuery가 JSON임을 감지하지 못함
서버 파싱 실패 서버가 예상한 data 객체를 파싱하지 못하거나 무시함

잘못된 구현: 구조 분해(...body) 없이 body 전달

  • 이 코드에서는 body를 구조 분해(...) 없이 직접 전달하고 있습니다. 이로 인해 다음과 같은 문제가 생깁니다
updateContent: builder.mutation<
  ContentUpdateResponse,
  ContentUpdateRequest
>({
  query: ({ id, body }) => ({
    url: `/api/content/${id}/`,
    method: "PATCH",
    body: body, // ❌ 구조분해 없음
  }),
}),


// 서버 전달 시 호출 형태
updateContent({
  id: 123,
  body: {
    data: { ... }
  }
});

→ 본문의 루트 키가 body로 감싸진 중첩 구조가 만들어집니다 ❌
{
  "body": {
    "data": {
      "title": "...",
      ...
    }
  }
}

올바른 구현

  1. id는 구조 분해로 추출되어 URL에만 사용됩니다.
  2. 나머지 모든 필드(data 등)는 body 객체로 본문에 직렬화 없이 전달됩니다.
  3. RTK Query의 fetchBaseQuery는 이 객체를 자동으로 JSON.stringify하고, Content-Type: application/json도 자동 설정합니다.
// mutation 정의
updateContent: builder.mutation<
  ContentUpdateResponse,
  ContentUpdateRequest
>({
  query: ({ id, ...body }) => ({
    url: `/api/content/${id}/`,
    method: "PATCH",
    body, // ✅ 객체 그대로 전달
  }),
});

// 호출부 예시
const handleSubmit = async () => {
  if (!title.trim()) return showError("제목을 입력해주세요.");
  if (!rcmRows || rcmRows.length === 0) {
    return showError("RCM 파일을 업로드해주세요.");
  }

  const body = {
    title,
    description,
    columns: header,
    records: rcmRows,
  };

  try {
    await updateRcm({ id: contentId, ...body }).unwrap(); // ✅ id는 path param, body는 JSON
    success("RCM이 성공적으로 수정되었습니다.");
    navigate(`/rcm`);
  } catch (err) {
    showError("수정 중 오류가 발생했습니다.");
  }
};

정리: 구조 분해는 단순한 문법이 아니다.

체크 항목 설명
id는 URL 전용 body에 포함시키지 않는다
data는 JSON root로 서버 명세와 일치하게 구성
JSON.stringify() 직접 호출 X RTK Query가 자동 처리함
구조 분해(...)는 필수 id와 body를 명확히 분리하기 위해

728x90
반응형