React

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

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> ); }
TypeScript
복사
// 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} /> ); }
TypeScript
복사
// 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;
TypeScript
복사
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;
TypeScript
복사
// 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> ); }
TypeScript
복사
// 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;
TypeScript
복사
두 패턴 모두 사용되어야 할 모듈은 다음과 같다.

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;
TypeScript
복사
// 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>;
TypeScript
복사
사용할 패턴의 파일을 먼저 생성한다음 modules라는 디렉토리를 만들어 위의 2개 파일(index.ts, counter.ts)을 생성해주면 카운터가 정상적으로 작동한다. Hooks와 HOC 패턴의 결과는 동일하다.