본문 바로가기

React

HOC & Hook & Typscript

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