개발/frontend

Redux 사용하기 (Redux tool kit)

체인의정석 2025. 7. 10. 15:45
728x90

Redux 구조

Redux는 데이터가 한 방향으로만 흐르는 flux 아키텍처에 따라 설계되었다.

Store : Redux에서 모든 Store라 불리는 객체 안에 저장 됨

Action, Dispatch : Redux에서는 Action이 일어나면 Dispatch를 하는데 어떤 일이  발생했을 시 Dispatch(송신)을 해서 State를 업데이트 한다.

Dispatch된 Action을 받는 부분이 Reducer 이다.

Reducer는 현재 State와 받은 Action에 대응해 새로운 State를 반환하는 함수이다.

Reducer가 새로운 State를 반환함으로써 Store안의 State가 업데이트 된다.

Redux toolkit 기본 예제

https://redux.js.org/introduction/getting-started

 

Getting Started with Redux | Redux

Introduction > Getting Started: Resources to get started learning and using Redux

redux.js.org

import { createStore } from 'redux'

/**
 * 이것이 (state, action) => state 형태의 순수 함수인 리듀서입니다.
 * 리듀서는 액션이 어떻게 상태를 다음 상태로 변경하는지 서술합니다.
 *
 * 상태의 모양은 당신 마음대로입니다: 기본형(primitive)일수도, 배열일수도, 객체일수도,
 * 심지어 Immutable.js 자료구조일수도 있습니다.  오직 중요한 점은 상태 객체를 변경해서는 안되며,
 * 상태가 바뀐다면 새로운 객체를 반환해야 한다는 것입니다.
 *
 * 이 예제에서 우리는 `switch` 구문과 문자열을 썼지만,
 * 여러분의 프로젝트에 맞게
 * (함수 맵 같은) 다른 컨벤션을 따르셔도 좋습니다.
 */
function counter(state = 0, action) {
  switch (action.type) {
    case 'INCREMENT':
      return state + 1
    case 'DECREMENT':
      return state - 1
    default:
      return state
  }
}

// 앱의 상태를 보관하는 Redux 저장소를 만듭니다.
// API로는 { subscribe, dispatch, getState }가 있습니다.
let store = createStore(counter)

// subscribe()를 이용해 상태 변화에 따라 UI가 변경되게 할 수 있습니다.
// 보통은 subscribe()를 직접 사용하기보다는 뷰 바인딩 라이브러리(예를 들어 React Redux)를 사용합니다.
// 하지만 현재 상태를 localStorage에 영속적으로 저장할 때도 편리합니다.

store.subscribe(() => console.log(store.getState())))

// 내부 상태를 변경하는 유일한 방법은 액션을 보내는 것뿐입니다.
// 액션은 직렬화할수도, 로깅할수도, 저장할수도 있으며 나중에 재실행할수도 있습니다.
store.dispatch({ type: 'INCREMENT' })
// 1
store.dispatch({ type: 'INCREMENT' })
// 2
store.dispatch({ type: 'DECREMENT' })
// 1

store라는 저장공간을 만들고, subscribe를 통해서 상태변화에 따른 ui 변경을 관리하고 (이 부분은 react redux를 많이 사용), dispatch를 통해서 스토어에다가 미리 reducer에 정의해둔 액션을 실행시킨다.

Reducer는 위의 예시처럼 (state, action)을 인자 값으로 받아서 함수를 실행해주는 부분이라고 이해했다.

Redux toolkit 튜토리얼 - 설치

https://ko.redux.js.org/tutorials/quick-start

 

Quick Start | Redux

- How to set up and use Redux Toolkit with React-Redux

ko.redux.js.org

먼저 설치 부터 해준다.

npm install @reduxjs/toolkit react-redux

그 후 redux store를 생성해 준다.

파일이름은 store.js로 두고 여기서 configureStore라는 API를 가져와서 써주면 된다.

import { configureStore } from '@reduxjs/toolkit'

export default configureStore({
  reducer: {}
})

이제 생성한 store를 React-Redux Provider를 통해 주입시킨다. src/index.js 쪽에 가서 다음과 같이 넣어주면 된다.

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

ReactDOM.render(
  <Provider store={store}>
    <App />
  </Provider>,
  document.getElementById('root')
)

나는 next.js를 쓰고 있는 코드를 보고 있었기 때문에 src/app의 layout.tsx에 해당 부분이 정의되어 있었다.

그 후 redux state slice를 만들어준다.

Redux toolkit 튜토리얼 - Slice 생성

src/features/counter/counterSlice.js 해당 경로에 createSlice API를 만들라고 되어 있는데, 내가 보는 코드는 next.js라서 그런지 redux라는 폴더를 따로 만들어두고 features라는 하위 디렉토리를 만들어 두었다.

import { createSlice } from '@reduxjs/toolkit'

