[React] Redux

수민🐣·2023년 4월 23일
0

React

목록 보기
34/36

Redux

Flux가 컨퍼런스에서 처음 발표되고 난 후, 많은 개발자들이 Flux에 관심을 가지기 시작했습니다. 하지만 Flux는 라이브러리나, 프레임워크가 아닌 디자인 패턴입니다. 즉 Flux 아키텍쳐를 사용하기 위해서는 개발자들이 직접 이 아키텍쳐에 맞게 코드로 구현해야 한다는 뜻입니다. 초창기에는 Flux 패턴을 각자의 방법대로 구현한 여러가지 라이브러리들이 쏟아져나왔습니다. 하지만 현재 Flux 패턴을 근간으로 하는 라이브러리의 표준은 Redux로 정립되었습니다.

Redux는 Flux, CQRS, Event Sourcing의 개념을 사용해서 만든 라이브러리로서 “JavaScript 앱을 위한 예측 가능한 상태 컨테이너"를 핵심 가치로 삼고 있습니다.

Redux는 모든 상태를 관리하는 컨테이너로서의 역할을 수행하고, 애플리케이션 내의 구성요소들은 컨테이너에 접근해서 상태를 읽어올 수 있기에 자바스크립트 앱에서 전역 상태 관리를 수행하기 위해서 사용할 수 있습니다.

Redux는 Flux 패턴의 단방향성을 차용했기에 Redux 내에서 발생하는 상태의 변화는 모두 예측 가능합니다. 이런 특성으로 인해 프론트엔드에서 발생하는 복잡한 상태들의 변화를 관리하기에 적격으로 판단되었으며, Redux가 발표되고 난 후 한동안 프론트엔드의 상태관리 표준은 Redux로 자리잡았습니다.

Redux의 3가지 원칙

Redux는 3가지 기본 원칙으로 이루어져있습니다.

1. Single source of truth

Redux 내의 모든 전역 상태는 하나의 객체 안에 트리구조로 저장되고, 이 객체를 Store라 부릅니다.
모든 상태(state)가 하나의 객체에 저장되기에 애플리케이션이 단순해지고, 예측하기 쉬워집니다. 또한 하나의 객체의 변화만 추적하면 되기에 Undo, Redo 등의 기능을 구현하기도 쉬워집니다.

{
  visibilityFilter: 'SHOW_ALL', // state 1
  todos: [                      // state 2
    {
      text: 'Consider using Redux',
      completed: true,
    },
    {
      text: 'Keep all state in a single tree',
      completed: false
    }
  ]
}

2. State is read-only

Redux의 State를 변화시키는 유일한 방법은 “Action 객체를 Dispatch를 통해서 전달하는 것입니다.” 그 외에 Store에 직접 접근해서 상태를 수정하는 등의 행위는 허용되지 않습니다.

Redux는 위와 같이 state를 불변하게 다루고, 변화시킬 수 있는 방법을 제약함으로서 안정성과 예측 가능성을 증대시킵니다.

모든 변화는 Dispatch를 통해서 중앙화되고, 순서대로 수행되기에 여러곳에서 동시에 데이터를 수정하면서 발생하는 race condition 문제 등이 발생하지 않게 됩니다.

또한, Action을 통해서 변화의 의도를 표현합니다. Action은 단순한 형태의 객체이기 때문에 이를 추적하거나, 로깅, 저장하는 등이 동작을 수행하기 용이하기에 디버깅을 손쉽게 할 수 있으며, 추후 테스트 코드를 작성하기도 용이합니다.

const action = {
	type: 'COMPLETE_TODO',
  index: 1
}

store.dispatch(action)

// ------------------------

store.dispatch({
  type: 'SET_VISIBILITY_FILTER',
  filter: 'SHOW_COMPLETED'
})

3. Changes are made with pure function

