Home [Redux][Query] About Redux ToolKit Query
Post
Cancel

[Redux][Query] About Redux ToolKit Query

현재 만들고 있는 프로젝트의 모든 서버와의 통신은 RTK Query를 통해 제어하고 있다. (한두개를 제외하고는)

api 통신을 하면서 받아오는 데이터를 바로바로 Redux에 저장할 수 있었고, 따로 actions 라는 boilerplate 를 만들어 주지 않아도 되어서 매우 편했다.

RTK Query 를 작성하면서 있었던 일을 좀 나열해 보고자 한다.

기존 상태관리를 위해서, vanilla redux 를 사용해왔었다. 실제로 사용해보면 상태관리가 매우 용이하여 props drilling 같은 불편한 상황은 발생하지 않았다. 하지만 redux 를 구현하기 위해 boilerplate 가 매우 많았다는 단점이 있었다.

이후로는 asyncThunk 로 api를 다뤘다. 기존에 설정한 axios를 통해 서버와 교류했으며, 로딩상태(done, loading, error) 를 관리할 수 있어서 좋았다. 하지만 thunk 또한 사용하기 위해 미리 작성해야 할 점들이 너무나도 많았다.

AsyncThunk 의 문제점

다음은 AsyncThunk 를 사용한 서버와의 통신이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// actions 폴더에 만든 AsyncThunk

export const GetStoreInfo = createAsyncThunk<
  GetStoreInfoResponse,
  GetStoreInfoRequest,
  {
    dispatch: AppDispatch;
  }
