
이번에 플젝 세팅을 진행하면서, React-Query를 사용하기로 했다!
TS환경에서 ReactQuery를 사용하면서 있었던 어려움과, 내가 결과적으로 어떤 방식으로 코드를 작성했는지를 기록으로 남기고자 한다.
아래 내용은 절대 정답이 아니며, 그저 ‘나는 이렇게 했다!’입니다.
내용에 오류가 있다면 댓글을 통해 알려주세요. 저도 더 공부해보도록 하겠습니다.
npm i @tanstack/react-query
API 요청에는 axios를 사용하였다.
npm i axios
이번 글은 react-query를 중점적으로 다루기 위해, axios 기본 설정에 대한 내용은 생략하도록 하겠다.
api요청에 대한 파일을 src/apis폴더에 작성하도록 구분하였다.
apis 폴더에서도 api 엔드포인트를 기준으로 파일을 작성하기로 하였다.
그 이유는 2가지가 있다.
api/도메인으로 나누었는데, 프론트에서 같은 형식을 유지하는 것이 좋을 것이라 생각했다.그래서 현재 테스트용으로 만들어주신 API를 나누어본다면 다음과 같다.

📂apis
├─📂health
│ ├─ health.d.ts
│ ├─ health.keys.ts
│ └─ health.ts
└─📂regist
각각의 폴더는 .d.ts .keys.ts .ts 파일을 가진다. 각 용도는 다음과 같다.
.d.ts : api 요청에서 사용하는 request, response 타입을 관리.keys.ts : querykey를 관리.ts : api를 요청하는 axios와 useQuery, useMutation을 관리.d.ts 를 @types 에서 관리할 수도 있겠지만, 같은 폴더로 묶음으로서 더 찾기 쉽고, React의 Props를 제외한 request와 response만 관리하고자 하였다.
세팅을 하면서 [React Query 잘 사용하기 - #3. 폴더 구조] 이 글을 많이 참고했는데, 어떻게 썼는지, 장단점이 확실히 적혀있어 좋았다. 기준을 바탕으로 팀원과 함께 어떻게 설정할지 결정하였다.
useQuery와 useMutation을 Custom Hook으로 만들었다.
Custom Hook으로 만들었을 때의 이점은 유지보수성, 가독성, 재사용성에 좋다.
useQuery는 기본적인 GET요청을 위해 사용한다.
react-query V3과 V4,V5에서의 사용 방법이 다르다. 인터넷 서칭에는 아직 V3의 자료가 많으니 유의하도록 하자. 되도록 공식문서를 참고하는 것이 좋다.
const info = useQuery('todos', fetchTodoList)//V3
const info = useQuery({ queryKey: ['todos'], queryFn: fetchTodoList })//V4,V5
axios요청 함수와 hook은 어떤 api인지 빠르게 확인할 수 있도록 바로 위 아래에서 관리하도록 했다.
querykey는 .keys.ts에서 불러와 사용했는데, 뒤에서 더 다루도록 하겠다.
/* 헬스 체크 (GET)
테스트 api
useQuery는 GET함수에서만 사용된다.
*/
async function getHealthData() {
const response = await axiosInstance.get<APIResponse<HealthTestResponse>>('/health');
return response.data.data;
}
export const useGetHealthQuery = () => {
const query = useQuery<HealthTestResponse>({
queryKey: healthKeys.info(),
queryFn: getHealthData,
staleTime: 10 * 1000, // 10초
gcTime: 30 * 1000, // 30초
// enabled: false,
});
return { healthQuery: query };
};
위 코드에서 useQuery의 반환값으로 객체 자체를 return하였지만, 이런식으로 query에서 사용할 것들만 export 하는 방법도 있다.
return {
healthData: query.data,
healthIsError: query.isError,
healthError: query.error,
healthIsSuccess: query.isSuccess,
...
};
개인적으로는, isError, isSuccess와 같이 반복되는 state를 나타낼 때에는 이름이 비슷할 수 밖에 없고, 이를 위해 매번 이름을 고민하고 있는 것 보다는, 그 상위 객체로 이름을 구분지으면 가독성이 좋을 것이라 생각하였다.
이 코드에서 신경쓴 부분은 useGetHealthQuery , healthQuery 처럼 hook의 이름을 설정하는 부분이다. 아직 test코드밖에 작성하지 않았지만, 이 부분에서 잘 구분될 수 있도록 이름을 명명하는 것이 이후 import하여 사용할 때에도 가독성 있을 것이다. 따라서 팀원과 명명규칙을 따로 정해두었다.
mutation도 query와 같은 방법으로 작성하였다.
참고로 mutation에서 사용할 request변수는 hook의 파라미터로 작성하지 않는다. mutate()에서 작성하도록 한다.
/* 헬스체크 (POST)
테스트 api
useMutation은 POST, PUT, PATCH, DELETE에서 사용된다.
*/
async function postHealthData(data: HealthRequest): Promise<HealthResponse> {
const response = await axiosInstance.post<APIResponse<HealthResponse>>('/health', data);
return response.data.data;
}
export function useHealthMutation() {
const queryClient = useQueryClient();
const mutation = useMutation({
// mutation에는 querykey를 작성하지 않는다.
mutationFn: (data: HealthRequest) => postHealthData(data),
onSuccess: (newData) => {
// ['health', 'detail']인 캐시 업데이트
queryClient.setQueryData(healthKeys.detail(), newData);
},
onError: () => {},
});
return { healthMutation: mutation };
}
이렇게 작성한 Hook은 간단하게 사용할 수 있다.
여러가지 상태 값을 필요에 따라 사용하면, 원하는대로 코드를 작성할 수 있을 것이다.
// Get 요청 방법
const GetTest = () => {
const { healthQuery } = useGetHealthQuery();
if (healthQuery.isLoading) {
return <div>Loading...</div>;
}
if (healthQuery.isError) {
return <div>Error: {healthQuery.error.message}</div>;
}
return (
<>
{healthQuery.isFetching && <p>fetch 요청중...</p>}
<p>TEST GET 요청 결과 : {healthQuery.data?.isHealthCheck.toString()}</p>
<button
onClick={() => {
healthQuery.refetch();
}}
>
GET 요청 다시 보내기
</button>
</>
);
};
// POST 요청 방법
const PostTest = () => {
const { healthMutation } = useHealthMutation();
const handleSubmit = () => {
const updateData: HealthRequest = {
name: '테스트',
input: '테스트 input',
};
healthMutation.mutate(updateData);
};
return (
<div>
<p>POST TEST</p>
<button onClick={() => handleSubmit()}>POST 보내기</button>
{healthMutation.isPending && <p>요청 전송중...</p>}
{healthMutation.isError && <p>Error: {healthMutation.error?.message}</p>}
{healthMutation.isSuccess && <p>POST Success 결과 : {healthMutation.data?.input}</p>}
</div>
);
};
queryKey는 react-query에서 캐싱을 관리하는 기준이다.
key는 string으로 해쉬되어 관리된다. undefined값이 들어가면 없는것으로 간주되니, 같은 해쉬가 생성되지 않도록 유의해야 한다.
따라서, key들이 중복되지 않도록 한 파일에서 관리하도록 하였다.
또한 이렇게 작성하게 된다면, useMutation의 onSuccess에서 캐싱을 관리할 때에도 더욱 읽기 쉬운 코드를 작성할 수 있을 것이다.
export const healthKeys = {
all: ['health'] as const,
info: () => [...healthKeys.all, 'info'] as const,
detail: () => [...healthKeys.all, 'detail'] as const,
list: (page: number) => [...healthKeys.all, 'list', page] as const, //예시
};
queryKey에 대한 내용은 [4. 효율적으로 React Query Key 선언하기] 이 블로그에 더 자세히 설명되어 있다.
개인적으로 react-query를 도입해보고싶어 시작하였지만, 쓰는 방법을 찾는 것도 어려웠고, 어떤게 잘 쓰는 방법인지도 아직 잘 모르겠다. 혹시 나처럼 react-query를 시도해보려는 분이 계시다면, 아래 Reference들을 참고하시고, 조금이라도 도움이 되었길 바란다..
Reference
reactQuery 공식문서
React Custom Hook 디자인 패턴: Query/Mutation 훅 분리
1. 실용적인 React Query
React Query 잘 사용하기 - #3. 폴더 구조
효율적으로 React Query Key 선언하기
4. 효율적으로 React Query Key 선언하기