개요
#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가지 패턴을 생성할 수 있다.
[ 역할 위임 대상자 ]
- React Query
- 화면단
그렇다면 어떤 기준으로 응답 처리 역할 위임자를 정해야 할까?
여기에 대한 나의 답은 아래와 같다.
"공통" 처리와 "화면별" 처리로 분류해서 정한다.
이게 가능한 이유는 onSuccess와 onError 같은 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를 호출하는 수단이고 결과를 사용자에게 전달하는 책임은 화면이 진다."
'사이드 프로젝트 아카이빙' 카테고리의 다른 글
| [Identicon library] 랜덤 프로필 이미지 생성하기 (0) | 2025.11.23 |
|---|---|
| [HTTPS] Vite HTTPS 개발 환경용 OpenSSL 인증서 발급 가이드 (0) | 2025.11.23 |
| [HTTPS] Vite 개발 환경에서 HTTPS 적용하기 with Proxy (0) | 2025.11.23 |
| [배포] Cloudflare Pages 배포에 인증 추가하기 (0) | 2025.11.23 |
| [Image Upload]이미지 리사이징(Resizing) with pica lib (0) | 2025.11.23 |