[React Redux] React Redux와 Redux Toolkit 함께 사용하기 - (3)

Redux Toolkit을 적용하여 React Redux 사용하기

Redux Toolkit이 소개된 이후부터 React Redux는 이와 함께 사용하는 것이 효율적이기 때문에 두 가지를 합쳐서 사용하는 것이 일반적이다. 만약 타입스크립트를 사용한다면 추가적으로 설정해줘야 하는 부분들이 있다.

configureStore

우선 store를 만들어야 한다.

// store.ts
export const store = configureStore({
  reducer: {},
});

export type RootState = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;

configureStore는 리덕스의 createStore를 추상화 한 것이다. 리덕스의 번거로운 설정 과정을 자동화 해주며, 이 안에는 reducer, middleware, devTools, preloadedState, enhancers를 정의해줄 수 있다.
마지막 두 줄은 store를 정의하고 그 store에 있는 메서드의 타입을 활용하여 RootStateAppDispatch의 타입을 정의해준 것이다.

Provider

앱 내의 컴포넌트 어디에서든 store에 접근할 수 있도록 index.tsx에서 뿌려준다.

// index.tsx
const root = ReactDOM.createRoot(
  document.getElementById('root') as HTMLElement
);
root.render(
  <React.StrictMode>
    <Provider store={store}>
      <App />
    </Provider>
  </React.StrictMode>
)

이로써 컴포넌트들은 store에 접근할 수 있게 된다.

useDispatch, useSelector

리액트 리덕스에서 dispatch와 selector를 사용할 때, 일반적인 경우 그냥 useDispatchuseSelector를 바로 사용하면 된다. 하지만 타입스크립트가 적용된 경우 두 메서드를 사용할 때마다 타입 선언을 해줘야 하기 때문에 이러한 번거로움을 최소화하기 위해 커스텀 훅을 만들어주는 게 좋다.

// hooks.ts
export const useAppDispatch = () => useDispatch<AppDispatch>()
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector

이렇게 미리 작성을 해두면 useDispatchuseSelector를 사용해야 하는 곳에서 useAppDispatchuseAppSelector를 사용함으로써 반복적인 타입 선언을 생략할 수 있다.

createSlice()

리덕스 툴킷에서 제공하는 가장 기본적이고 표준적인 접근 방법인 createSlice()를 생성해준다.

interface CounterState {
  value: number;
}

const initialState: CounterState = {
  value: 0,
};

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

export const { increment, decrement, incrementByAmount } = counterSlice.actions;

export const selectCount = (state: RootState) => state.counter.value;

export default counterSlice.reducer;

사용할 초기값의 타입과 값을 선언하고, createSlice를 선언한 후 export 해주면 된다.

incrementByAmount: (state, action: PayloadAction<number>) => {
      state.value += action.payload;
    },

참고로 action의 경우 PayloadAction<T> 타입을 갖게 되는데, 이는 리덕스 툴킷에서 제공하는 타입이며 제네릭 T에는 action.payload의 타입을 적어주면 된다.

간혹 타입스크립트가 초기값 initialState의 타입을 strict하게 판독하여 오류로 잡아낼 수 있는데 이럴 경우

const initialState = {
  value: 0,
} as CounterState

처럼 as를 활용해주면 된다.

컴포넌트에서 사용

// component.tsx
export default function Counter() {
  const count = useAppSelector((state) => state.counter.value);
  const dispatch = useAppDispatch();

  const onIncrement = () => {
    dispatch(increment());
  };

  const onDecrement = () => {
    dispatch(decrement());
  };

  const onIncrementByTwo = () => {
    dispatch(incrementByAmount(2));
  };

  return (
    <div>
      <div>{count}</div>
      <button type="button" onClick={onIncrement}>
        ADD
      </button>
      <button type="button" onClick={onDecrement}>
        SUB
      </button>
      <button type="button" onClick={onIncrementByTwo}>
        ADD+2
      </button>
    </div>
  );
}

필요한 state를 useAppSelector로 불러오고, 사용할 reducer들을 dispatch에 넣어준 뒤 필요한 곳에 렌더해주면 된다.

참고: React Redux 공식 문서
참고: Redux Toolkit 공식 문서