React

고차 컴포넌트(HOC)와 Hooks

오스타 2022. 5. 29. 01:22

고차 컴포넌트(HOC)와 Hooks

 

 

React를 비교적 최근에 사용하기 시작한 개발자 분들이라면, 클래스형 컴포넌트보다 함수형 컴포넌트와 Hook에 익숙할 것입니다.

React는 초기에 클래스 컴포넌트만을 지원하였지만 16.8버전에서 Hooks를 도입하며 본격적으로 함수형 컴포넌트를 지원하게 되었습니다. 현재에는 공식 문서에서도 함수형 컴포넌트 + Hooks 조합을 권장하고 있는 상황이지만 여전히 클래스 컴포넌트 또한 지원하고 있기에 우리는 과거 코드를 열다 보면 클래스 컴포넌트를 종종 발견하곤 할 것입니다.

이번 포스트는 클래스 컴포넌트와 함수형 컴포넌트를 비교하는 내용은 아닙니다. 단지 컴포넌트 간에 재사용될만한 로직들을 간편하게 정리하는 방법에 대해 공부하다 자연스레 커스텀 훅을 접하게 되었고 클래스 컴포넌트에서는 HOC 패턴이 사용되는 것을 알게 되었습니다. (함수형 컴포넌트 또한 HOC 패턴을 사용할 수 있습니다.) 사실 커스텀 훅과 HOC 패턴 모두 공식 문서에서도 소개할만큼 어느정도 정형화된 코드이긴 하지만 함수형 컴포넌트를 주로 써오던 저는 커스텀 훅을 사용했지, HOC 코드는 잘 사용하지 않았던 것 같습니다.

그래서 이번 기회에 HOC 패턴에 대해 공부하고 커스텀 훅과 함께 비교해보며 어떤 장단점을 가지는지 정리해보고자 합니다.

 

 

HOC (Higher Order Component)

고차 컴포넌트는 컴포넌트를 매개 변수로 받아 새로운 컴포넌트를 반환하는 함수를 의미합니다.

고차 함수를 떠올리면 고차 컴포넌트 또한 이해하기가 편해집니다. 고차 함수는 함수를 인자로 받거나 결과로 반환하는 함수를 의미하죠. 고차 컴포넌트는 함수 대신 컴포넌트를 인자로 받아 특정 로직을 포함하는 새로운 컴포넌트를 반환하게 됩니다.

사실 정의만 봐서는 이 패턴이 어떻게 특정 로직을 재사용성 높게 구성해주는지 감이 오지 않습니다. 그래서 구체적인 예제를 살펴보고자 합니다.

 

withLogined

import { useContext } from "react";
import { LoginContext } from "../context/LoginContext";
​
export function withLogined(InputComponent) {
  return () => {
    const { isLogined } = useContext(LoginContext);
​
    return isLogined ? <InputComponent /> : <h3>로그인 부탁 드림!</h3>;
  };
}

(시작하기 앞서 커스텀 훅에는 use 접두어를 붙이듯, HOC에는 with 접두어를 붙이는 컨벤션이 존재합니다.)

간단한 로그인 상태 체크 HOC 코드입니다. 우리는 때때로 로그인 상태에 따라 노출 유무를 결정하는 컴포넌트를 만들곤 합니다. 이 경우 로그인 체크 코드를 각 컴포넌트마다 작성할 필요없이 위와 같이 HOC를 활용할 수 있습니다.

export default withLogined(UserInfo);

이렇게 작성된 HOC를 로그인 체크 로직이 필요한 컴포넌트 래퍼로 간단하게 사용이 가능합니다.

 

withHover

HOC를 사용하는 하나의 예제를 더 살펴보고자 합니다. 우리는 이미지를 화면에 여러개 띄우고 각 이미지의 hover 이벤트에 어떠한 로직을 추가하고자 합니다.

import { useState } from "react";
​
export default function withHover(InnerComponent) {
  return (props) => {
    const [isHovered, setIsHovered] = useState(false);
​
    function handleMouseEnter() {
      setIsHovered(true);
    }
​
    function handleMouseLeave() {
      setIsHovered(false);
    }
​
    return (
      <InnerComponent
        {...{
          ...props,
          handleMouseEnter,
          handleMouseLeave,
          isHovered
        }}
      />
    );
  };
}

해당 withHover 코드는 useState를 활용하여 컴포넌트에 마우스를 올릴 경우 특정 로직을 실행시킬 수 있도록 합니다.

import withHover from "../hoc/withHover";
import "../styles/ImageBox.css";
​
function ImageBox({
  imageUrl,
  imageTitle,
  isHovered,
  handleMouseEnter,
  handleMouseLeave
}) {
  return (
    <div>
      {isHovered && <div id="hover">{imageTitle}</div>}
      <img
        src={imageUrl}
        alt={imageTitle}
        width="400px"
        onMouseEnter={handleMouseEnter}
        onMouseLeave={handleMouseLeave}
      />
      <h5>이미지에 마우스를 올리면 이미지 제목이 표시됩니다.</h5>
    </div>
  );
}
​
export default withHover(ImageBox);

그리고 위 코드는 withHover를 사용하는 ImageBox라는 컴포넌트인데요. 위 코드를 실행해보면 이미지에 마우스를 올릴 경우 이미지 타이틀이 페이지 상단에 표시되는 것을 확인할 수 있습니다.

 

 

