본문 바로가기

사이드 프로젝트 아카이빙

[React Query] 응답 처리 ‐ API 함수 vs Callback(onSuccess onError) 역할 분리

반응형

개요

#1. React Query Hook의 역할 정의 고민

React Query를 사용하다 보면 아래와 같은 고민을 가지게 됐었다.

응답(성공/실패) 처리 로직을 어디서 관리할 것인가?

 

프로젝트를 진행하면서 나는 queryFn/mutationFn와 같은 API 함수 내부에서 처리하도록 정의했다.

export function useMyHook() {
  return useQuery({
    queryKey: [...],
    queryFn: async () => {
      try {
        const response = await axios({
          method: 'GET',
          url: '/v1/mypath'
        });

        // == 성공 응답에 대한 별도 처리 로직 ==
        const result = () => {}

        return result;
      } catch (error) {
        // 명시된 코드일 경우 그대로 throw
        // 그 외의 경우 별도 처리
      }
    }
  });
}

 

반면, 팀원은 onSuccess/onError와 같은 Callback에서 처리하도록 정의했다.

export function useMyHook() {
  return useQuery({
    queryKey: [...],
    queryFn: async () => {
      const response = await axios({
        method: 'GET',
        url: '/v1/mypath'
      });

      return response.data;
    },
    onSuccess: () => {
      // 성공 응답 처리
    },
    onError: () => {
      // 실패 응답 처리
    }
  });
}

 

나와는 다른 방식으로 응답(성공/실패) 처리 로직을 작성한 팀원을 코드를 보며 해당 내용을 정리할 필요를 느꼈다.


내용

#1. React Query Hook의 역할 정의

React Query Hook의 기본 원칙을 살펴볼 필요가 있다 생각해 아래와 같이 가져왔다.

 

기본 원칙:
React Query Hook은 네트워크 요청 및 서버 상태 관리를 담당하고 UI 변화(알림, 라우팅 등)를 직접 처리하지 않는다.

 

기본 원칙을 토대로 역할을 정리하면 👇

관심사 책임 주체
API 요청 queryFn/mutationFn
응답에 대한 UI반응 onSuccess/onError/호출부
공통 에러 정책 axios interceptor, QueryClient

 

이렇게 분리해야 컴포넌트별로 동작을 자유롭게 변경할 수 있다고 한다.


#2. API 함수 내부 처리 vs Callback 처리

구분 API 함수 내부 처리 Callback 처리
재사용성 ❌ 화면 종속 ✅ 다양한 화면에서 활용 가능
코드 분리 ❌ UI 로직과 혼합 ✅ 관심사 명확하게 분리
테스트 용이성 ❌ 낮음 ✅ 높음
오류 정책화 ⚠️ 개별 처리 필요 ✅ 전역 핸들러 연계

 

결론:
API 함수 내부 즉, mutationFn과 queryFn은 "순수 함수"로 유지 응답(성공/실패)은 Callback 처리로 진행


#3. 구현 패턴 예시

결론을 토대로 결정을 내리면 응답(성공/실패)은 Callback으로 처리해야 한다.

이때 Callback을 정의해 응답을 처리하는 "역할"을 누구에게 위임할지에 따라서 크게 2가지 패턴을 생성할 수 있다.

 

[ 역할 위임 대상자 ]

  1. React Query
  2. 화면단

그렇다면 어떤 기준으로 응답 처리 역할 위임자를 정해야 할까?
여기에 대한 나의 답은 아래와 같다.

"공통" 처리와 "화면별" 처리로 분류해서 정한다.

 

이게 가능한 이유는 onSuccessonError 같은 Callback은 재정의 할 수 있기 때문이다.

 

화면단에서 React Query Hook을 인스턴스화 할 때 Callback을 재정의 해서 넘기기만 하면 된다.

이제 역할 위임자에 따른 간단한 구현 패턴을 차례대로 살펴보자.

 

[ 1. React Query 응답 처리 ]

// useLoginMutation.js
export const useLoginMutation = () => {
  useMutation({
    mutationFn: (payload) => api.login(payload),
    onSuccess: () => {
      alert('로그인 성공!');
      navigate('/home');
    },
    onError: () => {
      alert(err.message);
    }
  });
}
// LoginPage.jsx
const { mutate } = useLoginMutation();

mutate(data); // * 별도로 Callback을 재정의하지 않아 공통 처리 로직 이용 *

 

[ 2. 화면단 응답 처리 ]

// useLoginMutation.js
export const useLoginMutation = () => {
  useMutation({
    mutationFn: (payload) => api.login(payload),
    onSuccess: () => {},
    onError: () => {}
  });
}
// LoginPage.jsx
const { mutate } = useLoginMutation();

// * Callback을 재정의 하여 화면에 맞는 처리 로직 수행 *
mutate(data, {
  onSuccess: () => {
    alert('로그인 성공!');
    navigate('/home');
  },
  onError: (err) => {
    alert(err.message);
  }
});

 

※ 전역 에러 처리에는 Axios interceptor를 활용하는 방법도 있다.


#4. 실무 적용 팁

상황 전략
공통 처리(401, 500 등) interceptor, QueryClient
화면마다 UI 반응이 다른 경우 onSuccess/onError로 처리
테스트 코드 강화 mutationFn을 순수 함수로 유지
Retry 로직 필요 mutation 옵션(retry, retryDelay) 활용

정리

  • React Query Hook의 목적은 서버 상태 관리
  • UI 반응은 컴포넌트가 책임
  • 재사용성, 유지보수성을 위해 관심사 분리가 핵심
"Hook"은 API를 호출하는 수단이고 결과를 사용자에게 전달하는 책임은 화면이 진다."
반응형