React-redux HOC 패턴으로 props 전달 or Hooks로 사용하기 Passing props with React-redux HOC pattern or using them as Hooks

notion image

HOC 패턴

// Counter.tsx/*❤ : 카운터 프리젠테이셔널 컴포넌트 만들기: 컨테이너 컴포넌트와 구분하여 만든다.: 컴포넌트에서 필요한 값과 함수들을 모두 props 로 받아오도록 처리
  ❤ : 위 컴포넌트에서는 3개의 버튼을 보여주는데 3번째 버튼의 경우 클릭이 되면
  ❤ :  5를 onIncreaseBy 함수의 파라미터로 설정하여 호출. */import React from 'react';

type CounterProps = {
  count: number;
  onIncrease: () => void;
  onDecrease: () => void;
  onIncreaseBy: (diff: number) => void;
};

export default function Counter({
  count,
  onIncrease,
  onDecrease,
  onIncreaseBy,
}: CounterProps): JSX.Element {
  return (
    <div><h1>{count}</h1><button onClick={onIncrease}>+1</button><button onClick={onDecrease}>-1</button><button onClick={(): void => onIncreaseBy(5)}>+5</button></div>
  );
}
// CounterContainer.tsximport React from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { RootState } from '../modules/index';
import { increase, decrease, increaseBy } from '../modules/counter';
import Counter from '../components/Counter';

export default function CounterContainer(): JSX.Element {
//NOTE: ts에서 특별한 점은 useSelector 부분에서 state의 타입을 RootState로 지정해서 사용한다는 것 외에는 없다.const count = useSelector((state: RootState) => state.counter.count);
  const dispatch = useDispatch();

  const onIncrease = (): void => {
    dispatch(increase());
  };

  const onDecrease = (): void => {
    dispatch(decrease());
  };

  const onIncreaseBy = (diff: number): void => {
    dispatch(increaseBy(diff));
  };

  return (
    <Countercount={count}onIncrease={onIncrease}onDecrease={onDecrease}onIncreaseBy={onIncreaseBy}
    />
  );
}
// App.tsximport React from 'react';
import CounterContainer from './containers/CounterContainer';
// import Counter2 from './components/Counter2';// ❤ : Hooks 이전에는 컨테이너 컴포넌트를 만들 때 connect() 함수를 통해// ❤ : HOC 패턴을 통해 컴포넌트와 리덕스를 연동하여주었기 때문에 props로// ❤ : 필요한 값들을 전달해주는 것이 필수였으나 Hooks를 통해 로직을// ❤ : 분리하는 것도 좋은 방법function App(): JSX.Element {
  return (
    <><CounterContainer /></>
  );
}

export default App;

Hooks가 생긴 이후로는 Hooks로 로직을 분리시키는 방식이 선호되고 있다.

Hooks 패턴

//useCounter.tsx// ❤ : 프리젠테이셔널 / 컨테이너 분리를 하지 않고 작성하는 방법?// ❤ : Hooks let me do the same thing without an arbitrary division".// ❤ : 컴포넌트를 사용 할 때 props 로 필요한 값을 받아와서 사용하게 하지 말고,// ❤ : useSelector와 useDispatch로 이루어진 커스텀 Hook을 만들어서 이를 사용// ❤ : 컨테이너랑 똑같이 생긴 걸 useCounter 훅으로 만듦import { useCallback } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { RootState } from '../modules/index';
import { increase, decrease, increaseBy } from '../modules/counter';

function useCounter() {
  const count = useSelector((state: RootState) => state.counter.count);
  const dispatch = useDispatch();

  const onIncrease = useCallback(() => dispatch(increase()), [dispatch]);
  const onDecrease = useCallback(() => dispatch(decrease()), [dispatch]);
  const onIncreaseBy = useCallback(
    (diff: number) => dispatch(increaseBy(diff)),
    [dispatch]
  );

  return { count, onIncrease, onDecrease, onIncreaseBy };
}

