머리말
일단 제가 아는 선(현재 프로젝트에서 적용한)에서의 내용만 정리해보겠습니다. 틀린 부분이 있다면 바로 알려주세요!
그리고 사용하시다가 새로운 옵션이나 새로운 내용, 더 좋은 사용법을 알게되었다면 이 문서에 작성해주세요 😁
소개
react-query는 서버의 상태를 관리하는 라이브러리 이다.
공식사이트에서 말하는 서버의 상태란 아래와 같다.
- 클라이언트에서 제어하지 않는 원격에서 관리되고 유지되는 데이터
- 데이터 Fetching, Updating에 비동기 API가 필요
- 다른 사용자들과 공유되는 상태이기 때문에 내가 모르는 사이에 변경될 수 있음
- 신경쓰지 않으면 잠재적으로 out of date가 될 수 있음
- Is persisted remotely in a location you do not control or own
- Requires asynchronous APIs for fetching and updating
- Implies shared ownership and can be changed by other people without your knowledge
- Can potentially become "out of date" in your applications if you're not careful
사용 목적
내가 생각하는 react-query의 사용 목적중 하나는 기존 redux를 사용할 때 비동기 처리를 하기위해선
redux-thunk
나 redux-saga
를 사용해야 했는데 많은 양의 보일러플레이트 코드가 필요 했는데, react-query
를 사용하면 보일러플레이트 코드의 크기가 상당히 작아진다! 밑에 있는 예시 코드를 보면 조금 더 와닿을 것이다.사용 방법
초기 설정
import { QueryClientProvider, QueryClient } from "react-query"; import theme from "./assets/theme"; const App = () => { const queryClient = new QueryClient({ defaultOptions: { queries: { refetchOnWindowFocus: false, }, }, }); return ( <QueryClientProvider client={queryClient}> {/* 사용할 컴포넌트 들 */} </QueryClientProvider> ); }; export default App;
초기 설정은 정말 간단하다. emotion의 ThemeProvider나 ContextAPI의 ContextProvider를 사용하는 것 처럼
QueryClientProvider
로 감싸주기만 하면 된다.우리 프로젝트에서 설정한 옵션인
refetchOnWindowFocus: false
(기본값은 true이다.)는 브라우저에 포커스가 될 때 마다 쿼리(API 요청 이라고 생각하면 된다.)가 발생하는데 이를 끄는 옵션이다. 이것은 데이터가 stale
한 상태가 아니라 fresh
한 상태로 유지하려는 특징이다. 예를 들어 사용자가 브라우저를 오랫동안 보지않고 다른일을 하고 있다가 다시 브라우저로 돌아 왔을 때 보이는 데이터는 시간이 오래 지났기 때문에
stale
한 데이터라고 볼 수 있다. 그래서 데이터를 fresh
하게 유지하기 위해 백그라운드에서 새로운 데이터 요청을 한다.위의 방법처럼 전역적으로 설정할 수도 있고 각각의 쿼리별로 disable할 수 있다.
이 설정을 false로 한 이유는 개발자도구를 켜놓고 개발을 하는데 이게 개발자도구를 사용하고 브라우저를 다시 클릭만 해도 API 요청이 돼서 일단은 꺼놨다. 팀원들과 협의 후 추후 설정이 필요하다.
기본 사용
일단 내가 사용했던 두 가지는
useQuery
와 useMutation
이다.- useQuery(queryKey, queryFunction, options) : 서버의 상태를 가져올 때 사용한다 (조회) 공식문서
- queryKey : 해당 쿼리의 유일해야 하는 키 값이다. 이 키 값으로 내부적으로 데이터를 캐시하고, 데이터를 자동으로 가져올 수 있게 하기 때문에 중요하다. 키는 문자열로 지정할 수도 있지만 다양한 형태로도 지정할 수 있다. (공식사이트) 예를 들어 어떤 리스트가 있고, 리스트 각각의 아이템을 조회할 때 그 아이템의 키 값을 활용하는 것 같다.
- queryFunction : 실질적으로 비동기 요청을 할 함수이다. 우리 프로젝트에서는 axios를 사용해서 비동기 요청하기 때문에 여러분들이 정의한 axios 함수를 import 하여 사용한다.
- options : 여러 옵션들을 설정할 수 있다.
- onSuccess : 해당 쿼리가 성공했을 때 콜백
- onError : 해당 쿼리가 실패했을 때 콜백,
그리고 여기서 아예 서버로부터 응답을 받을 수 없는 에러가 발생했을 경우 response가 undefined로 오는 경우가 있더라구요 그래서 response에 대해 undefined 처리를 한번 해줬는데 이렇게 처리하는게 맞는지 아직 확신이 안섭니다!
- retry : 쿼리가 실패하면 react-query는 자동으로 쿼리 요청을 3번 진행한다.
- 타입스크립트 타입 지정 : 이 부분은 vscode에서 직접 useQuery의 타입을 확인해보고 작성했다. 아래 내용은 추측한 것이기 때문에 잘못된 내용이 있으면 바로 수정할 예정!
- unknown : 쿼리를 요청할 함수의 타입이다. unknown인 이유는 어떠한 타입의 함수가 들어올 지 예상할 수 없어서 라고 생각했다.
- QueryError : 쿼리에서 에러가 발생했을 때 받을 데이터의 타입이다. 아래 코드에선 onError 콜백의 인자로 넘어오는 데이터의 타입
- QueryData : 쿼리가 성공했을 때 받을 데이터의 타입이다. 아래 코드에선 onSuccess 콜백의 인자로 넘어오는 데이터의 타입
- string : 쿼리 key의 타입
// 프로젝트에서 사용한 방법 const profileQuery = useQuery<unknown, QueryError, QueryData, string>( "myProfile", () => requestGetMyProfile(), { onSuccess: ({ data }) => { formik.setValues({ ...data.data.user, ...data.data.introduction }); }, onError: ({ response }) => { const errorMessage = response ? response.data.message : login.message.UNKNOWN_ERROR; if ( response === undefined || response?.status === errorCode.UNAUTHORIZED || response?.status === errorCode.FORBIDDEN ) { setToken(""); } // TODO: 에러가 발생할 경우 Toast를 띄워 사용자에게 알려준다. // eslint-disable-next-line alert(errorMessage); }, retry: false, } ); // 이런식으로 에러여부, 로딩여부, 데이터, 에러를 구조분해 할당으로 가져올 수 있음 const { isLoading, isError, data, error } = useQuery('todos', fetchTodoList);
// QueryData, QueryError의 타입 // 쿼리마다 응답받는 데이터의 형태가 다를 수 있기 때문에 각자 설정 필요 export interface QueryData { data: { data: UserData; statusCode: number; }; } export interface QueryError { response: { status: number; data: { message: string | null; }; }; }
- useMutation(mutationFunction, options): 서버의 상태를 변경할 때 사용한다 (생성, 수정, 삭제) 공식문서 (전체적으로 useQuery와 사용법, 형태가 비슷하다.)
- mutationFunction : 비동기 요청할 함수이다.
- options : useQuery와 비슷한 옵션들을 설정할 수 있다.
- onSuccess : 해당 뮤테이션이 성공했을 때 콜백
- onError : 해당 뮤테이션이 실패했을 때 콜백
- 타입스크립트 타입 지정 : useQuery와 마찬가지로 vscode에서 직접 확인하여 작성했기 때문에 틀린 부분이 있다면 바로 수정할 예정!
- MutationData : 뮤테이션이 성공했을 때의 데이터 타입. onSuccess 콜백의 인자로 넘어오는 데이터
- MutationError : 뮤테이션이 실패했을 때의 데이터 타입. onError 콜백의 인자로 넘어오는 데이터
- unknown : mutationFunction의 인자로 넘어오는 데이터의 타입
- unknown : context의 타입 (react-query안에서 context라는 것을 사용할 수 있는데 아직 정확한 내용은 모르겠음)
const { mutate } = useMutation<MutationData, MutationError, unknown, unknown>( (values: FormValues) => requestSignup(values), { onSuccess: () => { alert(signup.message.COMPLETED_SIGNUP); history.push(routes.LOGIN); }, onError: ({ response }) => { const errorMessage = response ? response.data.message : signup.message.UNKNOWN_ERROR; alert(errorMessage); }, } );
// MutationData , MutationError 의 타입 // Mutation은 데이터의 변경이기 때문에 response 데이터의 타입이 비슷할 것이라 생각되어 commonTypes.ts 에 정의했습니다. // 하지만 데이터의 형태가 다르게 오는 경우가 있다면 별도로 정의해주시면 될 것 같음 export interface MutationData { data: { data: { [key: string]: string | null; }; statusCode: number; }; } export interface MutationError { response: { status: number; data: { message: string; }; }; }
마무리
일단 저도 깊게 학습하지 않아서 잘 쓰고 있는 것인지는 모르겠지만 쓰다가 더 좋은 방법이 있으면 차차 적용해 갈 계획입니다.
또한, 틀린 내용이 있을 수도 있으니 제가 쓴 글을 너무 맹신하는 것은 위험할 수도 있어요! 같이 학습하면서 누구나 맹신할 수 있을 정도의 글을 같이 작성했으면 좋겠습니다!!
여러분들도 하시다가 좋은 방법이 있다면 바로 알려주시면 너무 좋을 것 같아요
추가 내용
- 제가 위에
response
가undefined
로 오는 경우가 있다고 했는데 그 부분에 대한 내용인 것 같습니다. - 추가적인 의문사항 : 401이나 403에러는 axios interceptor에서 확인하지 못하나요?? 제가 링크 걸었던 깃헙 이슈를 보면
error.response
가 없을 경우 network error라고 하는데... 흠
// 403 에러가 발생했을 때 axiosInstance.interceptors.response.use( (res) => res, (error) => { console.log(error); console.log(error.response); // undefined console.log(error.data); // undefined console.log(error.status); // undefined return Promise.reject(error); } );


개발자도구의 네트워크 탭에선 403 상태코드를 확인할 수 있어요 😇


