React Redux 파헤치기

2023. 5. 18. 16:55프론트엔드/ReduxJS

Design Pattern

디자인패턴 : 소프트웨어를 설계하면서 자주 발생하는 문제에대한 모범답안

애플리케이션 전체를 다루기 위한 디자인 패턴들은 통상 여러 작은 범위의 디자인 패턴들을 함께 사용해서 만들어지기에 복합패턴이라고도 부름.

→ 그 중 모든 복합 패턴의 근간이라고 부를수 있는 패턴은 mvc 패턴

MVC

MVC 패턴은 애플리케이션을 Model, View, Controller 세가지 구성요소로 나눠서 설계하는 패턴입니다.

구성요소들의 역할은 아래와 같습니다.

  1. Model: 데이터의 형태를 정의하고, 데이터를 수정하는 역할을 담당
  2. Controller: 유저의 입력을 받아서 애플리케이션 내에서 어떻게 처리할지 판단 및 가공해서 모델 또는 뷰를 조작
  3. View: 모델을 UI로 표현, 사용자의 입력을 받아서 Controller에 전달

mvc 패턴에서 각 구성요소들은 양방향 통신이다.

mvc 패턴은 애플리케이션의 구성요소를 역할에 따라 분리했다, 그리고 모든 애플리케이션은 결국 본질적으로 데이터를 잘 조작해 화면에 보여주는 것이기 때문에 모든 디자인 페턴의 대부, 근본 MVC 패턴이 됐다.

하지만 프론트 분야에서 MVC 패턴을 그대로 사용하기에는 한계가 있음. → 애플리케이션의 규모가 커짐에 따라 특정 모델과 뷰가 양방향으로 소통할때 연쇄적인 변화가 발생하게 되고 결국 애플리케이션의 동작흐름을 분석하거나 예측할 수 없는 문제가 발생하게 된다.

FLUX

flux의 핵심은 단방향이다. 앞서 문제의 원인을 양방향으로 인한 연쇄적인 변화로 규정했기 때문에 자연스레 flux는 단방향으로 애플리케이션의 변화의 흐름을 최대한 단순화. 예측가능하게 하는데에 목표를 두었음.

flux는 4가지 구성요소로 이루어져 있다.

flux 패턴

  • Action: 어떤 변화를 발생시킬지 정의하는 type property와 변화에 필요한 데이터를 담고있는 단순한 객체입니다.
  • Dispatcher: Action을 받아서 모든 Store에 전달하는 역할을 수행합니다.
  • Store: 애플리케이션의 데이터를 저장하고, Dispatch에 전달된 Action에 따라 수정합니다.
  • View: Store에 저장된 데이터를 받아서, UI로 표현하고, 유저의 동작에 따라서 Action을 생성합니다.

단방향성으로 인해 애플리케이션은 특정한 순서에 따라서만 데이터와 UI가 변화하게 되었고 개발자들은 애플리케이션에서 어떤 변화가 일어나는지 파악하기가 쉬워졌으며, 나아가 애플리케이션의 동작을 예측하기도 쉬워졌다.

Redux

하지만 현재 Flux 패턴을 근간으로 하는 라이브러리의 표준은 Redux로 정립!

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

CQRS란 Command and Query Responsibility Segregation 의 약자로, 데이터 저장소로부터의 읽기와 업데이트 작업을 분리하는 패턴을 말한다.

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
    }
  ]
}
  1. State is read-only

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

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'
})
  1. 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함로 분리해서 코드를 작성한 다음 하나의 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의 구성요소와 데이터 흐름

  1. view
  2. 유저에게 보이는 ui를 의미 → store의 state를 기반으로 그려짐
  3. Actiontype property는 어떤 변화가 발생했는지 묘사하는 string 입니다. 통상 domain/eventName 의 형태를 따릅니다. 첫번째 domain 파트는 이 이벤트가 어떤 카테고리에 속하는지 표시하기 위함이며, eventName은 어떤 일이 발생했는지를 표현하는 부분입니다.
    {
    	type: 'TODO/ADD_TODO',
    	payload:"Learn Redux"
    }
    
    {
      type: 'TODO/SET_VISIBILITY_FILTER',
      filter: 'SHOW_COMPLETED'
    }
    
  4. Action 객체는 type property는 필수적으로 포함하고 있어야 하며, 그 외에 추가적으로 전달할 데이터가 있을 시 다른 property를 객체 안에 포함시킬 수 있습니다. 통상적으로 추가적인 정보를 전달하는 property의 이름은 payload 로 표현합니다. ( 이건 컨벤션이다. 굳이 정해져 있는건 아님)
  5. type property를 가지고 있는 자바스크립트 객체입니다. Action 객체는 애플리케이션 내에서 어떤 일이 일어났는지를 묘사하는 객체로 생각할 수 있습니다.
  6. Action Creator
    const addTodo = todo => {
    	return {
    		type: 'TODO/ADD_TODO',
    		payload:todo
    	}
    }
    
  7. Action Creator는 Action을 생성하는 함수 매번 액션객체를 손수 작성하는것은 중복이며, 번거롭고, 실수할 여지가 많은 작업이기에 Action Creator를 통해서 생성하는 것이 권장됨 ( 타입스크립트 사용하면서 점점 없어지는 추세)
  8. ReducerReducer는 아래의 원칙을 따라야 한다.
    • 새로운 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
    }
    
  9. Reducer는 이전 state 값과, action 객체를 인자로 받아서 새로운 state를 리턴하는 순수 함수 임. Reducer를 단순화 해서 표현하자면 (state, action ) ⇒ newState 의 형태로 표현할 수 있음.
  10. StoreRedux에서 store는 createStore 함수에 reducer를 인자로 넣으면서 호출해서 만들 수 있음.
  11. store는 getState란 메소드를 가지고 있으며 이를 통해 현재의 state값을 가져올 수 있음.
  12. Store는 Redux의 모든 state를 관리하는 객체
  13. Dispatch
    store.dispatch({ type: 'counter/increment' })
    
    store.getState() // {value: 1}
    
    store.dispatch({ type: 'counter/increment' })
    
    store.getState() // {value: 2}
    
  14. Dispatch는 Store객체에 포함되어있는 메소드입니다. 이 메소드를 통해서 Action 객체를 Store에 전달할 수 있습니다. Dispatch를 통해서 Action이 전달되면 Store는 Reducer를 통해서 새로운 state를 만들어냅니다.
  15. Selectors단일 store에 모든 state를 담아두기에 애플리케이션이 커질수록 store는 비대해짐이 동작을 매번 손수 반복하지 않기 위해서 selector 함수를 이용함.
  16. const selectCounterValue = state => state.value const currentValue = selectCounterValue(store.getState()) console.log(currentValue) // 2
  17. View에서는 이중에서 필요한 state만 가져오는 동작을 계속해서 수행하게 된다.
  18. Selector는 store에서 특정한 state만 가져오기 위한 함수

