typescript 를 이용해 hook 으로 hoc 를 만들면서 알게된 내용들을 정리하고자 한다.
수정사항이나 피드백 환영합니당
HOC (Higer Order Component)
react 문서에 따르면
고차 컴포넌트는 컴포넌트를 가져와 새 컴포넌트를 반환하는 함수입니다.
라고 한다.
어떤 component 에 기능만 추가해서 새로운 compoennt 를 반환할 때 사용할 수 있다.
자 이제 typescript 와 hook 을 이용해 hoc 함수를 만들어보자.
만들려는 것
props 로 숫자를 받아서 보여주는 다음과 같은 component 가 있다고 해보자.
interface ShowNumberProps {
value : number
}
const ShowNumber = ({value} : ShowNumberProps) => {
return (<div>{value}</div>);
}
각각의 페이지마다 숫자를 출력하는 형태가 모두 달라서, 페이지에 맞게 컴포넌트를 따로 만든 상태이다.
그런데 저런 형태의 모든 컴포넌트에 다음과 같은 기능을 추가해야 하는 상황이 왔다.
- 숫자를 증가 / 감소 시키는 Counter 기능
- 숫자를 누를 때마다 숫자의 색깔이 검은색/초록색으로 바뀌어야 함.
일일이 모든 Component 에 Counter 기능을 추가하는 것은 귀찮은 일이다... 그렇다면!
숫자를 보여주는 Component 를 받아서, Counter 기능과 색깔이 바뀌는 기능을 추가해주는 HOC 를 만들어 주면 된다.
typescript , hook 을 이용한 HOC 구현
만들려는 HOC 의 input/ouput 은 다음과 같다.
- input : 숫자를 보여주는 Component
- ouput : 해당 숫자를 increase/decrease + 숫자 누를때마다 색깔 바뀌는 기능이 추가 된 Counter Component.
대충 다음과 같은 형태일 것이다.
const makeCounter = (
WrappedComponent: React.ComponentType // functional or class component 모두 될 수 있다.
) => {
const CounterComponent = () => {
}
return CounterComponent;
}
hook 의 useState, useEffect 같은 api 는 callback 함수에서 사용할 수 없기 때문에, 저렇게 Component 를 정의하고 return 하는 형식으로 해야 한다.
WrappedComponent
는 Props 로 보여줄 숫자, 지정할 색깔, 이벤트 핸들러를, CounterComponent
는 초기 숫자 값을 받아야 한다.
각 Component 의 Props interface 를 정의하면 다음과 같다.
export interface WrappedProps {
value: number; // 보여줄 숫자.
isBlack: boolean; // 지정할 색깔
onClick: () => void; // 숫자를 click 시 실행되는 이벤트 핸들러
}
interface CounterProps {
defaultValue: number; // 숫자의 초기 값
}
위의 props 들을 적용시킨 HOC 의 최종 모습은 아래와 같다.
const makeCounter = <P extends WrappedProps>(
WrappedComponent: React.ComponentType<P>
): React.FC<CounterProps & Omit<P, keyof WrappedProps>> => {
const CounterComponent = ({ defaultValue, ...props }: CounterProps) => {
const [value, setValue] = useState(defaultValue);
const [colorOption, setColorOption] = useState(true); // true : black, false: green
const onClick = () => {
setColorOption(!colorOption);
};
const increment = () => {
setValue(value+1);
}
const decrement = () => {
setValue(value-1);
}
return (
<div>
<WrappedComponent
{...(props as P)}
value={value}
isBlack={colorOption}
onClick={onClick}
/>
<button onClick={increment}>+</button>
<button onClick={decrement}>-</button>
</div>
);
};
return CounterComponent;
};
하나씩 뜯어보자.
const makeCounter = <P extends WrappedProps>(
WrappedComponent: React.ComponentType<P>
)
HOC 가 인자로 받을 WrappedComponent 와 관련 타입을 지정해주는 부분이다.P
는 WrappedComponent 가 받을 Props 의 타입이다. WrappedProps
를 extends 해주기 때문에, WrappedComponent 는 WrappedProps
가 포함된 Props 들을 받아야 한다. (뒤쪽에 사용 예시를 보면 좀 더 이해가 갈 것이다.)
React.ComponentType<P>
이 부분은 WrappedComponent 가 Functional or Class 타입의 Component 이며, Props 로 P 타입을 받는다는 뜻이다.
: React.FC<CounterProps & Omit<P, keyof WrappedProps>> => {
HOC 가 반환하는 Component 의 타입을 지정해주고 있다. React.FC
로 Functinal Component 라는 것을 명시하고 있다.
그 뒤에 <.. > 에 있는 부분은 해당 Component 의 Props 타입을 지정해 주는 부분이라고 생각하면 될 것 같다.
즉. CounterComponent 의 Props 타입이다.
React.FC<CounterProps>
로 써도 될 것 같지만, 뒤에 이상한 저 부분들을 추가해준 이유는 WrappedComponent 가 추가적인 Props 를 받게 해주기 위함이다.
Omit<P, keyof WrappedProps>
은 P 타입의 Props 에서 value, isBlack, onClick
Props 를 제외하겠다는 뜻이다.
즉, WrappedProps 에는 없는 추가적으로 넣어준 props 만 있게 되는 것이다.
저 부분이 없으면 WrappedComponent 는 WrappedProps 에 없는 props 타입은 받을 수 없게 된다.
const CounterComponent = ({ defaultValue, ...props }: CounterProps) => {
...
<WrappedComponent
{...(props as P)}
위에서 볼 수 있듯이, defaultValue를 제외한 WrappedComponent 와 관련된 props 들은 ...props 로 받아서 그대로 WrappedComponent 에 넘겨주고 있다.
HOC Component 의 사용
위에서 만든 HOC 를 이용하는 부분이다.
import React from 'react';
import makeCounter , {WrappedProps} from './Hoc';
const ShowNumber = ({value, isBlack, onClick}: WrappedProps) => {
const color = isBlack ? 'black' : 'green';
return (
<div style={{color:color}} onClick={onClick}>{value}</div>
)
}
export default makeCounter(ShowNumber);
WrappedProps
를 받는 ShowNumber Component 를 만들어서 makeCounter
의 인자로 넣어주고 있다.
저렇게 만든 Hoc component 는 아래와 같은 형식으로 사용할 수 있다.
import Counter from './Counter';
export default function App() {
return (
<div className="App">
<Counter defaultValue={0}/>
</div>
);
}
WrappedComponent 에서 만약에 추가적인 props 를 받고 싶다면 아래와 같이 하면 된다.
interface ShowNumberProps extends WrappedProps {
label: string;
}
const ShowNumber = ({ label, value, isBlack, onClick }: ShowNumberProps) => {
const color = isBlack ? "black" : "green";
return (
<div style={{ color: color }} onClick={onClick}>
<span>{label}</span>
{value}
</div>
);
};
export default makeCounter(ShowNumber);
그리고 사용할 때 추가한 props 값을 넣어주면 된다.
<ShowNumber label={'Hello Number: '} defaultValue={0}/>
참고
Hoc 구현에는 아래 글을 많이 참고했다.
https://medium.com/@jrwebdev/react-higher-order-component-patterns-in-typescript-42278f7590fb
수정사항이나 피드백 있다면 정말정말 환영합니다...!!
'React' 카테고리의 다른 글
useEffect (0) | 2020.10.17 |
---|---|
React 공홈 문서 정리(1) (0) | 2020.08.31 |
LifeCycle api (0) | 2020.08.23 |