Redux 시작하기

Redux Toolkit 설치

# NPM
npm install @reduxjs/toolkit

# Yarn
yarn add @reduxjs/toolkit

Redux toolkit 이 무엇인가?

  • @reduxjs/toolkit 패키지는 코어 redux 패키지를 포함하여 redux 앱을 만드는데 필수적인 API 메서드와 공통 의존성을 포함한다. - redux docs

Redux docs 를 읽고 javascript, CRA 템플릿을 활용한 React Redux 앱을 만들어 실제 redux 를 어떻게 활용하는지 알아보고자 한다.

sudo npx create-react-app@^5 react-redux --template redux

이후 Redux 코어를 설치해줬다. 이때 설치속도, 보안상 이점등의 이유로 yarn 으로 redux 를 설치했다.

# npm
npm i redux

# yarn
yarn add redux

위의 명령어로 redux 프로젝트를 생성하면 기본적으로 redux 를 사용한 counter 코드가 생성되어 있다.

redux 란?

reduxaction 이라는 이벤트를 사용하여 애플리케이션 상태를 관리하고 업데이트하기 위한 패턴 및 라이브러리이다.

  • 상태가 예측 가능한 방식으로만 업데이트될 수 있도록 보장하는 규칙을 사용하여 전체 애플리케이션에서 사용해야 하는 상태에 대한 중앙 집중식 저장소 역할을 한다.

redux 는 전역 상태를 관리하는데 도움이 된다.

redux 개념과 작동흐름

redux 를 적용하기 전에 CRA 로만 구성한 간단한 Counter 컴포넌트를 생각해보자. ( redux 프로젝트 생성시 만들어지는 Counter 컴포넌트를 이해하기 위해 )

function Counter() {
  // State: a counter value
  const [counter, setCounter] = useState(0)

  // Action: code that causes an update to the state when something happens
  const increment = () => {
    setCounter(prevCounter => prevCounter + 1)
  }

  // View: the UI definition
  return (
    <div>
      Value: {counter} <button onClick={increment}>Increment</button>
    </div>
  )
}

useState 정도만 활용해서 카운터를 만들 수 있다. 이런 기존의 React 컴포넌트들은 사실 state, view, actions 로 구성되어 있다.

  • state : 앱을 구동하는 정보의 원천, source of truth
  • view : 현재 상태를 기반으로 UI 에 대한 선언적 설명
  • actions : 앱 내에서 유저의 입력 혹은 상태 변경을 유발하여 발생하는 이벤트

single source of truth : 앱의 전역 상태가 하나의 store 내부에 객체로 저장된다. 어떤 데이터도 여러 장소에 중복적으로 있으면 안되고 한곳에만 존재해야 한다.

  • 이를 통해 상태가 변경시 디버깅하기 쉬워진다.
  • 전체 앱과 상호작용하는데 필요한 로직을 중앙집중화할 수 있다.

이미지 출처 : https://ko.redux.js.org/tutorials/fundamentals/part-2-concepts-data-flow

이런 단방향 데이터 흐름은 얼핏 보기에 간단해 보이나 다수의 컴포넌트가 동시에 같은 상태를 사용하고 공유해야 한다면 매우 복잡해질 수 있다. React 는 이런 경우 lifting state up 이라고 하여 상태를 부모 컴포넌트에서 만들도록 useState 를 사용하는 부분을 자식에서 부모 컴포넌트로 올리고 데이터는 부모에서 자식 컴포넌트 방향으로 props 를 전달하는 식으로 해결하는 방법이 있으나 구조가 복잡해질때 props drilling 이 발생하여 실제로 해당 데이터가 필요없는데 props 전달을 위해 불필요한 재렌더링이 발생할 수 있다는 문제가 있었다.

이런 문제를 해결하기 위한 방법으로는 공유되는 상태를 컴포넌트로부터 분리하고 컴포넌트 트리 바깥에 중앙집중되는 장소에서 관리하는 것이다. 이런 방법을 통해 어떤 컴포넌트도 컴포넌트 트리의 어디에 존재하든 상관없이 상태에 접근하거나 action 을 유발할 수 있다.

