[Redux] redux 배우기

studying  · 20 mins read

리덕스란?

애플리케이션 상태를 관리하기 위한 open source Javascript 라이브러리이다.

React 또는 Angular와 같은 라이브러리와 함께 가장 일발적으로 사용된다.

리덕스를 사용하는 이유는?

여러 컴포넌트들의 state 관련 로직들을 다른 파일들로 분리시켜 효율적으로 state를 관리할 수 있게 되고, 한 state를 여러 컴포넌트끼리 공유할 때 여러 컴포넌트를 거치는 과정에서 해당 state를 공유하지 않는(필요로 하지 않는) 컴포넌트를 거치는 번거로운 작업 없이 손쉽게 state 값을 전달할 수 있다.

또한 리덕스의 middleware라는 기능을 통하여 비동기 작업, 로깅 등의 확장적인 작업들을 더욱 쉽게 할 수도 있게 해준다.

리덕스 개발자 도구(redux devtools)를 사용하여  현재 스토어의 상태를 개발자 도구에서 조회 할 수 있고 지금까지 어떤 액션들이 디스패치 되었는지, 그리고 액션에 따라 상태가 어떻게 변화했는지 확인 할 수 있다(시간여행 가능하게 해준다). 또한 액션을 직접 디스패치 할 수도 있다.

즉, 애플리케이션의 state 변화들을 debugging하는 데 사용되는 redux devtools를 사용할 수 있다.

리덕스에서 사용되는 중요한 키워드

  • action

상태에 변화를 주고 싶을 때 “action”을 발생시킨다.

“action”은 하나의 object로 표현된다.

“action” object는 type property를 반드시 포함해야 하고 나머지 property는 자유롭게 넣어줄 수 있다.

예시)

{

type: "INCREASE",

size: 1

}
  • action creator

위의 “action”을 만들어주는 함수이다.

입력 받은 parameter를 가지고 “action” object를 만들어 준다(return).

action creator를 사용하지 않고 action을 발생시킬 때마다 직접 action object를 typing할 수 있지만 action creator를 사용해서 action을 만들어 주어 컴포넌트에서 action을 쉽게 발생시킬 수 있다.

  • reducer

변화를 일으키는 함수이다.

“state”, “action“ parameter를 받아와서 새로운 state 값을 return해준다.

첫 번째 인자인 state는 현재의 상태를 의미하고, 두 번째 인자인 action은 발생한 action을 의미한다.

function reducer(state, action) {

// 상태 업데이트 로직

switch (action.type) {

case INCREASE:

return state + action.type;

default:

return state;

	}

}

redux의 reducer에서 default에는 기존 state를 그대로 return해주어야 한다.

  • 주의
  1. state를 update할 때 기존의 state를 건들지 않고 새로운 state를 생성하여 update하지 않고 직접 state를 조작하는 경우 state 값의 변경을 감지하지 못하게 되어 리렌더링을 할 수 없게 된다.

    또한 나중에 redux devtools를 통해 state값에 따라 시간을(렌더링을) 뒤로 돌리고 앞으로 돌리는 시간 여행을 할 수 없게 된다.

    따라서 state를 update하는 과정에서 배열을 업데이트 해야 할 때는 배열에 push하지 않고 concat 함수를 사용하여 새로운 배열을 만들어서 교체하거나 깊은 구조로 되어 있는 object를 업데이트 해야 할 때는 기존 객체를 건드리지 않고 Object.assign을 사용하거나 spread 연산자를 사용하여 업데이트 해야 한다.

  2. reducer는 순수한 함수여야 한다.

    즉, 똑 같은 parameter로 호출된 reducer는 항상 같은 값을 return해야 함다. new Date()나 랜덤숫자를 다루는 코드는 reducer 함수의 바깥에서 처리해줘야 한다(리덕스 미들웨어 사용).

  3. 한 프로젝트에 여러 개의 reducer가 있을 때는 redux 라이브러리에 내장된 combineReducers 함수를 사용하여 root reducer로 합쳐서 사용한다.

    const rootReducer = combineReducers({
      subReducer1,
    
      subReducer2,
    });
    
  • store