export default useCounter;
// Counter2.tsx/*: useCounter hook을 사용해서 Counter.tsx 사용
/*: 필요한 함수와 값을 props로 받아오는 게 아니라 useCounter Hook을 통해서 받아옴
/*: 이제 컨테이너 컴포넌트는 쓸모 없으므로 App 컴포넌트에 Counter2를 렌더링함 */import React from 'react';
import useCounter from '../hooks/useCounter';

export default function Counter2() {
  const { count, onIncrease, onDecrease, onIncreaseBy } = useCounter();

  return (
    <div><h1>{count}</h1><button onClick={onIncrease}>+1</button><button onClick={onDecrease}>-1</button><button onClick={() => onIncreaseBy(5)}>+5</button></div>
  );
}
// App.tsximport React from 'react';
// import CounterContainer from './containers/CounterContainer';import Counter2 from './components/Counter2';
// ❤ : Hooks 이전에는 컨테이너 컴포넌트를 만들 때 connect() 함수를 통해// ❤ : HOC 패턴을 통해 컴포넌트와 리덕스를 연동하여주었기 때문에 props로// ❤ : 필요한 값들을 전달해주는 것이 필수였으나 Hooks를 통해 로직을// ❤ : 분리하는 것도 좋은 방법function App(): JSX.Element {
  return (
    <><Counter2 /></>
  );
}

export default App;
두 패턴 모두 사용되어야 할 모듈은 다음과 같다.

Modules(리덕스 모듈과 RootReducers)

// modules/counter.ts// #: 리덕스 모듈 작성//NOTE: 액션 type 선언const INCREASE = 'counter/INCREASE' as const;
const DECREASE = 'counter/DECREASE' as const;
const INCREASE_BY = 'counter/INCREASE_BY' as const;

//NOTE: 액션 생성 함수 선언// return 생략할 수 있어서 화살표 함수 이용export const increase = () => ({ type: INCREASE });
export const decrease = () => ({ type: DECREASE });
export const increaseBy = (diff: number) => ({
  type: INCREASE_BY,
  payload: diff,
});

/*NOTE: 액션 객체들에 대한 type 준비하기
 * ReturnType은 함수에서 반환하는 타입을 가져올 수 있게 해주는 유틸 타입 */
type CounterAction =
  | ReturnType<typeof increase>
  | ReturnType<typeof decrease>
  | ReturnType<typeof increaseBy>;

//NOTE: 상태의 타입과 상태의 초깃값 선언하기// 리덕스의 상태의 타입을 선언할 때는 type or interface
type CounterState = {
  count: number;
};

const initialState: CounterState = {
  count: 0,
};

//NOTE: 리듀서 작성하기, useReducer와 비슷하다.// 함수의 반환 타입에 상태의 타입을 넣는 것을 잊지 마라function counter(state: CounterState = initialState, action: CounterAction) {
  switch (action.type) {
    case INCREASE:
      return { count: state.count + 1 };
    case DECREASE:
      return { count: state.count - 1 };
    case INCREASE_BY:
      return { count: state.count + action.payload };
    default:
      return state;
  }
}

export default counter;
// modules/index.tsimport { combineReducers } from 'redux';
import counter from './counter';

//NOTE: 리듀서가 하나 뿐이지만 추후 다른 리듀서를 더 만들 것이므로 루트 리듀서를 만듦const rootReducer = combineReducers({
  counter,
});

export default rootReducer;

/*: RootState 라는 타입을 만들어서 내보내주어야 한다.
/*: 이 타입은 추후 우리가 컨테이너 컴포넌트를 만들게 될 때
/*: 스토어에서 관리하고 있는 상태를 조회하기 위해서
/*: useSelector를 사용 할 때 필요로 한다. */export type RootState = ReturnType<typeof rootReducer>;
사용할 패턴의 파일을 먼저 생성한다음 modules라는 디렉토리를 만들어 위의 2개 파일(index.ts, counter.ts)을 생성해주면 카운터가 정상적으로 작동한다. Hooks와 HOC 패턴의 결과는 동일하다.