useQuery (선언적)를 쓴다면 react-query에서 제공하는 여러 기능들 (ex. refetchOnWindowFocus, enabled)이 "알아서" 돌아가지만,
useMutation (명령적)의 경우는 이런 기능들이 "알아서" 돌아가지 않습니다.
이유는 useQuery의 경우 선언하는 시점에 observer가 생성되지만 useMutation은 mutate을 사용하기 전까지 observer가 생성되지 않습니다. 또한 useMutation의 observer는 mutate가 돌아갈때마다 이전 observer를 unsubscribe 합니다.
아시다시피 useMutation은 CUD을 위한 훅이고 useMutation으로 GET 요청을 하는 것은 설계 용도를 벗어나는 사용법이 아닌가 싶습니다. 우선 1번과 2번의 차이는 1번의 경우 param이 바뀔때 서버로 요청을 1번만 보내게 됩니다. 2번의 경우는 2번 보냅니다. mutate를 할때 fetchList로 한번 보내고 이후 "someList" 키가 invalidate 되면서 useQuery가 한번 더 fetchList를 호출합니다. 이론적으로 useQuery는 여러 컴포넌트에서 호출해도 같은 값을 공유하지만, useMutation의 경우는 값을 공유하지 않는 것으로 알고 있습니다. 다만, mutation의 상태를 저장/관리하기 위해 MutationCache라는 곳에 mutation 자체와 관련 데이터를 따로 저장하는데요. 이걸 감안한다면 2번 방법은 메모리도 더 많이 쓰는 방법이 되는 것 같네요.
정리를 하자면:
- 1번 방법이 일반적인 방법인 것 같습니다. query key에 page, keywords를 넣어서 관리해도 무방해요.
- 2번 방법이 메모리 차원에서 더 비효율적입니다
- 2번 방법은 네트워크 요청을 2번 보냅니다
- 2번 방법은 훅이 설계된 의도대로 사용되지 않아서, 버그를 발생 시킬 수 있습니다
- (사견) 2번 방법은 코드 의도를 파악하기 쉽지 않은 것 같습니다
- (사견) page, keyword 조합마다 쿼리가 저장될텐데, 캐싱이 꼭 필요한건지/유용하게 사용할 수 있는지도 고민이 필요한거 같습니다
1번 방식은 useQuery hook을 사용하여 새로운 page 또는 keyword가 전달되면 자동으로 API를 호출하여 데이터를 업데이트 합니다. 즉, React Query의 기능만으로도 데이터를 재조회할 수 있습니다. onSuccess 옵션을 사용하여 API 호출에 대한 처리를 추가할 수 있습니다.
2번 방식은 useMutation과 useEffect를 사용하여 데이터를 재조회합니다. useMutation을 사용하여 API를 호출하고, onSuccess 옵션을 사용하여 API 호출에 대한 처리를 추가합니다. useEffect를 사용하여 페이지나 검색어가 변경될 때마다 useMutation을 호출하고, invalidateQueries를 사용하여 데이터를 다시 조회합니다.
React Query는 애플리케이션의 데이터를 관리하고 동기화하는데 사용 되는 라이브러리로 최근 많은 개발자들에게 인기를 얻고 있는 상태관리 라이브러리 입니다. 리액트 쿼리를 사용하는 이유는 크게 다음과 같습니다.
1) 간편한 데이터 관리
데이터 가져오기, 캐싱, 동기화 및 업데이트 처리를 간편하게 할 수있게 해줍니다.
2) 실시간 업데이트 및 동기화
실시간 데이터 업데이트와 자동 동기화를 지원하여 서버와 클라이언트 데이터의 일관성을 유지합니다.
3) 데이터 캐싱
데이터를 캐싱하여 불필요한 API 요청을 줄이고 애플리케이션의 성능을 향상 시킵니다.
4) 서버 상태 관리
서버 상태 관리 (예를들면 로딩중, 에러, 성공 등의 상태)를 간편하게 처리할 수 있습니다.
5) 간편한 설정
React Query는 간단한 설정으로 사용할 수 있습니다.
리액트 쿼리를 사용하기 전에 먼저 @tanstack/react-query와 devtools 를 설치해줍니다.
(devtools 는 선택사항이며 devtools을 사용하면 패칭한 데이터를 쉽게 관리할 수 있습니다.)
최상위 컴포넌트 위에 QueryClientProvider로 감싸줘야 합니다.
(devtools는 최상위 컴포넌트에 최대한 가까운 위치로 배치해주시면 됩니다.)
const root = ReactDOM.createRoot(
document.getElementById('root') as HTMLElement,
);
const store = configureStore();
const queryClient = new QueryClient({
defaultOptions: {
queries: {
refetchOnWindowFocus: false, // refetch 조건 막기
retry: 0, // 요청 실패했을 경우 재요청 횟수
},
},
});
root.render(
<Provider store={store}>
<QueryClientProvider client={queryClient}>
<ReactQueryDevtools initialIsOpen={true} />
<App />
</QueryClientProvider>
</Provider>,
);
devtools를 설치하게 되면 화면 하단에 꽃모양의 아이콘이 생기게 됩니다.
클릭해보면 현재 불러오고 있는 데이터들이 무엇이 있는지 한눈에 보며 관리할 수 있습니다.
정리
react-query의 장점에는 데이터를 캐시해서 전달하는 것이 있다. 쉽게 말해서react-query가 자체적으로 서버 데이터를 저장하고 있다가 api 요청 없이 해당 데이터를 보여준다는 것이다.
이 부분에서react-query는stale과fresh라는 개념을 사용하는데, 간단하게 음식을 생각하면 좋다.
1.유저가react-query한테 국밥(data)를 요청 2-1.react-query는 캐시에 신선한(fresh)국밥이 있으면 그대로 줌 2-2. 캐시에 상한(stale)국밥이나, 국밥 자체가 없으면서버에 국밥(data) 받아와서 줌
isLoading이랑 isFetching의 차이
isLoading은 서버에 데이터 요청을 처음 할 때
isFetching은 서버에 데이터 요청을 다시 할 때 (캐시된 데이터가 있을 때)
isFetching
isFetching은 어떠한react-query요청 내부의비동기 함수가 처리되었는지 여부에 따라true/false로 나누어 진다.
isLoading
isLoading은캐시된 데이터조차 없이,처음 실행된 쿼리일 때 로딩 여부에 따라true/false로 나누어 진다.
즉, 결론적으로isLoading과isFetching은 비슷하게'로딩'이라는 개념을 사용하지만기존에 캐시된 데이터가 있느냐에 따라 다르다.
간단하게 생각하자면isLoading은 어떤 데이터를 처음 가져올 때 사용하면 되고, isFetching은 데이터를 다시 가져와야 할 때 사용하면 된다.
국밥을 처음 가져올때는숟가락, 젓가락, 물도 가져다줘야 하지만, (isLoading) 다시 한번 주문하면국밥만 가져다준다. (isFetching)
이처럼두 경우에 있어서 작업의 차이가 필요할 때 사용한다고 보면 되겠다.
정리하면
isLoading은 React Query에서 쿼리 수준에서 사용되는 속성이고 특정 쿼리가 현재 데이터를 가져오고 있는지 여부를 나타내고 특정 쿼리에 대한 로드 상태를 조건부로 처리할 수 있다. isFetching은 React Query에서 전역 수준에서 사용할 수 있는 메서드이다. 애플리케이션의 쿼리가 현재 데이터를 가져오고 있는지 여부를 나타내고. 여러 쿼리에서 가져오는 데이터의 전체 로드 상태를 결정하는 방법을 제공한다.
상태에 변화가 생기면, 이전에 컴포넌트가 스토어한테 구독 할 때 전달해줬었던 함수 listener 가 호출된다. 이를 통하여 컴포넌트는 새로운 상태를 받게되고, 이에 따라 컴포넌트는 리렌더링을 한다.
redux의 원칙
Single Source of truth
동일한 데이터는 항상 같은 곳에서 가져옵니다.
즉, 스토어라는 하나뿐인 데이터 공간이 있다는 의미입니다.
State is read-only
리액트에서는 setState 메소드를 활용해야만 상태 변경이 가능합니다.
리덕스에서도 액션이라는 객체를 통해서만 상태를 변경할 수 있습니다.
Changes are made with pure functions
변경은 순수함수로만 가능합니다.
이는 리듀서와 연관되는 개념입니다.
Store - Action - Reducer
store 만들기
1. Provider 컴포넌트를 적용한다. Provider는 필수적으로 store를 지정해줘야 한다.
store란 이름 그대로 상태 정보를 객체로 정리해서 관리한다.
ConfigureStoreOptions 타입(인터페이스)이며, 파라미터로 reducer, middleware, devTools, reloadedState, enhancers 를 갖는다. 이중 리듀서 외에는 모두 옵셔널하다. 아래에서 더 디테일하게 다룰거지만 우선 redux toolkit에서 제공하는configureStore메서드를 사용하면 된다.const store = configureStore({ reducer: myReducerus }); 이 다음에 리덕스의 구성요소를 하나하나 뜯어보며 새로 선언해서 구성해보자
// ./src/index.tsx
import { Provider } from 'react-redux';
root.render(
{/* myReducer는 아직 없다. 새로 선언해야 한다 */}
const store = configureStore({ reducer: myReducer });
{/* 이 store는 임시, 따로 분리해서 새로 선언할 것이다 */}
return (
<Provider store={store}>
<main>
{/* 여기에 컴포넌트 추가^^ */}
</main>
</Provider>
);
2. store를 분리해서 따로 구현하기 전에 먼저 Reducer, type state, Action 을 만든다.
1. Redux Store가 저장할 상태 정보(State), Redux Store는 내 상태 정보를 저장한다. 2. 그리고 새 상태를 반환하는 함수인 리듀서(Reducer)를 사용한다.즉, 전역적으로 상태를 관리할 데이터 컨테이너인 store를 만들 것이며, 여기서 관리할 상태 데이터 타입을 선언하고, 상태 데이터를 새로 바꾸기 위한 리듀서를 만들어야 한다. 리듀서는 현재 state와 Action을 변수로 받아서 새로운 state를 반환한다.
src/store/configureStore.ts
import { legacy_createStore as createStore } from 'redux';
import rootReducer from './reducers';
const configureStore = (): any => createStore(rootReducer);
export default configureStore;
RootReducer 정의
여러 reducer을 사용하는 경우 reducer을 하나로 묶어주는 메소드
src/store/reducer.ts
import { combineReducers } from 'redux';
import main from '../redux/reducers/mainReducer';
import app from '../redux/reducers/appReducer';
import login from '../redux/reducers/loginReducer';
import tenant from '../redux/reducers/tenantReducer';
import mybenefit from '../redux/reducers/myBenefitReducer';
import receipt from '../redux/reducers/receiptReducer';
import snowpoint from '../redux/reducers/snowPointReducer';
import user from '../redux/reducers/userReducer';
const rootReducer = combineReducers({
main,
app,
tenant,
login,
mybenefit,
receipt,
snowpoint,
user,
});
export default rootReducer;
export type RootState = ReturnType<typeof rootReducer>;
import configureStore from './store/configureStore';
const store = configureStore();
root.render(
<Provider store={store}>
<App />
</Provider>,
);
이 코드에서 Provider는 최상위 컴포넌트(App)를 감싸고, 그 하위 컴포넌트들이 Redux 스토어에 접근할 수 있도록 해줍니다.
1. configureStore()
설명: configureStore는 Redux Toolkit에서 제공하는 함수로, Redux 스토어를 설정하는 데 사용됩니다. 이 함수는 기본적으로 여러 가지 설정을 간편하게 해주며, 미들웨어나 Redux DevTools를 자동으로 설정합니다.
주요 특징:
간편한 설정: redux-thunk와 같은 미들웨어가 기본적으로 포함되어 있어 비동기 액션을 쉽게 처리할 수 있습니다.
Redux DevTools: 브라우저의 Redux DevTools 확장을 자동으로 활성화해 상태 변경을 시각적으로 확인 가능.
미들웨어 설정: 추가적인 미들웨어를 설정하거나 기본 설정을 재정의할 수 있습니다.
2. <Provider store={store}>
설명: Provider는 react-redux 라이브러리에서 제공하는 컴포넌트로, React 애플리케이션에 Redux 스토어를 주입하는 역할을 합니다. store는 configureStore로 생성된 Redux 스토어입니다.
주요 역할:
Redux 스토어를 React 컴포넌트에 전달: React 애플리케이션의 모든 하위 컴포넌트들이 Redux 스토어에 접근할 수 있도록 해줍니다.
Context API 사용: Provider는 내부적으로 Context API를 사용하여 하위 컴포넌트에서 useSelector와 useDispatch를 통해 스토어의 상태를 읽고, 액션을 디스패치할 수 있게 합니다.
./store/configureStore.ts
import { legacy_createStore as createStore } from 'redux';
import rootReducer from './reducers';
const configureStore = (): any => createStore(rootReducer);
export default configureStore;
이 코드는 Redux의 스토어를 설정하는 함수 configureStore를 정의한 것으로, 전역 상태 관리를 위해 createStore를 사용하여 Redux 스토어를 생성하는 역할을 합니다.
1. import { legacy_createStore as createStore } from 'redux';
설명: legacy_createStore는 Redux Toolkit에서 제공하는 함수로, 기존의 createStore를 대체하는 방식입니다. redux의 최신 버전에서는 configureStore를 사용하는 것이 권장되지만, 이 코드에서는 기존 방식인 createStore를 사용하고 있습니다. legacy_createStore로 불리는 이유는 Redux Toolkit이 권장되면서 기존의 createStore가 레거시로 간주되기 때문입니다.
주요 역할: Redux 스토어를 생성하고 관리하는 핵심 함수로, 애플리케이션의 상태 트리를 관리합니다.
2. import rootReducer from './reducers';
설명: rootReducer는 애플리케이션에서 사용하는 리듀서(reducer)들의 집합입니다. Redux의 리듀서는 상태와 액션을 기반으로 새로운 상태를 반환하는 순수 함수입니다.
rootReducer의 역할: 여러 개의 리듀서를 combineReducers로 결합하여 전체 애플리케이션의 상태를 관리합니다. rootReducer는 이를 종합하여 Redux 스토어에서 사용할 수 있는 형태로 제공됩니다.
리듀서(reducer)들의 집합
./store/reducers.ts
import { combineReducers } from 'redux';
import main from '../redux/reducers/mainReducer';
.
.
.
.
.
const rootReducer = combineReducers({
main,
app,
tenant,
login,
mybenefit,
receipt,
snowpoint,
user,
});
export default rootReducer;
export type RootState = ReturnType<typeof rootReducer>;
rootReducer: 이 코드는 Redux의 combineReducers를 사용하여 여러 개의 리듀서를 결합한 것이며, rootReducer는 애플리케이션의 전체 상태 트리를 관리하는 함수가 됩니다. 리듀서들은 상태를 관리하는 역할을 하므로, rootReducer가 반환하는 것은 애플리케이션의 전체 상태입니다.
typeof rootReducer: TypeScript에서 typeof를 사용하면, 해당 변수나 함수의 타입을 가져올 수 있습니다. 여기서는 rootReducer 함수의 타입을 가져옵니다.
결과: ReturnType<typeof rootReducer>는 rootReducer 함수가 반환하는 전체 상태 트리의 타입을 추론합니다. 즉, RootState 타입은 rootReducer에서 반환되는 상태 객체의 구조를 기반으로 자동으로 추론됩니다. 이제 이 RootState 타입을 사용하면 Redux의 상태에 접근할 때 타입 검사를 통해 오류를 방지할 수 있습니다.
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
const queryClient = new QueryClient({
defaultOptions: {
queries: {
refetchOnWindowFocus: false, // refetch 조건 막기
retry: 0, // 요청 실패했을 경우 재요청 횟수
},
},
});
QueryClient
설명: QueryClient는 React Query의 핵심 객체로, 서버 데이터를 가져오고 관리하는 역할을 합니다. 모든 쿼리는 이 QueryClient 객체를 통해 관리되며, 다양한 옵션을 설정할 수 있습니다.
defaultOptions
설명: defaultOptions는 모든 쿼리에서 공통으로 적용될 기본 옵션들을 설정하는 부분입니다. 여기에서 쿼리 관련 기본 동작을 정의할 수 있습니다.
queries
설명: queries는 모든 쿼리에 적용되는 기본 설정을 정의합니다. 이 설정은 개별 쿼리에서 별도로 정의하지 않는 한 전역으로 적용됩니다.
1. refetchOnWindowFocus: false
설명: 브라우저 창에 포커스가 다시 돌아왔을 때 데이터를 다시 가져오는 동작을 막는 설정입니다.
기본 동작: React Query는 기본적으로 사용자가 브라우저 탭이나 창에 다시 포커스를 맞출 때(다시 활성화될 때) 자동으로 데이터를 다시 가져옵니다. 그러나 이 옵션을 false로 설정하면 이러한 자동 갱신을 방지합니다.
사용 이유: 애플리케이션에서 빈번하게 서버 데이터를 다시 불러올 필요가 없거나, 사용자가 창을 전환해도 데이터를 다시 가져오는 것이 불필요한 경우.
2. retry: 0
설명: 서버 요청이 실패했을 때 재시도할 횟수를 설정합니다. 여기서는 retry: 0으로 설정되어 있으므로, 요청이 실패하면 재시도를 하지 않고 즉시 실패로 처리됩니다.
기본 동작: React Query는 기본적으로 서버 요청이 실패했을 때 3번까지 자동으로 재시도합니다. 하지만 이 설정을 통해 재시도를 하지 않도록 설정한 것입니다.
사용 이유: 서버 요청 실패 시 재시도를 하지 않고, 즉각적으로 에러를 처리하고 싶을 때 사용됩니다. 예를 들어, 네트워크 트래픽을 줄이거나, 사용자가 즉시 실패를 인식해야 하는 경우에 유용합니다.
전체적으로
이 설정은 창이 다시 활성화되어도 데이터를 다시 가져오지 않으며, 서버 요청 실패 시 추가 재시도를 하지 않도록 구성된 QueryClient를 생성하는 예시입니다. 이는 사용자가 데이터를 자주 다시 불러오지 않아도 되고, 서버 요청 실패 시 자동 재시도가 필요 없는 경우에 적합한 설정입니다.