HOC 단점?

암묵적인 props 전달

HOC 패턴을 활용할 경우 개발자 입장에서는 props로 전달되는 속성들이 다소 명확하지 않기 때문에 사용하려는 HOC를 정확하게 파악하고 어떤 props들이 암묵적으로 전달되는지 숙지하고 있어야 하는 점이 있습니다. 뒤이어 소개할 custom hook의 경우 개발자가 사용할 hook만 정확하게 기입하면 되기 때문에 상대적으로 번거로움을 유발하는 요소가 적지 않나 생각합니다.

 

컴포넌트 구조의 복잡성 유발

export default withLogined(withHover(withStyles(ImageBox)));

위에서 작성한 ImageBox 컴포넌트에 다음과 같이 복수개의 HOC를 사용해야 하는 경우가 존재한다고 가정해 봅시다.

개발자 도구를 통해 React 컴포넌트를 디버깅해보면 실제로 고차 컴포넌트들이 표시되면서 불필요하게 depth가 커진 모습을 확인할 수 있습니다. HOC 패턴을 일부분만 적용한다면 위와 같은 문제는 없겠지만 본격적으로 패턴을 도입하기 시작하면 디버깅을 하는 개발자에게 조금은 불편함으로 작용하지 않을까 싶습니다.

const withImageBox = _.flowRight(withLogined, withHover, withStyles) // lodash method
const withImageBox = compose(withLogined, withHover, withStyles) // ramda

나열되는 HOC 형태를 보기 싫다면, 조합하여 하나의 HOC로 만들 수 있습니다. 위 코드와 같이 lodash, ramda와 같은 써드파티 라이브러리의 메소드를 활용한다면 간편하게 여러 HOC를 조합할 수 있는데요. 물론 조합의 경우도 디버깅의 depth는 똑같이 유지되며, 프로젝트의 불필요한 써드파티 라이브러리를 추가해야 한다는 점은 또 다른 고려사항이지 않나 생각이 듭니다.

 

props 네이밍 중복

HOC 패턴을 활용하게 되면 HOC에서 전달해주는 props와 inner 컴포넌트 props 사이에 네이밍이 겹칠 수 있습니다.

해당 문제점도 사실 암묵적인 props 전달 문제의 여파라고도 볼 수 있는데요. 이러한 문제점들은 개발자가 코드 작성에 있어 고려해야 할 사항들이 더 늘어날 것입니다.

 

 

Hooks

React는 함수형 컴포넌트를 등장시키면서 Hooks를 도입하였습니다. Hooks는 생명주기 메소드를 대체함과 동시에 재사용 로직을 위한 HOC 패턴을 대신해 더 나은 코드를 구성할 수 있게 하는데요.

import { useContext } from "react";
import { LoginContext } from "../context/LoginContext";
​
export default function useLogin() {
  const { isLogined, setIsLogined } = useContext(LoginContext);
​
  function loginAlert() {
    if (!isLogined) {
      alert("로그인 후 진행해주세요.");
    } else {
      alert("로그인이 완료된 상태에요^^");
    }
  }
​
  return { isLogined, setIsLogined, loginAlert };
}

위에서 작성해본 withLogineduseLogin 훅으로 구현해본 코드입니다.

import useLogin from "../hook/useLogin";
​
function UserInfo() {
  const { isLogined } = useLogin();
  return isLogined ? (
    <div>
      <h4>환영합니다! 유저분!</h4>
      <h5>해당 내용은 로그인한 상태에서만 보입니다!</h5>
    </div>
  ) : (
    <h3>로그인 부탁 드림!</h3>
  );
}
​
export default UserInfo;

훅으로 재사용 로직을 덜어내게 되면 HOC 패턴처럼 컴포넌트를 함수로 감쌀 필요가 없습니다. 이것은 컴포넌트의 depth를 깊게 하는 단점을 해소함과 동시에 암묵적인 props를 숙지하고 있을 필요도 없어집니다.

실제로 React 공식문서에서는 대부분의 경우 HOC 패턴을 Hooks로 대처할 수 있으며 컴포넌트 트리의 중첩을 줄이는데 도움이 된다고 명시되어 있습니다.

 

 

결론

모든 코드 작성에 있어 정답은 없는 것처럼, HOC 패턴이 Hooks보다 무작정 안 좋다는 의미는 아닙니다.

실제로 제가 참고한 포스트에서는 훅을 사용하면 컴포넌트 내부에 커스텀 동작을 추가하는 형태이기 때문에 HOC 패턴과 비교하여 버그를 도입할 위험이 잠재적으로 증가한다고 하는데요. (사실 개인적으로는 HOC 사용에서 암묵적 props로 발생할 수 있는 버그보단 Hooks의 버그 위험이 더 적다고 생각이 듭니다.)

또 기존 코드가 클래스형 컴포넌트로 작성되어 있을 경우 훅을 사용하기 위해 HOC 패턴으로 한 번 감싼 형태를 의도적으로 사용할 수 있다고도 생각합니다.

가장 좋은 것은 두 패턴 사용 모두에 익숙해지면서 현재 코드에 적절한 패턴이 어떤 것인지 판단할 수 있는 개발자가 되는 것이지 않을까 싶습니다 :)

 

 

Reference

LIST

'React' 카테고리의 다른 글

React Router v6 정리  (0) 2022.04.04
React Testing Library 튜토리얼  (0) 2022.03.28