우선 CRA 환경에서 redux 실습을 진행하였다. 전역 상태 값으로 number를 두고, +, -, reset 버튼을 만들어서 카운트를 변경하는 예제를 만들어 보았다. react-redux의 기본적인 연결 방법과, redux hook을 적용하여 코드를 단축시켰을 때 어떻게 달라지는지 보자.

 

 

1. 설치

npm i redux react-redux

 

 

2. redux 디렉토리 생성 및 하위 파일 생성.

 

 src 밑에 redux 디렉토리를 생성한다. 그 다음 actionCreator, actionTypes, store 파일을 생성한다.

 

 

 

3. action type 생성 (src/redux/actionTypes.js)

 

아래와 같이 카운트를 증가, 감소, 리셋 시킬 때의 액션 상수를 정의한다.

export const INCREMENT = 'INCREMENT';
export const DECREMENT = 'DECREMENT';
export const RESET = 'RESET';

 

 

4. action creator 생성 (src/redux/actionCreator.js)

 

action creator는 액션 타입과, payload를 받아서 action 객체를 리턴한다. 지금 예제에서는 payload에 따로 전달하는 값이 없으므로 간단하게 type만 리턴하도록 한다.

import { INCREMENT, DECREMENT, RESET } from './actionTypes';

export const increment = () => {
  return {
    type: INCREMENT,
  };
};

export const decrement = () => {
  return {
    type: DECREMENT,
  };
};

export const reset = () => {
  return {
    type: RESET,
  };
};

 

 

5. store 생성 (src/redux/store.js)

 

store에는 초기 number값을 0으로 갖는 초기 상태 값을 정의한다. 그 후 createStore 함수의 첫 번째 인자로 reducer를 전달한다. 두 번째 인자로 전달한 값은 chrome redux 확장 프로그램으로 redux의 동작을 확인할 때 쓰는 인자이므로, 넣지 않아도 동작한다.

 

INCREMENT 액션은 number 값을 1 증가시키고, DECREMENT 액션은 number 값은 1 감소시킨다. RESET 액션은 number 값을 0으로 초기화한다. reducer를 통해 가공된 state이 store의 새로운 state이 된다.

import { createStore } from 'redux';
import { INCREMENT, DECREMENT, RESET } from './actionTypes';

const initialState = {
  number: 0,
};

export default createStore((state = initialState, action) => {
  const { type, payload } = action;
  if (type === INCREMENT) {
    return { ...state, number: state.number + 1 };
  }
  if (type === DECREMENT) {
    return { ...state, number: state.number - 1 };
  }
  if (type === RESET) {
    return { ...state, number: 0 };
  }
  return state;
}, window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__());

 

6. 리액트에 store 주입 (src/index.js)

 

react-redux 모듈로부터 Provider를 import한 다음, 기존의 <App/> 컴포넌트를 감싸서 태그에 추가한다. 이 때, store를 props로 전달하면 App 컴포넌트에 store가 주입된다.

import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import { Provider } from 'react-redux';
import store from '../src/redux/store';

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

 

7. Counter 컴포넌트 구현 (src/index.js)

 

컴포넌트에 state과 dispatch를 주입하기 위해서는 react-redux의 connect 함수를 사용한다. connect 함수에 첫 번째 인자로는 mapStateToProps, 두 번째 인자로는 mapDispatchToProps가 들어간다. 두 값 모두 필수 값은 아니고, 필요 없는 인자는 null로 전달해도된다.

 

그렇게 connect에 인자를 전달하고 호출하면 새로운 함수가 반환되는데, 이 함수에 Counter 컴포넌트를 전달하여 실행한 결과를 export default하면 된다. 자세한건 아래 코드를 보자.

import React from 'react';
import { connect } from 'react-redux';
import { increment, decrement, reset } from '../redux/actionCreators';

const Counter = props => {
  const { number, increment, decrement, reset } = props;

  console.log('render');
  return (
    <div>
      <input readOnly value={number} />
      <input type="button" onClick={increment} value="+" />
      <input type="button" onClick={decrement} value="-" />
      <input type="button" onClick={reset} value="reset" />
    </div>
  );
};

const mapStateToProps = state => ({ number: state.number });
const mapDispatchToProps = { increment, decrement, reset };

export default connect(mapStateToProps, mapDispatchToProps)(Counter);

 

- 위의 코드에서 mapStateToProps, mapDispatchToProps 역할

 

(1) mapStateToProps

 

이 함수는 state을 매개변수로 받고, 컴포넌트가 props로 받고 싶은 값을 리턴하면 된다. 아래 코드처럼 작성하면 store의 state에서 number을 props에 number로 전달하는 함수가 된다. 리턴하는 값이 통째로 props가 되어 컴포넌트한테 전달된다고 생각하면 쉽다. 다시 말하자면 아래 함수는 { number : state.number } 라는 객체를 props로 전달하고 있는 것이다. 

 

const mapStateToProps = state => ({ number: state.number });

 