앱의 전체 state tree를 가지고 있고 몇가지 method를 가지고 있는 한 object이다.

우리의 애플리케이션의 전체 state tree를 가지고 있고 state에 변화를 일으키기 위한 유일한 방법은 store 내에서 action을 dispatch하는 것이다.

store object를 생성하기 위해서는 root reducer를 createStore라는 함수의 인자로 넘겨주게 되면 해당 reducer와 함께 store object를 반환해준다.

  • getState()

store의 첫 번째 내장 함수.

현재 state tree를 return해준다. store의 reducer에 의해 return된 마지막 state 값을 의미한다.

  • dispatch(action)

스토어의 두 번째 내장함수.

“action”을 parameter로 받아서 “action”을 발생시켜주는 함수이다.

state 변화를 발생시키는 유일한 방법이다.

dispatch 함수를 호출하면 store에서 현재 “state”값과 호출된 dispatch함수로 전달된 “action” 과 함께 reducer함수를 실행시켜 새로운 state를 만들어준다.

  • subscribe(listener)

store의 세 번째 내장함수.

change listener를 더해준다(구독해준다). 특정 함수를 parameter로 받아 “action”이 dispatch되어 state가 바뀔 때마다 해당 parameter 함수를 호출한다.

callback(listener 함수) 내에서 getState()를 호출하여 현재 상태 트리를 읽어보면 action이 dispatch되어 reducer에 의해 state값이 변경된 후의 state값을 확인할 수 있다.

subscribe 함수는 구독하려는 listener의 구독을 해지하는 함수를 return한다.

예시)

let unsubscribe = store.subscribe(handleChange);

unsubscribe(); // listener의 구독을 해지할 때 다음과 같이 호출

listener 안에서 dispatch 함수를 호출할 시 몇 가지 주의할 점이 있다.

  1. 아무런 조건 없이 dispatch 함수를 호출할 수 있지만 dispatch 함수가 호출될 때마다 다시 listener 함수가 호출되기 때문에 무한 루프가 발생하게 된다. 따라서 특정 조건 하에서만 listener 안에서 dispatch 함수를 호출해야 한다.
  2. dispatch() call로 인해 구독한 listener가 실행될 때 해당 dispatch()가 호출되기 전 시점의 것들이 사용된다. 만약 구독한 listener가 실행되는 중에 새로운 구독이나 구독 취소를 하더라도(listener 내에서) 현재 진행중인 dispatch()에는 영향을 미치지 않고, 다음 dispatch() call에서는 가장 최근의 구독 list가 사용된다.
  3. listener가 호출되기 전에 중첩된 여러 dispatch() call들이 state를 여러 번 update하는 경우 listener가 모든 state 변화들을 받아보는 것이 보장되지 않는다. 하지만 dispatch() call 전에 등록된 모든 구독은 listener가 호출되는 시점의 최신의 state 값을 받아보는 것이 보장 된다.(listener가 호출되는 시점까지의 여러 dispatch() call들 중 가장 나중의 call에 의해 변경된 state 값을 받게 된다.-> 확인해보기!!!!!)

보통 react에서 redux를 사용할 때 subscribe 함수를 직접 사용하지 않고 react-redux 라이브러리에서 제공하는 “connect” 함수나 “useSelector” hook을 사용하여 redux store의 state에 구독! 한다.

  • replaceReducer(nextReducer)

현재 store에서 사용중인 reducer를 교체한다. 코드 분할이나 동적으로 reducer를 불러오고 싶을 때 사용하거나 redux에서 핫리로딩을 구현하기 위해서 사용한다.

store가 앞으로 사용할 reducer를 parameter로 전달한다.

redux 사용 과정 한 눈에 담기

KakaoTalk_20210604_061558752

render함수가 실행되면 render함수에서(rendering하려는 컴포넌트 내부에서) getState함수를 호출시켜(1) store에서 state값을 가져와서(2, 3) 해당 ui를 rendering한다.(4,5)(step1,2,3,4,5를 거쳐)

