본문 바로가기
Develop

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

by 너드나무 2025. 11. 10.
반응형

서론

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
반응형