성장을 위한 기록

redux toolkit 파해쳐보기 기본 개념 정리 및 사용법 본문

FE (Front End) (구)/React

redux toolkit 파해쳐보기 기본 개념 정리 및 사용법

B_Tae 2022. 6. 9. 22:02

Redux-toolkit은 왜 나왔나?

Redux-toolkit은 새로운 개념이 아닌 redux로직을 작성하는 하나의 방법이다. 그럼 기존의 redux와는 어떤 차이가 있을 까?

사람들이 말하길, 그리고 사용하면서 느낀 바로는 redux는 3가지 문제점이 있다.

  1. redux store 환경이 복잡하다ㅏ.
  2. redux를 유용하게 사용하려면 많은 패키지를 사용해야한다.
  3. 하나의 일을 수행하기 작성하는 코드가 너무 많다.

이런 문제점을 해결하기 위해 toolkit이 나왔고, toolkit을 이용하여 코드를 단순하고 간단하게 작성할 수 있다.

toolkit API

configureStore()

import { configureStore } from "@reduxjs/toolkit";
import thunk from "redux-thunk";
import memoReducer from "./modules/memoSlice";

const middlewares = [thunk];
const store = configureStore({
reducer: {
    memo: memoReducer,
},
middleware: [...middlewares],

preloadedState,

});
export default store;

위 예제는 간단하게 작성했지만, 더 많은 정보를 전달한다. (reducer,middlewware,devTools,preloadedState,enhancer)

  • reducer
  1. 단일함수를 전달하여 스토어의 root reducer로 바로 사용 가능( 위 예제가 그런방식)
  2. slice reducer들로 구성된 객체를 전달 할 수 있다. 이 경우 cinfigureStore 내부에 combineReducers함수를 통해 자동적으로 병합하여 reducer를 생성하게 된다.
  • middleware
    미들웨어 함수를 담는 배열, 사용할 모든 미들웨어를 배열에 담아서 명시할 수 있다.
    생략할 경우에는 getDefaultMiddleware를 호출한다.
    위 예제에는 thunk 함수를 연결했지만, redux-thunk는 default로 제공한다.
  • devTools
    불리언 값으로 리덕스 개발자 도구를 끄거나 킬 수 있다. default값은 true이다.
  • preloadedState
    스토어의 초기값을 설정할 수 있다. proloadedState에 변수를 담아 연결하면 store에 초깃값 설정이 된다.
  • enhancers
    redux store enhancers의 배열이며, 콜백 함수로 정의할 수도 있다.
    • 배열로 정의된 경우, 리덕스의 compose 함수로 전달되어 병합된 최종적 enhacer가 createStore함수로 전달된다.
    • 콜백 함수로 전달한 경우 applyMiddleware보다 앞서 추가할 수 있다. 즉 middleware보다 적용되는 순서를 앞서 정할 수 있다.
      const store = configureStore({
      ...
      enhancers: (defaultEnhancers) => [offline, ...defaultEnhancers],
      })

createReducer()

리듀서 함수를 생성하는 util 함수다.
특징으로 immer 라이브러리를 사용하여 mutative한 형태로 작성해도 immutability을 유지할 수 있다.
쉽게 생각해서 기본 redux에서는 store값에 불변성을 유지하기 위해 전체 데이터를 새로 저장하는 느낌의 방식을 사용했다. (객체나 배열 값을 변경할 수 없었다.) 하지만 toolkit에서는 immer라는 친구가 이를 알아서 처리해주기 때문에 store에 저장된 객체나 배열을 바로 변경할 수 있다.


또한 리덕스에서는 보통 switch-case문을 사용해 type별로 case를 나눠 관리했고, 중첩된 상태에 대해서는 값을 업데이트할 때 모든 단계에 복사가 필요했다. 이는 코드가 길어지는 범인이자 가독성을 떨어트렸다.

redux-toolkit에서는 액션을 처리하는 방법이 2가지가 있다

  1. builder callback
  2. map object

2가지 역할은 동일하지만, typescripte와의 호환성을 위해 첫번째 방법인 builder callback을 더 선호한다.
나도 이 방법만 사용해봤고, 여기서도 이 방법만 정리해보려한다.

bulder callback은 2가지 방법으로 표기할 수 있다.