앞서, Redux의 State를 변화시키는 유일한 방법은 Action 객체를 Dispatch를 통해서 전달하는 것이라고 했습니다. Action이 Store에 전달된 후 실질적으로 Action을 통해서 Store를 변경시키는 동작은 Reducer라 불리는 순수함수를 통해서 수행됩니다.

순수함수란 동일한 Input을 받았을 경우 항상 동일한 Output을 내는것이 보장되어 있는 함수를 의미합니다. 순수함수가 되기 위해서는 함수 내에 사이드이펙트가 없어야 합니다. 만약 사이드 이펙트가 있는 경우 그 함수는 해당 사이드 이펙트에 의해서 같은 Input이라도 다른 Output을 리턴할 수가 있습니다.

// pure function
function sum(x,y) {
	return x + y;
}

sum(1,2) // 3
sum(1,2) // 3
sum(1,2) // 3
sum(1,2) // 3
sum(1,2) // 3

// non-pure function
function sum(x) {
	return x + Math.random();
}

sum(1) // ?
sum(1) // ?
sum(1) // ?
sum(1) // ?
sum(1) // ?

Reducer는 이전 state 값과, action 객체를 인자로 받아서 새로운 state를 리턴하는 순수 함수입니다.

여기서 주목해야 할 점은 새로운 state를 리턴한다는 점입니다. 즉, 기존의 state 객체를 수정하는 것이 아니라, 기존의 state 객체를 이용해서 새로운 state 객체를 만들어내는식으로 동작한다는 점입니다.

Redux는 Store가 하나이기에 이를 관리하는 Reducer 또한 하나여야 합니다. 하지만 각기 다른 관심사가 하나의 함수에 모두 들어가게 되면 유지보수에 좋지 않기에 애플리케이션이 커지면 여러개의 Reducer 함수(slice reducer)로 분리해서 코드를 작성한 다음 하나의 reducer(root reducer)로 통합하는 방식을 활용합니다.

function visibilityFilter(state = 'SHOW_ALL', action) {
  switch (action.type) {
    case 'SET_VISIBILITY_FILTER':
      return action.filter
    default:
      return state
  }
}

function todos(state = [], action) {
  switch (action.type) {
    case 'ADD_TODO':
      return [
        ...state,
        {
          text: action.text,
          completed: false
        }
      ]
    case 'COMPLETE_TODO':
      return state.map((todo, index) => {
        if (index === action.index) {
          return Object.assign({}, todo, {
            completed: true
          })
        }
        return todo
      })
    default:
      return state
  }
}

import { combineReducers, createStore } from 'redux'
// root reducer(visibilityFilter, todos)를 통합해서 rootReducer 생성
const reducer = combineReducers({ visibilityFilter, todos })

// root reducer를 통해서 store 생성
const store = createStore(reducer)

Redux의 구성요소와 데이터 흐름