export const counterSlice = createSlice({
  name: 'counter',
  initialState: {
    value: 0
  },
  reducers: {
    increment: state => {
      // Redux Toolkit allows us to write "mutating" logic in reducers. It
      // doesn't actually mutate the state because it uses the Immer library,
      // which detects changes to a "draft state" and produces a brand new
      // immutable state based off those changes
      state.value += 1
    },
    decrement: state => {
      state.value -= 1
    },
    incrementByAmount: (state, action) => {
      state.value += action.payload
    }
  }
})

// Action creators are generated for each case reducer function
export const { increment, decrement, incrementByAmount } = counterSlice.actions

export default counterSlice.reducer

createSlice API안에 initialState에는 초기값을 지정해 두었다. 그리고 그 밑에 reducers에서는 staste를 받아서 상태변화를 시켜주는 함수들인 reducers가 들어가 있다.

slice를 만들기 위해서는 slice를 정의해주는 string형태의 이름과 초기 상태 값 그리고 하나 이상의 reducer 함수들이 정의되어야 한다. slice가 생성되게 되면 생성된 redux 액션이랑 reducer함수들을 export해서 사용할 수 있게 된다.

Redux는 상태 변화를 일이키지 않는다는 것을 가정하고 데이터를 복사하고 복사한 데이터를 업데이트 하지만 createSlice나 createReducer함수는 Immer라는 것을 내부적을 사용해서 원래는 변화가 불가능한 상태들을 업데이트 시켜줄 수 있게 한다.

Redux toolkit 튜토리얼 - Store에 Reducer 추가

 slice가 완성되고 난 후에는 Slice를 Store에 추가해 주어야한다.

import { configureStore } from '@reduxjs/toolkit'
import counterReducer from '../features/counter/counterSlice'

export default configureStore({
  reducer: {
    counter: counterReducer
  }
})

reducer 파라미터 안에다가 필드를 정의하고 store에게 해당 slice reducer 함수를 사용해서 상태를 업데이트 가능하게 해주어야 한다.

내가 보는 코드에는 reducer가 하나가 아니였기 때문에 reducer 안에 따로 rootReducer를 만들고, combineReducers라는 함수를 사용하여서 만들었다.

이 부분은 reducer를 한번에 합쳐주는 api인데 예시를 보겠다.

//reducers/todos.js
export default function todos(state = [], action) {
  switch (action.type) {
    case 'ADD_TODO':
      return state.concat([action.text])
    default:
      return state
  }
}

//reducers/counter.js
export default function counter(state = 0, action) {
  switch (action.type) {
    case 'INCREMENT':
      return state + 1
    case 'DECREMENT':
      return state - 1
    default:
      return state
  }
}

이런식으로 여러 reducer가 있다고 치면

import { combineReducers } from 'redux'
import todos from './todos'
import counter from './counter'

export default combineReducers({
  todos,
  counter
})

위와 같이 combineReducers api를 써서 하나로 합칠 수 있다.

https://ko.redux.js.org/api/combinereducers/

 

combineReducers | Redux

API > combineReducers: merging slice reducers to create combined state

ko.redux.js.org

 

Redux toolkit 튜토리얼 - 리액트 컴포넌트에서 Redux State와 Actions 사용해보기

import React from 'react'
import { useSelector, useDispatch } from 'react-redux'
import { decrement, increment } from './counterSlice'
import styles from './Counter.module.css'

export function Counter() {
  const count = useSelector(state => state.counter.value)
  const dispatch = useDispatch()

  return (
    <div>
      <div>
        <button
          aria-label="Increment value"
          onClick={() => dispatch(increment())}
        >
          Increment
        </button>
        <span>{count}</span>
        <button
          aria-label="Decrement value"
          onClick={() => dispatch(decrement())}
        >
          Decrement
        </button>
      </div>
    </div>
  )
}

위의 단계까지 왔다면 react-redux 훅을 써서 이젠 redux sotre와 react component간의 상호작용이 가능하다. store 안에 있는 데이터를 읽어올 때는 useSelector를 사용하고 useDispatch를 써서 actions를 가져오는 식으로 상호작용을 한다.

import React from 'react'
import { useSelector, useDispatch } from 'react-redux'
import { decrement, increment } from './counterSlice'
import styles from './Counter.module.css'

export function Counter() {
  const count = useSelector(state => state.counter.value)
  const dispatch = useDispatch()

  return (
    <div>
      <div>
        <button
          aria-label="Increment value"
          onClick={() => dispatch(increment())}
        >
          Increment
        </button>
        <span>{count}</span>
        <button
          aria-label="Decrement value"
          onClick={() => dispatch(decrement())}
        >
          Decrement
        </button>
      </div>
    </div>
  )
}

dispatch를 사용해서 이미 정의 된 액션을 가져와서 사용할 수 있게 된다.

참고로 타입스크립트를 쓸 경우 해당 AppDispatch함수를 사용하면 스토어의 디스패치 함수의 타입을 추론해서 알아서 정의해준다.

import { AppDispatch } from "@/redux/store";

 

 

728x90
반응형