store에 저장되어 있는 state 값이 바뀔 때마다 render함수를 호출하도록 하여

state값이 바뀔 때마다 ui가 갱신되게 하기 위해서 subscribe 함수를 사용한다.

render함수를 store의 subscribe에 등록 → state값이 바뀔 때마다 1번의 step들이 일어난

store.subscribe(render);

state object update 과정

ui(rendering된 화면에서) 클릭과 같은 event가 발생하여 “action”이 dispatch에게 전달되면(1, 2)

dispatch가 1. 현재의 state값과 “action”을 reducer의 parameter로 전달하여(3) reducer를 호출하여state값을 바꾸고(reducer가 새로운 state값을 return하여 state값이 변경)(4) 2.  subscribe에 등록되어 있는 구독자들을 다 호출해주어(5) render가 호출되어(6) 1번의 step들이 다시 일어나서 새로운 state에 맞게 ui(화면)이 갱신된다.

API reference

  • createStore

앱의 state tree 전체를 보관하는 redux store를 만들어 준다. 애플리케이션 내에는 단 하나의 스토어만 있어야 한다.

첫 번째 parameter로 reducer 함수를 parameter로 전달 받는다(require).

두 번째 인자로 preloadedState (any)를 전달 반는다(optional).

combineReducers로 루트 리듀서를 만들었을 때에는 이 인자는 전달했던 것과 같은 키 구조를 가지는 평범한 object여야 한다.

세 번째 인자로 enhancer 함수를 전달 받는다(optional). middleware나 시간 여행 등의 서드 파티 기능을 store에 추가하기 위해 지정할 수 있다.

(redux에서는 applyMiddleware()라는 enhancer만 제공)

앱의 전체 store tree를 가지고 있는 object인 store를 return해준다.

  • combineReducers

여러 reducer를 값으로 가지는 object를 parameter로 받아서 createStore에 넘길 수 있는 하나의 reducer(root reducer)로 바꿔주는 함수이다. 만들어진 root reducer는 subReducer들을 모두 호출하고 결과를 모아서 하나의 state object로 만들어 준다.

state object 형태는 reducers로 전달된 key들을 따른다.

예시)

const rootReducer = combineReducers({ subReducer1, subReducer2 } );

// rootReducer의 state object의 형태는 다음 아래와 같다.

{

subReducer1: 

subReducer2: 

}

ES6의 속성 단축 표기로 생각해 보면

combineReducers({

subReducer1: subReducer1,

subReducer2: subReducer2

}))

즉, state object의 형태가 reducers로 전달된 key들을 따르고 있는 걸 볼 수 있다.

예시 2)

const rootReducer = combineReducers({
  key1: subReducer1,

  key2: subReducer2,
});

위와 같이 호출하면 state object의 형태는 다음과 같다.

{

key1: 

key2: 

}
  • applyMiddleware

  • bindActionCreators

mapDispatchToProps함수에서 자동으로 액션 생성 함수에 dispatch가 감싸진 형태로 호출할 수 있다.

const mapDispatchToProps = (dispatch) => ({
  onIncrease: () => dispatch(increase()),

  onDecrease: () => dispatch(decrease()),

  onSetDiff: (diff) => dispatch(setDiff(diff)),
});

const mapDispatchToProps = (dispatch) =>
  // bindActionCreators 를 사용

  bindActionCreators(
    {
      increase,

      decrease,

      setDiff,
    },

    dispatch
  );

connect 함수에서는 mapDispatchToProps가 함수가 아니라 아예 객체형태일 때에는 bindActionCreators를 대신 호출

export default connect(
  (state) => ({
    number: state.counter.number,

    diff: state.counter.diff,
  }),

  {
    increase,

    decrease,

    setDiff,
  }
)(CounterContainer);

mapStateToProps의 두 번째 파라미터(optional) ownProps는 우리가 컨테이너 컴포넌트를 렌더링할 때 직접 넣어주는 props이다.

