React, 고차 컴포넌트 (HOC) 사용하기

eunji hwang·2020년 7월 13일
3

REACT

목록 보기
7/16

참고 :
리액트 공식문서 고차 컴포넌트(higher-order-component)
책 : 실전 리액트 프로그래밍/이재승 저: 아래 작성된 예제 코드

고차 컴포넌트 (HOC)

  • 고차 컴포넌트(HOC, higher-order-componentm), 이하 HOC로 통일
  • HOC는 컴포넌트를 인자로 받아 새로운 컴포넌트를 반환하는 함수
  • 컴포넌트 로직을 재사용 하기 위한 방식
  • HOC의 이름은 with이름지정 규칙을 따름
  • 예) 리덕스 - connect() 함수 / 라우터 - withRouter()

HOC의 장점

  • 반복적인 코드 재사용 용이

HOC의 단점

  • 속성 값의 암묵적 전달. 예 : redux-connect() 사용 시 넘기지 않은 this.props.dispatch를 사용할 수 있는 점
  • 동명의 props를 생성/사용할 때 충돌발생
    • 서로 다른 HOC에서 동일 props 이름 사용.
    • InputCompo의 props와 HOC의 props에서 동일 이름 사용.
    • 예: redux-dispatch를 사용, withMyHOC-dispatch 사용으로 충돌 => 마지막 값으로 덮어쓰기
  • 의례적 절차 필요
    • 항상 함수로 감싸줘야 함
    • displayName 설정해야함 (recompose/getDisplayName 패키지로 InputCompo의 displayName 불러와야함)
    • 정적메서드 호출 설정해야함 (hoist-non-react-statics 패키지 사용해야 함)
    • typescript와 같은 정적타입언어를 사용할때 타입 정의가 까다롭다

사용 예제

아래의 HOC 사용 예제는 이재승 저, 실전 리액트 프로그래밍 책에 수록된 예제입니다. 자세한 설명과 함께 예제를 보기 원하시면 책을 보시길 추천!

  • 함수 with이름(InputCompo, 그밖에 인자들...)작성 : 인자는 컴포넌트와, 추가로 필요한 인자들
  • 반환 : OuputCompo를 반환하며, OuputCompo의 반환값에는 inputCompo를 지정

1) 마운트 시 이벤트 발동

// withMountEvent : 해당 HOC는 componentDidMount()될 때 이벤트가 발생!
function withMountEvent(InputCompo, compoName){ // 필요한 매개변수는 추가하기
  return (
    // 반환 컴포넌트 작성
    class OutputComp extends React.Component {
      // 마운트 라이프 사이클에서 이벤트 작성
      componentDidMount() {
        sendEvent(compoName);
      }
      render(){
        return (
          <InputCompo {...this.props} /> // InputCompo를 OuputCompo에서 반환
        )
      }
    }
  )
}

2) 마운트 여부를 알려주는 HOC

서버사이드렌더링(SSR)에서 화면 일부분이 클라이언트사이드렌더링(CSR)하기를 원할 경우 주로 사용하는 방식

// HOC 작성
function withHasMount(InputCompo){
  return (
    class OutputCompo extends React.Component {
      state = {
        hasMounted : false,
      }
      componentDidMount() {
          this.setState({ hasMounted: true })
      }
      render(){
        return (
          <InputCompo {...this.props} hasMounted={this.state.hasMounted} />
          // InputCompo에서 this.props.hasMounted 를 추가로 전달,
          // hasMounted가 true일 경우 CSR!
        )
      } 
    }
  )
}

3) 로그인 체크 HOC

// HOC 작성 : 함수형 작성
function withLogined(InputCompo){
  return (
    // props 분해
    function({isLogin, ...props}){
      return (
        if(isLogin) return <InputCompo {...props} />;
        if(!isLogin) return <p>권한이 없음니당~</p>;
      )
    }
  )
}
  • HOC 없이 구현하려면, 확인이 필요한 컴포넌트 마다 함수를 구현해야한다.
  • props.isLogin 프로퍼티는 HOC를 벋어나면 불필요 함으로 제외하고 props를 넘김

4) 클래스 상속을 이용한 HOC