Redux의 구성요소는 아래와 같습니다.

  1. View

    유저에게 보이는 UI를 의미를 의미하며, store의 state를 기반으로 그려집니다.

  2. Action

    type property를 가지고 있는 자바스크립트 객체입니다. Action 객체는 애플리케이션 내에서 어떤 일이 일어났는지를 묘사하는 객체로 생각할 수 있습니다.

    type property는 어떤 변화가 발생했는지 묘사하는 string 입니다. 통상 domain/eventName 의 형태를 따릅니다. 첫번째 domain 파트는 이 이벤트가 어떤 카테고리에 속하는지 표시하기 위함이며, eventName은 어떤 일이 발생했는지를 표현하는 부분입니다.

    Action 객체는 type property는 필수적으로 포함하고 있어야 하며, 그 외에 추가적으로 전달할 데이터가 있을 시 다른 property를 객체 안에 포함시킬 수 있습니다. 통상적으로 추가적인 정보를 전달하는 property의 이름은 payload 로 표현합니다.

    {
    	type: 'TODO/ADD_TODO',
    	payload:"Learn Redux"
    }
    
    {
      type: 'TODO/SET_VISIBILITY_FILTER',
      filter: 'SHOW_COMPLETED'
    }
  1. Action Creator

    Action Creator는 Action을 생성하는 함수입니다. 매번 액션 객체를 손수 작성하는 것은 중복이며, 번거롭고, 실수할 여지가 많은 작업이기에 Action Creator를 통해서 생성하는 것이 권장됩니다.

    const addTodo = todo => {
    	return {
    		type: 'TODO/ADD_TODO',
    		payload:todo
    	}
    }
  1. Reducer

    Reducer는 이전 state 값과, action 객체를 인자로 받아서 새로운 state를 리턴하는 순수 함수입니다. Reducer를 단순화 해서 표현하자면 (state, action) => newState 의 형태로 표현할 수 있습니다.

    Reducer는 아래의 원칙을 따라야 합니다.

    • 새로운 state는 오로지 기존의 state와 action 객체를 통해서만 계산되어야 한다. 그 외의 요소들에 영향을 받아서는 안된다.
    • Reducer는 기존의 state를 수정해서는 안된다, 기존의 state를 복사하고, 복사한 state에 변화를 발생시킨 후 return 하는식으로 동작해야 한다.
    • Reducer 내부에서는 비동기 통신, 랜덤 값을 사용하는 것 등의 그 어떤 사이드 이펙트도 수행되서는 안된다.

    Reducer 함수는 일반적으로 아래의 과정을 수행합니다.

    1. Reducer가 현재 전달받은 Action을 처리할 수 있는지 판단한다.
      1. 처리할 수 있을 경우 state와 action을 통해서 새로운 state 객체를 만든 뒤 리턴한다.
    2. 처리할 수 없는 Action인 경우에는 기존의 state를 그대로 리턴한다.
    const INITIAL_STATE = { value: 0 }
    
    function counterReducer(state = INITIAL_STATE, action) {
      // Reducer가 현재 전달받은 Action을 처리할 수 있는지 판단한다.
      if (action.type === 'counter/increment') {
        // 처리할 수 있을 경우 state와 action을 통해서 새로운 state 객체를 만든 뒤 리턴한다.
        return {
          ...state,
          value: state.value + 1
        }
      }
    
      // 처리할 수 없는 Action인 경우에는 기존의 state를 그대로 리턴한다.
      return state
    }
  1. Store

    Store는 Redux의 모든 state를 관리하는 객체입니다. Redux에서 store는 createStore 함수에 reducer를 인자로 넣으면서 호출해서 만들 수 있습니다.

    store는 getState란 메소드를 가지고 있으며 이를 통해 현재의 state값을 가져올 수 있습니다.

    import { countReducer } from 'redux'
    // root reducer(visibilityFilter, todos)를 통합해서 rootReducer 생성
    const reducer = combineReducers({ countReducer })
    
    // root reducer를 통해서 store 생성
    const store = createStore(reducer)
    
    // getState 메서드를 통해 현재 state 획득
    store.getState() // {value: 0}
  1. Dispatch

    Dispatch는 Store객체에 포함되어있는 메소드입니다. 이 메소드를 통해서 Action 객체를 Store에 전달할 수 있습니다. Dispatch를 통해서 Action이 전달되면 Store는 Reducer를 통해서 새로운 state를 만들어냅니다.

    store.dispatch({ type: 'counter/increment' })
    
    store.getState() // {value: 1}
    
    store.dispatch({ type: 'counter/increment' })
    
    store.getState() // {value: 2}
  1. Selectors

    Selector는 store에서 특정한 state만 가져오기 위한 함수입니다. 단일 store에 모든 state를 담아두기에 애플리케이션이 커질수록 store는 비대해지고, View에서는 이중에서 필요한 state만 가져오는 동작을 계속해서 수행하게 됩니다. 이 동작을 매번 손수 반복하지 않기 위해서 selector 함수를 이용합니다.

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

Redux의 데이터 흐름

0개의 댓글