REDUX

REDUX

  • 상태 관리 라이브러리
  • 규모가 큰 애플리케이션에서, 컴포넌트 내에서 상태를 관리하는 것은 복잡하고 유지보수가 어렵다.
  • 이 문제를 해결하기 위해 Angular, Ember, backgone과 같은 프레임워크와 라이브러리들이 나왔다. 그러나 특히 Angular1의 경우 양방향 데이터 흐름(bidirectional data flow)을 사용한다.
  • React 컴포넌트와 분리해서 상태를 관리하는 Flux architecture가 도입되었는데, unidirectional data flow(단방향 데이터 흐름)으로 데이터가 한 방향으로만 흐른다.
  • Redux는 Flux architecture를 이은 상태 관리 라이브러리이다.
  • Redux는 자바스크립트 앱에서 예측 가능한 상태 관리를 해주는 컨테이너로서, 리액트나 앵귤러같은 라이브러리와 상호작용하거나, 독자적으로 사용될 수 있다.


Flux architecture

  • Action, Dispatcher, Store, View
  • View: 컴포넌트 트리로서, 유저가 view와 상호작용할 수 있다. 버튼 클릭 등의 이벤트를 통해 Action을 trigger한다.
  • Action: 상태 업데이트에 필요한 정보들을 담고있다.
  • Dispatcher: Action에서 Store로 데이터가 이동할 때의 운반책
  • Store: 상태가 관리되는 곳으로서, 새로운 상태는 View를 업데이트 시킨다.


Flux vs. Redux

  • store가 여러개 작용하는 Flux와 다르게 Redux에서는 오직 하나의 Store만 존재한다.
  • Redux는 하나의 Dispathcer 대신 여러 개의 Reducer를 사용한다.
  • Redux에서 Reducer가 Actions으로부터 새로운 상태 업데이트를 위한 정보를 받아 이전 상태와 함께 압축(reduce)한다.
  • Redux: Reducer + Flux


View -> Action -> Reducer(s) -> Store -> View


  • 단방향 데이터 흐름으로 loop를 돌고, view는 action을 트리거하기도 하며, store로부터 새로운 state를 받아 업데이트하기도 한다.
  • 즉 view는 state 변화에 따라 업데이트 되고, 또한 state 변화를 일으키기도 한다.


Action

  • 자바스크립트 객체로서, type을 갖고 있고 부가적인 payload를 가질 수 있다.
  • type은 string 타입이며, 다른 payload들은 어떤 타입이든 부여될 수 있다.
{
  type: 'Add_To_Basket',
  id: 0
}
  • dispatching: Action을 실행하는 것. store에 있는 state를 변경하기 위해 action을 실행할 수 있다.
  • Action 실행, 즉 dispatching은 view의 버튼 클릭 등에 의해 일어날 수 있다.
  • dispatching이 일어나면 action의 정보들이 reducer를 통하게 된다.


Reducer

  • 순수 함수(pure function)이기 때문에, 같은 input이 들어오면 같은 output을 도출한다.
  • view의 이벤트로 인해 Action 객체가 이러한 reducer 함수들로 전달된다.
  • Reducer의 parameter: state(이전 상태), action
  • 첫 번째 인자인 state는 store에 있는 전역 상태 객체
  • 두 번째 인자 action은 type과 추가적인 payload를 가지고 전송된 객체
  • Reducer 함수는 이전 상태와 action 객체를 압축하여 새로운 상태 객체를 반환한다. 이 때 이전 상태는 immutable하게 다뤄지며 pure function이기 때문에 side-effect가 없다.
  • state는 immutable 해야하기 때문에, push 대신 concat을 사용한다. push의 경우 state가 변경된다. concat의 경우 이전 state는 온전한 채로 새로운 상태 객체를 리턴할 수 있다.
// wrong
function reducer(state, action) {
  state.push(action.id)
  return state;
}

// do it like this!
function reducer(state, action) {
  return state.concat(action.id)
}
  • action 객체가 reducer에 전달되면 action 객체의 type을 분석한다. reducer가 해당 action type에 대한 조건을 가지고 있을 때만 새로운 상태 객체를 반환한다. reducer가 다루지 않는 action type을 전달받을 경우 이전 상태를 반환한다.
  • 여러 action type들에 대해 reducer는 다른 상태를 반환할 수 있는데, 이에 대한 조건은 switch case를 통해 분기할 수 있다.
  • 조건에 해당되지 않는 action type에 대비하여 default를 정할 수 있다.
function reducer(state, action) {
  switch(action.type) {
    case 'ADD_TO_BASKET':
      return {
        // do something
      }
    case 'REMOVE_FROM_BASKET':
      return {
        // do something
      }
    default:
      return state;
  }
}


상태 업데이트에 자주 쓰이는 메소드

  • map은 state를 mapping하면서 새로운 객체(배열)를 반환한다. 이전 state는 유지된다.
  • Object.create() 메소드는 파라미터로 전달된 객체들을 병합하면서 새로운 객체를 반환한다. 같은 키를 가진 객체의 경우 나중에 들어오는 객체의 값(value)으로 대체된다.