이게 Redux 의 기본 아이디어이다. 그래서 Redux를 애플리케이션의 전역 상태를 포함하는 단일화된 중앙 집중 상태관리를 통해 코드를 예측가능하게 하는 패턴이라고 부르기도 한다.

불변성

mutable 이 변경 가능을 말하고 immutable 이 변할수 없는 것을 말한다. 자바스크립트 객체와 배열은 기본적으로 mutable 하다. 만약 immutably 하게 값을 수정하고 싶다면 원본을 수정해서는 안되고 존재하는 원본의 복사본을 만들어 복사본을 수정해야 한다.

이를 Object.assign, concat, spread operator 등을 통해 구현가능하나 많은 경우 spread operator 를 사용하는 것 같다.

redux 는 모든 상태 업데이트를 immutably 하게 처리한다.

redux 용어

지난 redux 입문 편 에서도 다루기는 했는데 좀더 자세한 정보가 담겨있는 문서가 보여서 다시 정리하려 한다.

Actions

action 은 plain JavaScript object 이다.

type field 를 반드시 포함해야 하며 action 을 나타내는 이름을 type 에 넣어주는데 이때 "domain/eventName" 같은 형태의 문자열을 보통 사용한다. 이때 domain 은 이후 feature 라는 개념과 같은 의미를 나타내는 것 같았다.

src/ 디렉토리에 App.jsx, index.js 등이 있는데 store 도 하나만 존재하고 root reducer 도 하나만 존재하다보니 하나의 reducer 에 모든것을 관리하기 어려워져 reducer 를 비슷한 기능끼리 reducer splitting 을 하여 관리를 용이하게 했다.

이때 비슷한 기능들을 features 라고 하는 디렉토리에 slice 들로 나누어 관리하는데 여기서의 domain 과 같은 의미이다.

그래서 분리가 되면 todos 라는 도메인 / features 에 todo 와 관련된 컴포넌트, reducer, 테스트코드들을 같이 관리하는 형태를 취하는 것 같다.

다시 돌아와서 액션 객체는 발생한 현상에 대한 추가적인 정보를 가질 수 있는데 convention, 정해진 규칙으로는 이런 정보들을 payload 에 담아 전달한다. 이때 payload 는 문자열이 될수도 있고 객체나 배열이 올수도 있다.

const addTodoAction = {
  type: 'todos/todoAdded',
  payload: 'Buy milk'
}

Reducers

reducer 는 현재의 상태와 액션을 입력받아 다음 상태를 계산해주는 순수함수이다. 이때 액션은 없을수도 있다. reducer 를 받은 액션 타입에 따라 이벤트를 다루는 이벤트 리스너라고 생각해도 된다.

reducer 라는 이름이 붙은 이유는 Array.reduce() 메서드와 비슷하게 동작하기 때문이다. Array.reduce() 메서드는 콜백함수를 갖는데 이 콜백함수는 인자로 현재까지 누적된 결과와 현재 값을 받아서 다음 결과를 만든다. 이를 통해 reduce 는 Array 에서 콜백함수의 결과물로 특정 값을 반환하게 된다.

이처럼 reducer 도 현재까지의 상태와 변경할 액션을 받아 다음 결과를 만들기에 reducer 라는 이름이 붙은 것이다.

그런데 다음 state 를 계산하기는 해도 기존의 state 를 직접 변경하는 것이 아니다! redux 는 immutable 하게 상태를 update 하기위해 기존 상태는 그대로 두고 복사본을 만들어서 그 복사본을 수정한다.

reducer 는 비동기 로직, 랜덥값 계산 혹은 다른 side effect 를 하거나 야기해서는 안된다.

const initialState = { value: 0 }

function counterReducer(state = initialState, action) {
  // Check to see if the reducer cares about this action
  if (action.type === 'counter/incremented') {
    // If so, make a copy of `state`
    return {
      ...state,
      // and update the copy with the new value
      value: state.value + 1
    }
  }
  // otherwise return the existing state unchanged
  return state
}