<CounterContainer myValue={1} /> 이라고 하면 { myValue: 1 } 값이 ownProps가 됨

이 두번째 파라미터는 다음과 같은 용도로 활용 할 수 있다.

const mapStateToProps = (state, ownProps) => ({
  todo: state.todos[ownProps.id],
});

리덕스에서 어떤 상태를 조회 할 지 설정하는 과정에서 현재 받아온 props에 따라 다른 상태를 조회 할 수 있다.

  • compose

redux 사용하기

  • redux 라이브러리 설치
yarn add redux

리덕스 모듈이란?

action type, action creator, reducer가 들어있는 js 파일을 의미한다.

각각 다른 파일에 저장할 수도 있지만 개발하는 데에 불편하다.

하나의 파일에 reducer와 action과 관련된 코드들을 몰아서 작성하는 패턴을 Ducks 패턴이라고 부른다.

모듈 만들기 예시

  • modules/module1.js
/* 액션 타입 */
// Ducks 패턴 -> 액션 이름에 접두사를 넣어 다른 모듈의 액션 이름이 겹치는 것을 방지
const ADD = " module1/ADD";
const SUBTRACT = " module1/ SUBTRACT ";

/* 액션 생성함수 */
export const add = () => ({ type: ADD });
export const subtract = () => ({ type: SUBTRACT });

/* state initialization */
const initialState = {
  number: 0,
  diff: 1,
};

/* reducer 선언 */
export default function subReducer1(state = initialState, action) {
  switch (action.type) {
    case ADD:
      return {
        ...state,
        number: state.number + state.diff,
      };
    case SUBTRACT:
      return {
        ...state,
        number: state.number - state.diff,
      };
    default:
      return state;
  }
}

state의 initialization을 다음과 같이 설정할 수도 있다.

function subReducer1(state, action) {
  if (state === undefined) {
    return { number: 0, diff: 1 };
  }
  switch (action.type) {
    case ADD:
      return {
        ...state,
        number: state.number + state.diff,
      };
    case SUBTRACT:
      return {
        ...state,
        number: state.number - state.diff,
      };
    default:
      return state;
  }
}
  • modules/module2.js
/* 액션 타입 */
// Ducks 패턴 -> 액션 이름에 접두사를 넣어 다른 모듈의 액션 이름이 겹치는 것을 방지
const MULTIPLY = " module2/ MULTIPLY ";
const DIVIDE = " module2/ DIVIDE ";

/* 액션 생성함수 */
export const multiply = () => ({ type: MULTIPLY });
export const divide = () => ({ type: DIVIDE });

/* state initialization */
const initialState = {
  number: 0,
  diff: 1,
};

/* reducer 선언 */
export default function subReducer2(state = initialState, action) {
  switch (action.type) {
    case MULTIPLY:
      return {
        ...state,
        number: state.number * state.diff,
      };
    case DIVIDE:
      return {
        ...state,
        number: state.number / state.diff,
      };
    default:
      return state;
  }
}

위의 module1, module2에서의 reducer들을 하나의 rootReducer로 만들기

  • modules/index.js
import { combineReducers } from "redux";
import subReducer1 from "./module1";
import subReducer2 from "./ module2";

const rootReducer = combineReducers({
  subReducer1,
  subReducer2,
});

export default rootReducer;

위의 rootReducer와 함께 store 만들기

  • src/index.js
import React from "react";
import ReactDOM from "react-dom";
import "./index.css";
import App from "./App";
import * as serviceWorker from "./serviceWorker";
import { createStore } from "redux";
import rootReducer from "./modules";

const store = createStore(rootReducer); //스토어 생성
console.log(store.getState());
//  {
// subReducer1: {
// number: 0,
//   diff: 1

// }
// subReducer2:{
// number: 0,
//   diff: 1
// }
// }

ReactDOM.render(<App />, document.getElementById("root"));

serviceWorker.unregister();

리액트 프로젝트에 리덕스를 적용 할 시 react-redux 라이브러리를 사용해야 한다.