function withClassExtends (InputCompo) {
  return (
    class OutputCompo extends InputCompo {
      // InputCompo의 인스턴스에 접근가능
    }
  )
}
  • InputCompo의 인스턴스에 접근가능 : life-cycle, state, props, etc(methods, variable)
  • 디버깅 할 때 사용

5) 디버깅 HOC

// 디버깅 HOC
function withDebug (InputCompo) {
  return (
    class OutputCompo extends InputCompo {
      render (){
        return (
         <>
          // InputCompo의 props, state에 접근
          <p>props: {JSON.stringify(this.props)}</p>
          <p>state: {JSON.stringify(this.state)}</p>
          {super.render()} // InputCompo의 render()호출
         </>
        )
      }
    }
  )
}
  • 상속자 InputCompo의 props, state를 상속받아 화면에 출력
  • super.render() 로 상속받은 render() 호출

6) div요소 감싸는 HOC

function withWrappedDiv (InputCompo) {
  return (
    class OutputCompo extends InputCompo {
      render (){
        const inputRender = super.render();
        return (
         <>
          {inputRender && inputRender.type === 'div'
           ? <InputCompo {...this.props} />
           : <div><InputCompo {...this.props} /></div>
          }
         </>
        )
      }
    }
  )
}

InputCompo를 상속받아 사용할 때 life-cycle 호출시 버그 발생 주의
약속된 위치에서 호출되는 life-cycle의 강제 호출은 버그 발생 야기한다.

  • 무조건 div 태그로 감싸는 HOC
  • super.render()를 통해 render의 요소가 div인지 확인
  • true : <Inputcompo /> 리턴
  • false div요소로 감싼 <div><Inputcompo/></div> or React.createElement('div', null, inputRender)

7) 여러 HOC 동시 사용하기

export default withOne(withTwo(withTree(InputCompo)))
  • 너무 많은 HOC 중첩 사용은 렌더링 성능을 악영향
  • 디버깅할 때 불편

디버깅 할때 편하게 하는 팁!

  • displayName 값을 수정

8) HOC의 displayName 설정

// recompose/getDisplayName 패키지 사용
import getDisplayName from 'recompose/getDisplayName'

function withDisplayNameChange(InputCompo) {
  class OutputCompo extends Component {
    // ...
  }
  // getDisplayName을 통해 InputCompo의 displayName 값을 얻는다.
  OutputCompo.displayName = `withDisplayNameChange(${getDisplayName(InputCompo)})`
  
  return OutpoCompo
}

9) HOC에 정적 메서드 전달

컴포넌트가 정적 메서드를 갖고 있을때, 정적메서드는 HOC에 전달되지 않음. hoist-non-react-statics 패키지를 통해 정적 메서드를 전달 받도록 한다.

import hoistNonReactStatic from 'hoist-non-react-statics'
function withStaticMethod (InputCompo){
  class OutpuCompo extens React.Component {
    // ...
  }
  //  hoistNonReactStatic(정적 메서드 받을 컴포, 정적 메서드 보낼 컴포) 
  hoistNonReactStatic(OutputCompo, InputCompo) 
  return OuputCompo;
}
  • hoistNonReactStatic(정적 메서드 받을 컴포, 정적 메서드 보낼 컴포)
  • react-router패키지의 withRouter에서 위 방식 사용

HOC방식 vs Render Props방식?

리액트 공식 문서 - Render Props 방식
랜더 속성 값(render-props) : 코드 재사용을 위한 함수타입의 속성값을 이용하는 패턴

  • CompoA는 자주 사용할 컴포넌트
  • CompoA에서 render 될 때 내부 내용을 변경하고싶은데, 복붙-수정의 하드코딩으로 CompoB를 만들어야해? N--Ooo!
  • CompoA에서 render-props방식을 사용해서 내부 내용을 전해줄 수 있음.
    • 조금 다르지만 props.children으로 Layout 잡는 것과 비슷.
  • render-props방식에는 HOC의 단점이 존재하지 않음.
  • typescript 사용 용이.
  • render-props방식을 사용할 경우 코드가 지저분 해짐.
profile
TIL 기록 블로그 :: 문제가 있는 글엔 댓글 부탁드려요!

1개의 댓글

comment-user-thumbnail
2021년 8월 31일

좋은글 감사합니다 ㅎㅎ

답글 달기