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 패턴의 결과는 동일하다.