Store

  • 전역 상태(state) 객체를 보관하는 공간으로, Redux 내에 하나의 store만 존재하며, store는 하나의 상태만 가지고 있다.
  • 앞서 살펴 본 action, reducer는 자바스크립트인 데 반해 store는 redux 라이브러리데 종속되어 있으므로 사용을 위해선 import(불러오기)를 통해 새로운 store 객체(인스턴스)를 생성해야한다.
  • createStore 메소드는 reducer를 파라미터로 받는다.
import { createStore } from 'redux';
const store = createStore(reducer);
  • createStore는 선택적으로 두번째 파라미터를 받는데, 여기서 상태를 초기화시킬 수 있다.
const store = createStore(reducer, [])
  • react와 함께 사용할 경우 reducer를 통해 상태 초기화를 할 수 있다.


dispatch

  • action 객체를 전송하여 변화된 상태(state)를 store가 받고 이러한 업데이트를 view에 전달하기 위해서 store와의 상호작용이 필요하다.
  • 이렇게 store에 reducer를 인식시킨 후에 action 객체의 전송(dispatch)이 이루어진다.
// dispatch an action
store.dispatch({
  type: 'REMOVE_FROM_BASKET',
  id: 0
})

// store의 global state 불러오기
store.getState();

// store의 활성화(subscribe)/비활성화(unsubscribe)
// 상태가 바뀔 때 마다 활성화 => 새로운 상태를 불러오게 된다.
const unsubscribe = store.subscribe(() => {
  console.log(store.getState());
});
unsubscribe()


React-Redux connect

  • Provider 컴포넌트를 사용하여 Redux store가 React 컴포넌트에서 사용가능하도록 한다.
import { Provider } from 'react-redux';

ReactDOM.render(
  <Provider store={store}>
    <App/>
  </Provider>,
  document.getElementByI('root')
)
  • App 컴포넌트가 직접적으로 props를 받지 않는다.
//..
import Stories from './Stories';

const App = () => {
  <div className="app">
    <Stories />
  </div>
}

export default App;
  • Stories 컴포넌트는 App으로부터 어떠한 props도 전달받지 않았지만 props를 사용할 수 있다.
  • Stories 컴포넌트가 redux state와 action에 연결되어 접근할 수 있는 컴포넌트로 업그레이드 할 수 있기 때문이다.(HOC; 고차 컴포넌트) 이는 mapStateToProps와 mapDispatchToProps 메소드를 통해 이루어진다.
import { connect } from 'react-redux';

const mapStateToProps = (state) => {
  // redux store에서 react 컴포넌트로 state의 일부를 props로 매핑
}

const mapDispatchToProps = (dispatch) => {
  // props를 통해 react 컴포넌트로 redux action을 함수 형태로 전달
}

export default connect(
  mapStateToProps,
  mapDispatchToProps
)(Stories)


mapStateToProps

  • redux store의 전역 state에 접근
  • redux store의 업데이트를 listen하는 상태가 됨(subscribe)
  • store subscription이 상태 변화를 감지하면 mapStateToProps 함수가 실행된다.
  • 첫 번째 파라미터로 redux store의 state 객체를 전달받는다.
  • 두 번째 파라미터는 선택적이며, 부모 컴포넌트의 props를 받는다.
  • 전역 state의 객체를 리턴하고, 선택적으로 부모 컴포넌트의 props 객체도 반환할 수 있다.


mapDispatchToProps

  • redux store의 dispatch 메소드에 접근
  • action을 dispatch할 수 있다.
  • 함수를 부모 컴포넌트에 전달하여 state를 변경할 수 있다.
  • 마찬가지로 두 번째 파라미터로 props를 선택적으로 전달받아 action 객체에 포함시킬 수 있다.


View -> (mapDispatchToProps) -> Action -> Reducer(s) -> Store -> (mapStateToProps) -> View


Redux Hooks

useSelector()

const result : any = useSelector(selector : Function, equalityFn?: Function)

const counter = useSelector(state => state.counter);
  • selector 함수를 통해 Redux store의 state에 접근
  • mapStateToProps와 유사
  • 컴포넌트가 렌더링될 때마다 redux store의 state와 함께 호출된다.
  • 한 컴포넌트 내에서 여러 번 사용될 수 있다.
  • 이전 selector 리턴값과 현재 리턴값을 엄격 비교(strict comparison; ===)하여 값이 다를 때만 리렌더링한다.
  • store에서 값을 여러 번 불러올 경우 엄격 비교는 같은 값이어도 계속 리랜더링을 유발할 수 있다. 이를 방지하기 위해 비교함수로 shallowEqual 함수를 쓸 수 있다.
import { shallowEqual, useSelector } from 'react-redux';

const selectedData = useSelector(selectorReturningObject, shallowEqual)


useDispatch()

  • Redux store의 dispatch 메소드에 대한 참조를 반환
  • action 객체를 발송(dispatch)할 수 있다.
import { useDispatch } from 'react-redux';

export const Counter = ({ value }) => {
  const dispatch = useDispatch()
  
  return (
    <div>
      <span>{value}</span>
      <button onClick={() => dispatch({ type: 'NUMBER_UP' })}
        Increase number
      </button>
    </div>
  )
}

Categories:

REDUX

Load Comments