React Redux & RTK Introduce

류지승·2025년 2월 13일

React

목록 보기
17/19
post-thumbnail

Usage Summary

Install Redux Toolkit and React Redux

npm install @reduxjs/toolkit react-redux

Common

// main.jsx
import React from 'react'
import ReactDOM from 'react-dom/client'
import './index.css'
import App from './App'
import store from './app/store'
import { Provider } from 'react-redux'

// As of React 18
const root = ReactDOM.createRoot(document.getElementById('root'))

root.render(
  <Provider store={store}>
    <App />
  </Provider>,
)

// App.js
import React from 'react';
import { useSelector, useDispatch } from 'react-redux';

const App = () => {
  // 상태를 useSelector로 읽어옴
  const count = useSelector((state) => state.counter.count);  // counter 리듀서의 상태
  const user = useSelector((state) => state.user);  // user 리듀서의 상태
  const dispatch = useDispatch();

  // 카운터 증가/감소
  const increment = () => {
    dispatch({ type: 'INCREMENT' });
  };

  const decrement = () => {
    dispatch({ type: 'DECREMENT' });
  };

  // 사용자 설정/초기화
  const setUser = () => {
    dispatch({ type: 'SET_USER', payload: { name: 'John', email: 'john@example.com' } });
  };

  const clearUser = () => {
    dispatch({ type: 'CLEAR_USER' });
  };

  return (
    <div>
      <h1>Counter: {count}</h1>
      <button onClick={increment}>Increment</button>
      <button onClick={decrement}>Decrement</button>

      <h2>User Info</h2>
      <p>Name: {user.name}</p>
      <p>Email: {user.email}</p>
      <button onClick={setUser}>Set User</button>
      <button onClick={clearUser}>Clear User</button>
    </div>
  );
};

export default App;

React Redux VS RTK

Reducer

// React Redux
const initialState = { count: 0 };

const counterReducer = (state = initialState, action) => {
  switch (action.type) {
    case 'INCREMENT':
      return { count: state.count + 1 };
    case 'DECREMENT':
      return { count: state.count - 1 };
    default:
      return state;
  }
};

export default counterReducer;

// RTK
import { createSlice } from '@reduxjs/toolkit';

const counterSlice = createSlice({
  name: 'counter',
  initialState: { count: 0 },
  reducers: { // immer를 이용하여 불변성
    increment: (state) => {
      state.count += 1;
    },
    decrement: (state) => {
      state.count -= 1;
    },
  },
});

export const { increment, decrement } = counterSlice.actions; // 액션 생성자 자동
export default counterSlice.reducer; // 리듀서 자동

React Redux vs RTK

리듀서 선언 방식
React Redux에서는 리듀서를 수동으로 작성한다. state를 기본값으로 초기화하며, 액션 타입에 따라 switch 문을 사용하여 각 액션에 맞는 상태 변화를 처리한다.
RTKcreateSlice를 사용하여 리듀서와 액션 생성자를 자동으로 생성한다. reducers 객체에서 각 액션을 정의하며, 각 액션에 대한 상태 업데이트를 간단하게 함수를 사용하여 처리한다.

state 불변성
React Redux는 state는 직접 수정하지 않고, 새로운 객체를 반환하여 상태를 변경한다. 즉, state.count + 1 또는 state.count - 1을 계산하고 그 결과로 새로운 객체를 반환하는 방식이다.
RTKImmer라는 라이브러리를 사용하여 상태를 직접 변경할 수 있도록 한다. 즉, state.count += 1처럼 상태를 직접 수정할 수 있지만, 내부적으로는 불변성을 유지한 채 상태가 변환된다.

액션 타입
React Redux액션 타입(INCREMENT, DECREMENT)을 문자열로 하드코딩하여 사용한다. 액션 타입을 switch 문에서 직접 비교해야 하기 때문에 액션 타입 관리가 수동으로 이루어진다.
RTK에서는 액션 타입을 문자열로 하드코딩할 필요 없이, createSlice가 자동으로 액션 생성자와 액션 타입을 생성해준다. 이를 통해 액션 타입을 별도로 관리할 필요가 없다. increment와 decrement 액션createSlice에서 자동으로 생성하며, 이를 바로 사용할 수 있다.