React Redux

Redux는 자바스크립트 앱을 위한 예측 가능한 상태 컨테이너

React전용이 아닌 모든 자바스크립트 앱에 활용될 수 있다는 의미임

React 또한 자바스크립트이기에 Redux를 사용할 수 있습니다. React에서 Redux를 사용하게 되면 1.전역 상태 관리, 2. Props drilling 문제를 해결할 수 있음

Redux는 어떤 자바스크립트 앱이든 사용할 수 있도록 범용성을 갖추고 있습니다. 하지만 이를 반대로 생각해보면 React에 최적화되지 않았다는 것을 의미합니다. 이러한 이유로 인해 Redux를 React와 통합하기 위해서는 Redux의 store에 저장된 state를 React 컴포넌트에서 가져올 수 있게 해주고, redux state가 변경될 경우 react state와 마찬가지로 리렌더링 해주는 등의 복잡한 과정을 수행해줘야 합니다.

react-redux가 제공하는 기능

  1. provider
    // 일부 코드 생략
    
    import { Provider } from 'react-redux';
    import store from './store/index';
    import App from './App';
    
    root.render(
      <Provider store={store}>
        <App />
      </Provider>
    );
    
  2. React 컴포넌트들에게 Redux Store에 접근할 수 있는 기능을 제공해주는 컴포넌트입니다. 내부적으로 Context API를 활용하고 있습니다.
  3. useSelector리렌더링 기능도 함께 제공
  4. import { useSelector } from 'react-redux'; const Counter = () => { const count = useSelector((state) => state.value); return <h1>{count}</h1>; };
  5. 컴포넌트에서 Redux Store의 값을 가져올 수 있는 hook입니다. Redux의 Selector를 React Hook으로 표현한 형태
  6. useDispatch
    import { useSelector, useDispatch } from 'react-redux';
    
    const Count = () => {
      const count = useSelector((state) => state.counter);
      const dispatch = useDispatch();
    
     const increase = () => {
        dispatch({ type: 'counter/increment' });
      };
    
      return (
        <div>
          <h1>{count}</h1>
          <button onClick={increase}>increment</button>
        </div>
      );
    };
    
    export default Count;
    
    Middleware ?서버 프레임워크에서는 흔히 미들웨어에서 CORS 관련 설정, 로깅 등의 목적으로 활용된다.리덕스의 미들웨어도 마찬가지, 서버단의 미들웨어와 다른점은 서버의 미들웨어는 요청과 응답 사이에서 동작하지만 리덕스의 미들웨어는 액션이 디스패칭되어서 reducer에 전달되는 과정 사이에서 동작한다는 점이다.
  7. 미들웨어의 가장 큰 특징은 “연결" 할 수 있다는 점입니다. 각각의 미들웨어는 서로 독립적이며, 프레임워크 안에 여러개의 미들웨어를 추가해서 연결할 수 있습니다. 이로 인해 개발자는 미들웨어를 기반으로 일련의 흐름을 작성하듯이 프로그램을 설계할 수 있게 됩니다.
  8. 미들웨어란 무엇일까요? 미들웨어는 “프레임 워크의 요청과 응답 사이에 추가할 수 있는 코드” 라고 생각할 수 있다.
  9. 컴포넌트에서 action을 store에 보내기 위한 dispatch 함수를 가져올 수 있는 hook

'프론트엔드 > ReduxJS' 카테고리의 다른 글

Core Redux concepts  (2) 2022.03.03