react-redux 라이브러리 설치

yarn add react-redux

index.js 파일에서 react-redux 라이브러리에서 제공하는 Provider라는 컴포넌트를 불러와서 App 컴포넌트를 감싸준 후 Provider 컴포넌트의 props로 store를 넘겨 주면 렌더링하는 모든 컴포넌트에서 redux store에 접근 할 수 있게 된다.

예시)

import React from "react";
import ReactDOM from "react-dom";
import "./index.css";
import App from "./App";
import * as serviceWorker from "./serviceWorker";
import { createStore } from "redux";
import { Provider } from "react-redux";
import rootReducer from "./modules";

const store = createStore(rootReducer);

ReactDOM.render(
  <Provider store={store}>
    <App />
  </Provider>,
  document.getElementById("root")
);

serviceWorker.unregister();

Presentational Component vs Container Component

presentational component: redux store에 직접적으로 접근하지 않고 필요한 값이나 함수를 props로만 받아와서 사용하는 컴포넌트이다. 주로 UI를 선언하는 것에 집중할 때 사용된다.

container component: redux store의 state를 조회하거나 action을 dispatch할 수 있는 component이다. 주로 HTML 태그 사용 없이 다른 presentational component들을 불러와서 사용한다.

꼭 이런 형태로 컴포넌트들을 구분 지을 필요는 없다.

예시)

  • Counter.js
import React from "react";
// presentational component
function Counter({ number, diff, onIncrease, onDecrease }) {
  return (
    <div>
      <h1>{number}</h1>
      <div>
        <button onClick={onIncrease}>+</button>
        <button onClick={onDecrease}>-</button>
      </div>
    </div>
  );
}

export default Counter;
  • CounterContainer.js
import React from "react";
import { useSelector, useDispatch } from "react-redux";
import Counter from "../components/Counter";
import { increase, decrease } from "../modules/module1";

function CounterContainer() {
  // useSelector는 리덕스 스토어의 상태를 조회하는 Hook
  // state의 값은 store.getState() 함수를 호출했을 때 나타나는 결과물과 동일
  const { number, diff } = useSelector((state) => ({
    number: state.module1.number,
    diff: state.module1.diff,
  }));

  // useDispatch 는 리덕스 스토어의 dispatch 를 함수에서 사용 할 수 있게 해주는 Hook
  const dispatch = useDispatch();
  // 각 액션들을 디스패치하는 함수들을 만드세요
  const onIncrease = () => dispatch(increase());
  const onDecrease = () => dispatch(decrease());

  return (
    <Counter
      number={number}
      diff={diff}
      onIncrease={onIncrease}
      onDecrease={onDecrease}
    />
  );
}

export default CounterContainer;

리덕스 개발자 도구

현재 스토어의 상태를 개발자 도구에서 조회 할 수 있고 지금까지 어떤 액션들이 디스패치 되었는지, 그리고 액션에 따라 상태가 어떻게 변화했는지 확인 할 수 있다.

추가적으로, 액션을 직접 디스패치 할 수도 있다(시간여행 가능하게 해줌).

설치 방법

1. 크롬 웹 스토어에서 확장 프로그램을 설치

2. 프로젝트에 redux-devtools-extension 설치

yarn add redux-devtools-extension

3. js 파일에서

import { composeWithDevTools } from redux-devtools-extension;
const store = createStore(rootReducer, composeWithDevTools());
// 리덕스 개발자 도구 활성화

크롬 개발자 도구의 Redux 탭에서 시간 여행 가능!

api reference from react-redux library

  • Provider

Provider 컴포넌트는 redux store에 접근해야 하는 어느 중첩된 컴포넌트에서 redux store를 사용할 수 있도록 한다.

대부분의 애플리케이션에는 컴포넌트를 top level에서 rendering한다.

Props of Provider component

  1. store: 우리의 애플리케이션에서의 single Redux store
  2. children
  3. context
  • connect

React component를 redux store에 연결해주는 함수이다.

