Redux #6 리액트-리덕스 연동 컴포넌트 예시

  • 이번 포스팅은 리덕스를 사용할 때 컴포넌트를 어떤 식으로 나누어서 만드는지를 알아보고 container component에서 리덕스를 사용하고 presentational component에서 UI에만 집중하고 필요한 데이터를 props를 통해 받아와서 관심사의 분리를 하는 방법을 알아본다.
  • 아래 예시는 input에 숫자를 조절하면 diff값이 바뀌어서 +, -를 눌렀을 때 그 값만큼 변하는 예제다.
  • presentational component
    리덕스 스토어에 직접적으로 접근하지 않고 필요한 값, 함수를 props로만 받아와서 사용하는 컴포넌트
  • container component
    액션을 디스패치 하거나 store에 조회하는 컴포넌트

1. presentational component

components/Counter.js

import React from 'react';

export default function Counter({ number, diff, onIncrease, onDecrease, onSetDiff }) {
  const onChange = e => {
    onSetDiff(parseInt(e.target.value), 10); // 기본적으로 input의 value는 string이다. 그래서 변환 필요.
  };

  return (
    <div>
      <h1>{number}</h1>
      <div>
        <input type='number' value={diff} onChange={onChange} />;
        <button onClick={onIncrease}>+</button>
        <button onClick={onDecrease}>-</button>
      </div>
    </div>
  );
}

2. container component

  • useSelector는 상태를 조회.
    state는 현재 리덕스의 상태에서 어떤 것을 불러올지를 정해서 비구조화할당으로 꺼낸다.
  • useDispatch를 쓰면 dispatch를 사용할 수 있게 된다.
    함수를 만들어서 함수가 실행되면 액션이 디스패치되게 한다.

container/CounterContainer.js

import React from 'react';
import Counter from '../components/Counter';
// 상태를 조회하기 위해 useSelector 사용
// 액션을 dispatch하기 위해 useDispatch 사용
import { useSelector, useDispatch } from 'react-redux';
import { increase, decrease, setDiff } from '../modules/counter';

export default function CounterContainer() {
  const { number, diff } = useSelector(state => ({
    // state에는 state.getState()한 것과 같은 값이 들어있다.
    number: state.counter.number, // number에는 state -> counter 안에 있는 number를 넣어주겠다.
    diff: state.counter.diff,
  }));
  // useSelector의 결과물이 두 가지를 선택한 객체가 된다.
  // 그래서 number, diff만 남는 객체가 되고 비구조화할당으로 두 가지를 꺼내온 것이다.
  // 👏 즉, 꺼내올 값들을 useSelector를 이용해서 객체의 키로 지정해주고 비구조화할당으로 꺼낸 것.

  //--------
  const dispatch = useDispatch();
  const onIncrease = () => {
    dispatch(increase());
  };
  const onDecrease = () => {
    dispatch(decrease());
  };
  const onSetDiff = diff => {
    dispatch(setDiff(diff));
  };

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

App.js

지난번 포스팅에서 index.js에 Provider로 감싸주었고 store를 전달해줬었다.

import CounterContainer from './containers/CounterContainer';

function App() {
  return (
    <div className='App'>
      <CounterContainer />
    </div>
  );
}

export default App;

정리


컨테이너와 프레젠테이셔널 컴포넌트를 분리해서 작성하면 프레젠테이셔널 컴포넌트를 재활용해줄 수 있다. 관심사를 분리할수도 있어서 굉장히 유용한 패턴이다. 리덕스의 창시자가 이런 패턴을 공개했다.(2019년에 나눠서 해도 되는데 꼭 그렇게 할 필요는 없다고 코멘트를 남겼다. 하지만 대다수 프로젝트가 이렇게 진행된다.) 벨로퍼트님은 폴더를 따로 나누지는 않지만 컨테이너와 프로젠테이셔널을 구분해서 작업하신다고 한다.


아래는 이전 포스팅 참고 코드다. 위 코드를 분석할 때 바로 참고할 수 있도록 넣어둔다.

modules/counter.js

// 액션 타입 선언
const SET_DIFF = 'counter/SET_DIFF';
const INCREASE = 'counter/INCREASE';
const DECREASE = 'counter/DECREASE';

const initialState = {
  number: 0,
  diff: 1,
};

// 액션 생성 함수
export const setDiff = diff => ({ type: SET_DIFF, diff });
export const increase = () => ({ type: INCREASE });
export const decrease = () => ({ type: DECREASE });

// reducer
export default function counter(state = initialState, action) {
  switch (action.type) {
    case INCREASE:
      return {
        ...state,
        number: state.number + state.diff,
      };
    case DECREASE:
      return {
        ...state,
        number: state.number - state.diff,
      };
    case SET_DIFF:
      return {
        ...state,
        diff: action.diff,
      };
    default:
      return state;
  }
}

modules/index.js

  import { combineReducers } from 'redux';
import counter from './counter';
import todos from './todos';

const reducer = combineReducers({ counter, todos });
export default reducer;

좋은 웹페이지 즐겨찾기