>("Web/StoreInfo", async (data, { getState, dispatch, rejectWithValue }) => {
  try {
    const response = await setupAxios(getState(), dispatch).get(
      `/Web/StoreInfo?storeUrl=${data.storeUrl}`
    );
    return response.data as GetStoreInfoResponse;
  } catch (error) {
    return rejectWithValue(error.message);
  }
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
// reducer 폴더

// redux 에 저장할 기본상태 initialState
export  const  initialState  = {
isExpiredToken:  false,
accessToken:  null,
companyInfo:  null,
storeInfo:  null,
storeIDValidateStatus:  false,
WaitConfirmUserTotalCount:  0,
WaitConfirmUserLists: [],

GetStoreInfoLoading:  false,
GetStoreInfoDone:  false,
GetStoreInfoError:  null,

GetFindWmsIDLoading:  false,
GetFindWmsIDDone:  false,
GetFindWmsIDError:  null,

GetStoreIDValidateStatusLoading:  false,
GetStoreIDValidateStatusDone:  false,
GetStoreIDValidateStatusError:  null,

PostSignUpLoading:  false,
PostSignUpDone:  false,
PostSignUp

export  const  userSlice  =  createSlice({
name:  'user',
initialState,
// 우리가 흔히 알고있는,dispatch 를 통해 상태를 업데이트 하는 reducer 이다.
// dispatch(userSlice.actions.signOut());  userSlice 에서  액션중 signout() 을 하여 dispatch 해주는 명령어다.
reducers: {
signOut(state) {
reactCookies.remove('refreshToken', { path:  '/' });
reactCookies.remove('omsID', { path:  '/' });
localStorage.clear();
router.replace('/');
state.isExpiredToken  =  true;
},
setAccessToken(state, action) {
state.accessToken  = action.payload;
localStorage.setItem('AccessToken', state.accessToken);
},
},
// api 통신을 통해서 상태값을 업데이트 시켜줄 때는 extraReducers 에 저장한다.
// 통신의 결과값을 initialState 에 하나하나 저장해 주어야 한다.
// 상태는 pending, fulfilled, rejected 가 있다.
extraReducers: (builder) => {

builder
.addCase(GetStoreIDValidateStatus.pending, (state) => {
state.GetStoreIDValidateStatusLoading  =  true;
state.GetStoreIDValidateStatusDone  =  false;
state.GetStoreIDValidateStatusError  =  null;
})
.addCase(GetStoreIDValidateStatus.fulfilled, (state, action) => {
state.GetStoreIDValidateStatusLoading  =  false;
state.GetStoreIDValidateStatusDone  =  true;
state.storeIDValidateStatus  = action.payload;
})
.addCase(GetStoreIDValidateStatus.rejected, (state, action) => {
state.GetStoreIDValidateStatusLoading  =  false;
state.GetStoreIDValidateStatusError  = action.payload;
})
  • asyncThunk 를 사용하면 api 통신을 하고 가져오는 값을 state 로 저장할 수 있다.
  • 하지만 pending, fulfilled, rejected 각각을 상태값으로 저장해야 하며, 그에대한 케이스도 작성해야 되어서 매우 불편하다.
  • 기존 redux 보다는 나아졌지만, 여전히 작성해야 할게 너무 많다…

Query 가 나오게 된 배경

asyncThunk 로도 제어가 가능했지만 , 위와같은 불편함을 해결하고자 redux 에서는 ReduxToolkit Query 가 나오게 되었다.

  • UI 스피너를 표시하기 위해 로드 상태 추적
  • 동일한 데이터에 대한 중복 요청 방지
  • UI가 더 빠르게 느껴지도록 낙관적인 업데이트
  • 사용자가 UI와 상호 작용할 때 캐시 수명 관리

따라서 query 의 특징이라고도 볼 수 있는 위 네가지를 좀더 살펴보고자 한다. 실제로도, 내가 프로젝트에 사용했을 때 매~우 편리해졌다고 느꼈다. vanilla redux의 사용법을 까먹을 정도로 말이다.

Query 에 대한 예제

api 작성하기(services 폴더)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
export  const  masterApi  = rtkApi.injectEndpoints({ // masterApi 를 다음과 같이 정의한다.
endpoints: (build) => ({
// 해당통신의 이름을 정하고, build.query 한다. <> 안에는 request 와 response 의 타입이 정의되어 있다.
getItemsShorts: build.query<GetProductMasterListShortsResponse, GetRequestProps>({
// 쿼리안의 argument. 즉 data 는 request 의 props 다.
query: (data) => ({
url:  `wms/api/management/custcds/${data.custCd}/items-shorts?display=${data.display}&startIndex=${data.startIndex}`,
}),
// post 의 경우 조금 다르다. mutation 돌연변이라는 단어를 사용하고, props 보다는 payload 에 담아보낼 body의 타입을 정의해준다.
postProductMaster: build.mutation<WmsProductMaster, WmsProductMaster>({
query: (body) => ({
url:  `wms/api/management/custcds/NA01/items/${body.itemCd}`,
// get 요청과 다르게 method 를 추가해서 구분한다.
method:  'POST',
body,
}),
}),
}),

// 마지막으로 해당 쿼리를 export 시켜준다. 아래와 같이 use가 붙고, get 요청의 경우 query 가 붙어 자동으로 이름을 생성해준다.
export  const {
useGetItemsShortsQuery,
// post 의 경우에는 api 이름뒤에 Mutation 이 붙는다.
usePostProductMasterMutation,
} = masterApi;
  • 여기까지는 services 라는 폴더안에 만든 api 통신들이다.
  • api 통신요청 할 때의 request props 와 response 의 데이터 타입을 미리 정의해두어서, 프로젝트 진행시 잘못된 값 기입 즉 휴먼에러를 방지할 수 있어서 좋았다.
  • 여기서 export 한 useGetItemShortsQuery 같은 것들은, 추후 api 요청시 실행시킬 수 있는 명령어다.

Reducer 폴더에 작성하기 (initialState, actions 등)

initialState 상태 초기값 설정하기

1
2
3
4
5
6
7
const initialState = {
  // 상품 마스터 요약본 목록들
  productMasterLists: [] as ProductMasterShortList[],
  // 상품 마스터 상세내용
  productMasterDetail: {} as WmsProductMaster,
  totalCount: 0,
};
  • TS 답게 미리 정의해둔 타입을 정해두면 , lists 나 detail 을 어디서든지 활용하기가 좋다. 타입에러도 방지할 수 있다.

Slice 만들어 주기

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
export  const  masterSlice  =  createSlice({
name:  'master',
initialState,

reducers: {
getProductMasterDetail(state, action) {
state.productMasterDetail  = action.payload;
},
},
extraReducers: (builder) =>
builder
.addMatcher(masterApi.endpoints.getItemsShorts.matchFulfilled, (state, action) => {
state.productMasterLists  = action.payload.productMasterList;
state.productMasterLists.forEach((list, idx) => (list.key  = idx +  1));
state.totalCount  = action.payload.total;
})
  • master 페이지에서 사용할 masterSlice 를 만들어 준다.
  • AsyncThunk 랑 비슷하게, 이름을 지어준다.
  • 상태 초기값(바뀐 값과 비교할 수 있는 initialState 를 넣어주고, 일반적인 reducers 와 , api통신을 통해서 바로 상태를 업데이트 해주는 extraReducers를 만들어준다.
  • action.payload 는 response 에 있는 data 라고 생각하면 편하다.

RootReducer 에 추가하기

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// ConfigureStore 파일에서 작업하기
const rootReducer = (state, action) => {
  switch (action.type) {
    case HYDRATE:
      return action.payload;
    default: {
      const combinedReducer = combineReducers({
        user: userSlice.reducer,
        order: orderSlice.reducer,
        store: storeSlice.reducer,
        center: centerSlice.reducer,
        master: masterSlice.reducer,
        input: inputSlice.reducer,
        output: outputSlice.reducer,
        [rtkApi.reducerPath]: rtkApi.reducer,
      });
      return combinedReducer(state, action);
    }
  }
};
  • 지금까지 만들었던 masterSlice.reducer 를 RootReducer에 추가해 주어야 한다.

boilerplate 를 마치며

boilerpate 작업 끝! 너무나도 간단하게 초기작업이 끝났다. 더이상 api 통신을 하나하나 다~ 적어가며 할 필요없이! 명령어만 입력해주면 요청한 페이지에서 알아서 서버의 응답을 가져올 수 있다. 다음시간에는 실제 사용방법과 여러가지 기능들을 알아보자.


참조 : https://redux-toolkit.js.org/rtk-query/overview

This post is licensed under CC BY 4.0 by the author.

[Debug][Blog] 포스트 하면서 발생한 Errors

[TS] Interface 와 Class 언제 무엇을 쓸까