extraReducers: (builder) => {
builder
// 연속적으로 작성
.addCase(__getMemo.pending, (state) => {
state.loading = true;
})
.addCase(__getMemo.fulfilled, (state, action) => {
state.loading = false;
state.text = action.payload;
})
/// 라인마다 빌더 메서드를 호출
extraReducers: (builder) => {
builder.addCase(__getMemo.pending, (state) => {
state.loading = true;
});
builder.addCase(__getMemo.fulfilled, (state, action) => {
state.loading = false;
state.text = action.payload;
})

Builder 메서드는 3가지가 있다

  • builder.addCase(actionCreator, reducer)
    특정 action type과 맵핑되는 case reducer를 추가한다. 다른 2가지 메서드 보다 앞서 작성해야한다.
  • builder.addMatcher(matcher, reducer)
    새로 들어오는 모든 액션에 대해서 사용자가 작성한 matcher 함수와 일치한는지 확인하고 리듀서를 실행한다.
  • builder.addDefaultCase
    그 어떤 case reducer나 matcher 리듀서도 실행되지 않았다면 기본 케이스 리듀서가 실행된다.

위 예제에는 addCase만 사용했고 아직 addmatcher을 활용하는 방법을 잘 모르겠다.
addDefaultCase는 addCase에서 pending, error 부분을 합쳐 작성 할 수 있다.

.addDefaultCase((state, action) => {
if (action.meta?.requestStatus === "pending") {
    //....
}
if(action.meta?.requestStatus === "rejected"){
    //....
}
});

이런 방식으로 사용해봤는데, addCase에서 pending과 rejected가 없다면 그 상황에 defaultCase가 발생하고 그에 따른 로딩과 에러 처리를 한 번에 할 수 있다.

createAction()

앞서 설명하진 않았지만, creactAction과 reacteReducer를 합칠 수 있는 방식이다.

const initialState = { value: 0 } as CounterState

const counterSlice = createSlice({
  name: 'counter',
  initialState,
  reducers: {
    increment(state) {
      state.value++
    },
    decrement(state) {
      state.value--
    },
    incrementByAmount(state, action: PayloadAction<number>) {
      state.value += action.payload
    },
  },
  extraReducers:.....
})

개념적으로 보자면 Slice는 initialState, reudcer 함수들의 객체, slice이름을 받아 이에 맞는 action creator와 action type을 자동으로 생성한다. 따라서 우리는 코드를 더 간단하게 작성할 수 있게 된다.

createAsyncThunk

createAction의 비동기 버전을 위해 만들었다고 한다.

export  const  __addMemo = createAsyncThunk(
"memos/ADD_MEMO",
async (payload, thunkAPI) => {
const  memoData = await  addDoc(collection(db, "memolist"), payload);
return { id:  memoData.id, text:  payload.text };
}
);

첫 번째 인자로는 action 타입 문자열과 두 번째 인자로는 프로미스를 반환하는 콜백 함수를 인자로 받아서 사용한다.
payload는 dispatch할 때 전달한 data가 들어가게 되고, thunkAPI는 다양한 내장 함수를 받아 사용할 수 있다.

만약 에러가 발생한다면, Error 인스턴스를 포함하는 rejected promise를 반환하거나 thunkAPI.rejectWithValue 메서드를 사용해 에러를 처리할 수 있다.

createAsyncThunk를 사용하면 비동기 처리를 편하기 할 수 있지만 thunk만 지원한다.

createSelector

데이터를 추출할 때 사용할 수 있다. 나는 지금까지 useSelector를 사용했다. createSelector는 이 결점을 보완하기 위한 좋은 방법이다.
우선 createSelector는 memoization을 기반으로 동작한다. 이 의미는 입력 값을 받을 때 저장하고 다음 호출에도 입력 값이 동일하다면 함수가 한 번만 실행되도록 하는 것을 보장한다.

작성하면서

누구한테 정보를 전달하기 보다는 개념을 정리하고 싶은 생각에 작성해봤다. 아직 모르는 부분이 많다. 이해하지 못한 부분도 많았지만, toolkit을 사용하는 방법에 대한 개념을 정리할 수 있었다.

앞으로 얼마나 toolkit을 이용할 상황이 있을지는 모르겠지만, 혼자 사용할 때는 애용할 것 같은 생각이 든다.

 

 

참고 

 

Redux-toolkit 공식문서 : https://redux-toolkit.js.org/

Redux Toolkit (리덕스 툴킷)은 정말 천덕꾸러기일까? : http://blog.hwahae.co.kr/all/tech/tech-tech/6946/

 

 

Comments