연결된 component에게 component가 필요로 하는 store에 있는 data를 제공해주고 action을 store에 dispatch할 수 있는 함수들을 제공해준다.

전달받은 component class를 변경시키지 않고 해당 component를 둘러싸는 새로운 connected component를 return해준다.

redux store의 state와 dispatch가 각각 mapStateProps, mapDispatchToProps 함수의 첫 번째 parameter로 전달된다.

mapStateToProps, mapDispatchToProps 함수들의 return값들은 각각 stateProps, dispatchProps로 지칭된다.

- syntax
function connect(mapStateToProps?, mapDispatchToProps?, mergeProps?, options?)

mergeProps parameter는 컴포넌트가 실제로 전달받게 될 props를 정의한다(이 parameter는 거의 사용x).

(stateProps, dispatchProps, ownProps) => Object

이 파라미터를 사용하지 않으면 { …ownProps, …stateProps, …dispatchProps }이다.

options parameter는 컨테이너 컴포넌트가 어떻게 동작할지에 대한 옵션이다.

이 옵션을 통하여 context 커스터마이징, 최적화를 위한 비교 작업 커스터마이징, 및 ref 관련 작업을 할 수 있다.

  • Hooks

react-redux 라이브러리는 자체의 custom hook api들을 포함하는데 이러한 hook들은 우리의 react component가 redux store에 구독하고 action들을 dispatch하도록 해준다.

기존의 connect API로도 state값을 조회할 수 있지만 custom hook api는 더 간단하고 typescript에서 더 잘 작동한다.

1. useSelector

redux store로부터 state 값을 조회할 수 는 hook이다. connect 함수를 통해 store로부터 state 값을 조회하는 것보다 훨씬 간결하게 코드를 작성할 수 있어 코드의 가독성이 좋아진다.

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

useSelector 함수의 첫 번째 인자인 selector 함수는 connect 함수의 mapStateToProps 함수와 비슷하다.

차이점:

  1. object 외에도 어느 value를 return할 수 있고 selector가 return하는 value는 useSelector() hook의 return값으로 사용된다.
  2. useSelector()은 redux store에 subscribe되어 action이 dispatch될 때마다 이전의 selector result value와 reference 비교를 하여 value가 다른 경우에 해당 컴포넌트를 리렌더링 하고 value가 이전의 result value와 같은 경우에는 컴포넌트가 리렌더링되지 않는다.
  3. mapStateToProps함수와 달리 ownProps argument를 받지 않지만

    closure을 통해서 혹은 curried selector를 사용해서 props가 사용될 수 있다.

  4. default로 strict(===) reference equality checks를 사용한다(not shallow equality)

(connect()에서는 shallow equality checks를 사용해서 리렌더링 할지를 결정한다.)

2. useDispatch

redux store로부터 dispatch 함수(reference)를 return해준다.

  • syntax
const dispatch = useDispatch();

dispatch함수는 컴포넌트에 전달되는 store instance가 동일한 instance라면 항상 stable하다. 그러나 React hooks lint rules는 dispatch가 stable해야 한다는 것을 알지 못하며 dispatch variable이 useEffect, useCallback에 대한 dependency array에 추가되어야 한다고 warning을 보낸다. 따라서 다음 아래 코드와 같이 dependency에 dispatch variable을 추가해주어 warning을 없앨 수 있다.

예시 1)

const dispatch = useDispatch();

const incrementCounter = useCallback(
  () => dispatch({ type: "increment-counter" }),

  [dispatch]
);

예시 2)

useEffect(() => {
  dispatch(fetchTodos());

  // Safe to add dispatch to the dependencies array
}, [dispatch]);

3. useStore

const store = useStore();

Provider 컴포넌트에 전달된 redux store와 동일한 store에 대한 reference를 return해준다.

이 hook은 자주 사용하지 말고 useSelector() hook을 사용하는 것을 권장(선호)한다.

이 hook은 reducer를 교체해야 할 때처럼 store에 접근해야 하는 드문 상황에서 유용할 수 있다.

References