Redux 개념

redux

기존 프로젝트에서 useState hook을 이용해 상태를 관리했다. 페이지에서 전체 상태를 관리하고 컴포넌트로 프로퍼티를 넘겨주는 방식으로 개발했다. 컴포넌트가 세분화 되고 자식의 자식의 자식의… 컴포넌트가 생기면서 prop drilling 문제가 생겼다. 오직 데이터를 전달하는 역할만 하는 컴포넌트가 많아졌다. 정확히 어느 컴포넌트에서 해당 데이터가 사용되는지 확인하는데 어려움이 있었다. 이를 해결하기 위해 Redux Toolkit을 도입했다. Redux Toolkit은 Redux에서 만든 공식 라이브러리로 Redux 작업을 단순화하고 더 쉽게 작성할 수 있도록 한다. Toolkit 내용은 뒤로 하고 Redux 동작 원리와 키워드에 대해 정리해보려한다.

Redux를 사용하고 싶다면 Redux Toolkit을 사용하자!


Redux #

Redux는 전역 state를 관리하는 라이브러리이다. state를 컴포넌트에 종속시키지 않고 외부에 store를 두어 관리한다. 이를 통해 모든 컴포넌트들은 트리 위치에 상관없이 state에 접근하거나 작업을 할 수 있다. 즉 state 관리와 관련된 개념을 정의하고 view와 분리한다.

redux-store

state를 중앙에 집중

Redux는 단방향 데이터 흐름 구조를 사용한다. 간단하게 흐름을 설명하면 다음과 같다.

위의 흐름을 인지하고 Redux의 용어 개념과 함께 Redux의 동작 방식을 정리하려 한다.


Store #

애플리케이션 state를 가지고 있는 객체이다. Redux 앱에는 단 하나의 store만 있어야 한다. 하나의 store에서 state들이 객체 형식으로 저장된다.


Action #

상태를 업데이트하는 정보가 담긴 객체이다. store에 데이터를 넣는 유일한 방법!

Action은 타입(type)과 데이터(Toolkit에서는 payload)를 가지고 있다.

// action 객체 예시
{type: 'todos/todoAdded', payload: todoText}
{type: 'todos/todoToggled', payload: todoId}
{type: 'todos/colorSelected', payload: {todoId, color}}


Reducer #

현재 state와 action을 받아 새 state를 반환하는 함수이다.

type Reducer<S, A> = (state: S, action: A) => S
const initialState = { value: 0 }

function counterReducer(state = initialState, action) {
  switch (action.type) {
    case 'counter/increment': {
      return {
        ...state,
        value: state.value + 1,
      }
    }
    default:
      return state
  }
}

Reducer는 다음과 같은 규칙을 따라야 한다.

Reducer가 외부 변수에 의존하거나 무작위로 동작하면 어떤 일이 발생할지 알 수 없기 때문에 코드를 예측 가능하게 만드는 것이 중요하다.

불변성을 따르지 않을 경우 객체 업데이트를 인지하지 못해 UI 렌더링에 버그가 생길 수 있다. 원본을 변경하는 대신 새 복사본을 반환하는 작업을 수행해 불변성을 지켜야 한다. 객체 참조

https://daveceddia.com/react-redux-immutability-guide/

// ❌ Illegal - by default, this will mutate the state!
state.value = 123

// ✅ This is safe, because we made a copy
return {
  ...state,
  value: 123,
}

여기서 Redux Toolkit의 장점이 나오는데, Toolkit은 불변성을 지켜주어 업데이트 규칙에 대한 생각을 덜어준다.


Dispatch #

dispatch 함수는 action을 파라미터로 전달한다. 호출을 해서 전달하면 store에서 reducer 함수를 실행해 해당 action을 처리하는 로직이 있다면 참고하여 새 state를 생성한다. 기본 dispatch 함수는 반드시 동기적으로 store의 reducer에 action을 보내야한다.

각각의 용어에서 보면 Redux의 세 가지 원칙을 알 수 있다.


Redux의 전체적인 흐름 #

redux-data-flow


React Redux #

react-redux를 설치한다

npm install react-redux

// yarn을 사용한다면

yarn add react-redux

React App을 Provider로 감싸준다

Provider는 store를 앱의 모든 컴포넌트에서 사용할 수 있도록 해준다.

import React from 'react'
import ReactDOM from 'react-dom/client'

import { Provider } from 'react-redux'
import store from './store'

import App from './App'

// As of React 18
const root = ReactDOM.createRoot(document.getElementById('root'))
root.render(
  <Provider store={store}>
    <App />
  </Provider>
)

React 요소가 Redux와 상호 작용할 수 있도록 hook을 제공한다

export function Counter() {
  const count = useSelector(selectCount)
  const dispatch = useDispatch()

  return (
    <div>
      <div className={styles.row}>
        <button
          className={styles.button}
          aria-label='Increment value'
          onClick={() => dispatch(increment())}
        >
          +
        </button>
        <span className={styles.value}>{count}</span>
        <button
          className={styles.button}
          aria-label='Decrement value'
          onClick={() => dispatch(decrement())}
        >
          -
        </button>
      </div>
    </div>
  )
}


Reference #

https://redux.js.org/

https://react-redux.js.org/