Store

현재 redux 앱의 상태는 store 라고 불리는 객체내에 있다. store 는 reducer 를 전달하여 생성해줄 수 있고 store 에 getState() 를 통해 현재 상태값을 반환할 수 있다.

store 는 redux 로부터 import 한 createStore 를 통해 생성해줄수도 있지만 @reduxjs/toolkit 으로부터 import 한 configureStore 을 통해 생성해줄 수 도 있다.

import { configureStore } from '@reduxjs/toolkit'

const store = configureStore({ reducer: counterReducer })

console.log(store.getState())
// {value: 0}

Dispatch

store 는 dispatch 라 불리는 메서드를 갖고 있는데 상태를 유일하게 변경할 수 있는 방법이다. dispatch 는 action 객체를 전달하여 동작한다. dispatch 로 action 을 전달하면 store 는 내부적으로 reducer 함수를 실행하여 새로운 상태를 저장할 것이고 우리는 getState() 를 통해 바뀐 값을 얻을 수 있다.

store.dispatch({ type: 'counter/incremented' })

console.log(store.getState())
// {value: 1}

Selectors

selector 는 처음보는 개념이었는데 redux 가 하나의 상태에서 모든 값을 관리하다보니 구조가 복잡해질수록 불필요하게 모든 데이터를 가져오게 되는데 selector 를 사용하면 상태 값에서 정보 일부만 다룰 수 있게 해준다.

const selectCounterValue = state => state.value

const currentValue = selectCounterValue(store.getState())
console.log(currentValue)
// 2

redux 작동흐름

먼저 Redux store 가 root reducer 함수를 사용해서 생성된다.

import { createStore } from 'redux';
import reducer from './reducers';

let store = createStore(reducer);

위의 코드에서 store 는 createStore 를 통해 생성되는데 이때 인자로 reducer 가 필요하다.

그러고 store 는 root reducer 를 한번 호출하고 반환값을 초기 상태로 저장한다.

위의 코드의 store 를 만드는 과정에서 초기 상태가 저장되는 것 같다..

UI 가 처음 렌더되면 UI 컴포넌트는 Redux store 의 현재 상태에 접근하고 해당 데이터를 사용하여 무엇을 렌더링할지 결정한다. 또한 UI 컴포넌트는 미래에 어떤 store 가 업데이트되었는지 확인하여 만약 상태가 변경되었는지 여부를 알기 위해 subscribe 한다.

function render() {
	let state = store.getState();
    ...
}

store.subscribe(render);

이 경우 render 는 현재 store 에 상태가 변경되면 이를 반영하여 UI 컴포넌트를 업데이트한다.

이후에 버튼을 클릭한다던지 무언가 변경이 발생하면 앱은 Redux 스토어로 action 을 dispatch 한다. dispatch({type: 'counter/increment'})

그러면 store 는 이전 상태와 action 을 갖고 reducer 함수를 다시 실행하여 새로운 상태값을 반환한다.

store 는 store 가 업데이트되었다는 것을 구독하고 있는 모든 UI 들에게 알린다. 그러면 store 의 데이터를 필요로 하는 각 UI 컴포넌트는 그들이 필요로하는 상태값이 변경되었는지 확인한다.

store 를 구독해서 이 데이터들을 보고있는 각 컴포넌트는 새 데이터를 갖고 강제로 재렌더링되니 스크린에 변경사항을 업데이트할 수 있다.


gif 출처 : https://ko.redux.js.org/tutorials/essentials/part-1-overview-concepts/

위의 사례를 보면 store 가 생성될때 초기상태를 갖고 있고 이벤트가 발생하면 action 과 dispatch 를 통해 변경점을 store 에 알려서 이전 상태와 action 으로 새로운 상태를 만들어 UI 에 반영하는 모습을 볼 수 있다.


이미지 출처 : https://opentutorials.org/course/4901/24935

profile
미래의 나를 만들어나가는 한 개발자의 블로그입니다.

0개의 댓글

Powered by GraphCDN, the GraphQL CDN