이렇게 mapStateToProps를 connect 함수에 첫 번째 인자로 전달하고 나면, 컴포넌트는 number의 값이 변하면 컴포넌트를 다시 렌더링한다. 즉, redux store를 subscribe 한 것과 같다. 이렇게 연결을 하고나면 store의 state이 바뀔 때마다 mapStateToProps로 연결한 '모든' 컴포넌트의 mapStateToProps가 실행된다. 그래서 공식 문서를 보면 mapStateToProps는 최대한 빨라야 한다고 써있다. expensive한 연산인 경우 데이터가 변경되지 않을 때 수행하지 않도록 해야 성능에 지장이 생기지 않는다.

 

참고) mapStateToProps의 두 번째 매개변수로 ownProps를 작성하는 경우도 있다. 이 경우 ownProps에는 해당 컴포넌트에 직접 주입한 props값이 전달된다.

 

ex) 아래 코드처럼 test 라는 prop를 전달하면, ownProps에는 { test : 1 } 이 전달된다.

<Counter test={1} />

 

아래는 mapStateToProps의 매개변수에 따른 호출 조건이다.

 

(2) mapDispatchToProps

 

이 함수는 2가지 형태로 작성할 수 있다. 위에서 작성한 코드는 축약형이고, 원래대로 길게 작성하면 아래와 같다.

 

1) 원래대로 작성

const mapDispatchToProps = (dispatch, ownProps) => ({
  increment: () => dispatch(increment()),
  decrement: () => dispatch(decrement()),
  reset: () => dispatch(reset()),
});

위의 코드에서 increment, decrement, reset은 모두 acionCreator.js에서 가져온 action creator이다. 객체의 속성 값과 action creator의 이름을 동일하게 지정하면 되고, 그 속성 값으로는 위의 코드 처럼 dispatch에 action creator를 실행한 객체를 전달하면 된다.

 

 

2) 축약형

const mapDispatchToProps = { increment, decrement, reset };

 

1)과 2)는 동일하게 동작한다. 다만, 2) 처럼 작성하면 redux가 1)의 방법으로 작성한 것 처럼 객체의 속성값에 매핑해준다.

 

mapDispatchToProps를 connect 함수에 전달하면, 해당 컴포넌트를 store에게 dispatch를 할 수 있게된다. 만약 mapDispatchToProps를 null로 전달한다면, 해당 컴포넌트는 default로 dispatch 함수 자체를 props에 전달받는다.

 

 

 

8. App 컴포넌트에 Counter 컴포넌트 추가 (src/App.js)

import React from 'react';
import './App.css';
import Counter from './components/Counter';

function App() {
  return (
    <div className="App">
      <Counter />
    </div>
  );
}

export default App;

 

9. 실행

 

아래 명령으로 앱을 실행하면 카운터가 잘 동작하는 것을 볼 수 있다.

npm start

 

 

10. redux hook을 이용한 방법 (src/components/Counter.js)

 

react-redux의 hook을 이용하면 mapStateToProps와 mapDispatchToProps를 작성하지 않을 수 있다. 지금은 간단한 코드라서 큰 차이가 없지만, 액션이 다양해지는 경우 mapStateToProps와 mapDispatchToProps를 작성하는 보일러 플레이트 코드를 굉징히 커질 수 있다. hook을 통해 아래 처럼 간략하게 작성해보자.

 

const Counter = () => {
  const number = useSelector(state => state.number);
  const dispatch = useDispatch();

  const handleIncrement = useCallback(() => dispatch(increment()), [dispatch]);
  const handleDecrement = useCallback(() => dispatch(decrement()), [dispatch]);
  const handleReset = useCallback(() => dispatch(reset()), [dispatch]);

  console.log('render', number);
  return (
    <div>
      <input readOnly value={number} />
      <input type="button" onClick={handleIncrement} value="+" />
      <input type="button" onClick={handleDecrement} value="-" />
      <input type="button" onClick={handleReset} value="reset" />
    </div>
  );
};

 

1) useSelector

 

useSelector는 store의 state에서 원하는 부분을 전달받도록 할 수 있다. mapStateToProps가 하는 역할과 동일하다. 

 

2) useDispatch

 

useDispatch함수를 실행하고 리턴되는 dispatch를 통해 store에 액션을 dispatch 할 수 있다. 위에서 useCallback을 이용해 dispatch 함수의 변화가 생기지 않으면 handle... 으로 시작되는 함수들을 다시 생성하지 않도록 메모이제이션 하였다. 해당 내용은 아래 문서를 참고하면 좋을 것 같다.

 

react-redux.js.org/api/hooks

 

 

 

참고) hook을 이용한 방법과 connect의 차이에 관한 내용

 

useSelector는 기본적으로 action이 dispatch된 후 selected value가 바뀐경우에만 재 렌더링 하는 방식으로 동작한다. 하지만 useSelector는 connect와 다르게  부모 컴포넌트의 재 렌더링으로부터 자식 컴포넌트가 재 렌더링되는 것을 막지는 못한다. (props가 변하지 않았음에도!)

 

이 경우에 성능 최적화가 필요하다면, React.memo를 이용해서 재 렌더링을 막을 수 있다. 

 

react-redux.js.org/api/hooks

 

출처

1. react-redux.js.org/

2. redux.js.org/

+ Recent posts