createSlice vs createReducer 차이점

특징createSlicecreateReducer
자동 생성되는 것리듀서 + 액션 생성자리듀서만 자동 생성
액션 생성자자동으로 생성되며, counterSlice.actions에서 사용할 수 있음별도로 액션 생성자 정의 필요 (수동으로 작성)
구현 방식리듀서를 함수 형식으로 작성하고, 액션 타입은 자동으로 생성됩니다.switch문을 사용하지 않고, 맵 객체로 리듀서를 정의
불변성 처리Immer를 통해 불변성 자동 처리Immer를 사용하여 불변성 관리 자동 처리
사용 목적리듀서와 액션 생성자를 동시에 관리하고 싶을 때 적합리듀서만을 관리하고 싶을 때, 액션을 별도로 정의하고 싶을 때 적합
예시increment, decrement 액션과 리듀서를 동시에 정의함INCREMENT, DECREMENT 액션을 별도로 정의하고, 리듀서만 관리
  • createSlice리듀서와 액션을 한 번에 관리할 수 있어 간단하고 직관적이다.
  • createReducer는 리듀서만을 세밀하게 정의할 수 있으며, 액션을 별도로 정의하고 싶을 때 유용하다.

Store

// React Redux
import { createStore, applyMiddleware, combineReducers } from 'redux';
import thunk from 'redux-thunk';
import { composeWithDevTools } from 'redux-devtools-extension';
import counterReducer from './counterReducer';
import userReducer from './userReducer';

// combineReducers를 사용해서 리듀서 결합
const rootReducer = combineReducers({
  counter: counterReducer,
  user: userReducer,
});

// 스토어 생성
const store = createStore(
  rootReducer,
  composeWithDevTools(applyMiddleware(thunk)) // 미들웨어와 Redux DevTools 설정
);

export default store;

// RTK
import { configureStore } from '@reduxjs/toolkit';
import counterReducer from './counterSlice';
import userReducer from './userSlice';

const store = configureStore({
  reducer: {
    counter: counterReducer,
    user: userReducer,
  },
});

export default store;

React Redux vs RTK

리듀서 설정
React ReduxcombineReducers를 사용하여 여러 리듀서를 결합한다. 이를 통해 counter와 user 상태를 관리하는 리듀서를 결합한 후, rootReducer를 생성하고 이를 createStore로 전달한다.
RTKconfigureStore에서 reducer 속성으로 객체를 전달하여 리듀서를 결합한다. configureStore는 내부적으로 리듀서를 결합하는 작업을 자동으로 처리하므로 combineReducers를 별도로 사용할 필요가 없다.

Middleware 및 Redux DevTools
React ReduxapplyMiddleware를 사용하여 thunk 미들웨어를 설정한다. 또한 composeWithDevTools을 사용하여 개발자 도구를 설정한다.
RTKconfigureStore를 사용하면 미들웨어 설정이 자동으로 처리된다. 기본적으로 redux-thunk가 내장되어 있어서 별도로 설정할 필요가 없다. 또한 Redux DevTools도 기본적으로 활성화되어 있으며, 별도의 설정 없이 사용할 수 있다.

스토어 생성
React ReduxcreateStore를 사용하여 스토어를 생성한다. 또한 applyMiddleware와 composeWithDevTools를 함께 사용하여 미들웨어와 개발자 도구를 설정한다. 더 많은 설정이 있을 경우 추가해야한다.
RTKconfigureStore를 사용하여 스토어를 생성한다. 이 함수는 내부적으로 필요한 설정(미들웨어, Redux DevTools 등)을 자동으로 처리한다.

Redux ToolKit Typescript

Project Setup

Define Root State and Dispatch Types

RTKconfigureStore 에서 별도로 추가적인 Type 정의가 필요하지 않다. 하지만 RootStateAppDispatch 타입은 추출하는 게 유지보수 측면에서 좋다.

RootState와 AppDispatch의 타입을 추출하면 유지보수가 좋은 이유
RootState란, redux에서 사용하는 모든 state를 의미한다. 즉, store.getState()를 호출하면 전체 state가 나오는데, 이 때의 타입을 RootState라고 한다.

export type RootState = ReturnType<typeof store.getState>

import { useSelector } from 'react-redux'
import { RootState } from './store'

const CounterComponent = () => {
  const counter = useSelector((state: RootState) => state.counter.value)
  return <div>Counter: {counter}</div>
}

RootStateconfigureStore에서 자동추론에 의해 타입 정의를 하게 되면, 전역 Component에서의 useSelector에서 state 타입정의를 안전하게 하면서 편리하게 사용할 수 있다.

Appdispatch란 Redux의 dispatch 함수의 타입을 의미한다.

RTK에서는 store.dispatch도 자동으로 타입을 추론하지만, 컴포넌트에서 useDispatch를 사용할 때 정확한 타입을 지정하는 것이 좋다.

export type AppDispatch = typeof store.dispatch

import { useDispatch } from 'react-redux'
import { AppDispatch } from './store'
import { increment } from './counterSlice'

const CounterButton = () => {
  const dispatch = useDispatch<AppDispatch>() 
  return <button onClick={() => dispatch(increment())}>Increment</button>
}

Define Typed Hooks

RootState & AppDispatch를 사용하면 전역 component에서 타입 정의를 간편하게 할 수 있다는 장점이 존재하지만, useSelector 또는 useDispatch를 선언할 때마다, type을 직접 넣어줘야하기 때문에 (RootState / AppDispatch) 개발자의 실수 유발 및 번거로울 수 있다. 따라서 RTK에서 이를 해결하기 위해 store에서 RootState & Appdispatch 타입 선언 이후 hooks를 정의한다.

const store = configureStore({
  reducer: {
    counter: counterSlice.reducer,
  },
})

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

// react-redux v8 미만

export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector
export const useAppDispatch = () => useDispatch<AppDispatch>()

// react-redux v8 이상
export const useAppSelector = useSelector.withTypes<RootState>()
export const useAppDispatch = useDispatch.withTypes<AppDispatch>()

export default store

Application Usage

Define Slice State and Action Types

RTK 기준 initial Slice를 선언할 때, 초기 상태의 타입에 대해서 정의해야한다. action 같은 경우 PayloadAction<T>를 사용하여 타입을 정의해야한다.

import { createSlice, PayloadAction } from '@reduxjs/toolkit'
import type { RootState } from '../../app/store'

// slice에서 사용하는 value의 interface 정의
interface CounterState {
  value: number
}

// 초기 state를 interface CounterState 타입 할당
const initialState: CounterState = {
  value: 0,
}

export const counterSlice = createSlice({
  name: 'counter',
  initialState,
  reducers: {
    increment: (state) => {
      state.value += 1
    },
    decrement: (state) => {
      state.value -= 1
    },
    // PayloadAction을 이용하여 action 타입 정의
    incrementByAmount: (state, action: PayloadAction<number>) => {
      state.value += action.payload
    },
  },
})

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

생성된 액션 생성자는 PayloadAction<T> 타입을 기반으로 payload 인자를 정확하게 타입화한다. incrementByAmount같은 경우 action.payload 타입으로 number만 가능하다는 얘기다. 경우에 따라 typescript 명시적 타입 할당을 해야하는 경우가 있다. 예시 그런 경우에는 as를 이요하여 타입을 명시적으로 할당하면 된다.

// Workaround: cast state instead of declaring variable type
const initialState = {
  value: 0,
} as CounterState

Use Typed Hooks in Components

컴포넌트에서 redux에서 제공하는 훅을 사용하는 게 아닌 useAppSelector, useAppDispatch를 호출하여 사용해야한다.

import React, { useState } from 'react'

import { useAppSelector, useAppDispatch } from 'app/hooks'

import { decrement, increment } from './counterSlice'

export function Counter() {
  // The `state` arg is correctly typed as `RootState` already
  const count = useAppSelector((state) => state.counter.value)
  const dispatch = useAppDispatch()

  // omit rendering logic
}
profile
성실(誠實)한 사람만이 목표를 성실(成實)한다

0개의 댓글