logoStephen's 기술블로그

포스트 검색

제목, 태그로 포스트를 검색해보세요

Higher Order Component 이해하기

Higher Order Component 이해하기
React
성훈 김
2025년 7월 28일
목차

HOC, 고차 컴포넌트란?

고차 컴포넌트 ( HOC, Higher Order Component )는 컴포넌트 로직을 재사용하기 위한 React의 패턴이다. React API가 아닌, 리액트의 디자인 패턴이라고 할 수 있다. 구체적으로 설명하자면, 기본 기능을 가진 컴포넌트를 받아서 새 기능을 추가하여 확장된 컴포넌트를 반환하는 패턴이다.
 
일반 컴포넌트는 props를 받아서 UI를 반환하지만, 이 HOC패턴의 특징은 컴포넌트를 받아서 새로운 컴포넌트를 반환하는 것이 특징이다.
 

왜 이 패턴이 필요할까?

일단 어떤 문제점이 있길래 이러한 패턴을 사용하는 것일까? 그래서 문제가 되는 상황을 살펴보고, 이 글을 읽으시는 분들이 직접 판단해서 사용해보는 것이 좋을 것이라 생각한다.
 
일단 일반적인 Profile 컴포넌트와 Dashboard 컴포넌트를 보여주는 페이지가 있다고 가정해 보자.
JSON
import { useState, useEffect } from "react";

function Profile() {
  const [isLoading, setIsLoading] = useState(true);

  useEffect(() => {
    const timer = setTimeout(() => {
      setIsLoading(false);
    }, 2000);

    return () => clearTimeout(timer);
  }, []);

  if (isLoading) {
    return (
      <div>
        <h2>Profile</h2>
        <div className="spinner" />
      </div>
    );
  }

  const user = { name: "John Doe", email: "john@example.com" };

  return (
    <div>
      <h2>Profile</h2>
      <pre>{JSON.stringify(user, null, 2)}</pre>
    </div>
  );
}

function Dashboard() {
  const [isLoading, setIsLoading] = useState(true);

  useEffect(() => {
    const timer = setTimeout(() => {
      setIsLoading(false);
    }, 3000);

    return () => clearTimeout(timer);
  }, []);

  if (isLoading) {
    return (
      <div>
        <h2>Dashboard</h2>
        <div className="spinner" />
      </div>
    );
  }

  const data = { key: "value" };

  return (
    <div>
      <h2>Dashboard</h2>
      <pre>{JSON.stringify(data, null, 2)}</pre>
    </div>
  );
}

function WithoutHoc() {
  return (
    <main className="container">
      <Profile />
      <Dashboard />
    </main>
  );
}

export default WithoutHoc;
현재 코드의 구조를 간단하게 설명을 하자면,
  • Profile에서 2초간의 로딩을 useEffect로 관리하며 isLoading에 대한 로딩UI를 처리하고 있다.
  • Dashboard는 3초간의 로딩을 같은 방식으로 관리하며 isLoading 상태에 대한 로딩UI를 처리하고 있다.
  • WithoutHoc함수에서는 두 가지 컴포넌트를 묶어서 반환하고 있다.
 
이 구조에서는 조금 붚편한 사항이 있다는 걸 느낄 수 가 있는데, 비슷한 loading상태관리에 대한 로직이 중복된다는 점이다. 이러한 구조를 사용한다면 대규모 프로젝트에서는 어마어마한 코드중복이 될 가능성이 높다. 어차피 로딩 상태를 관리하는 로직이 어떤 컴포넌트든 비슷하다면, 이러한 상황에서 HOC패턴을 적용을 고려해 볼 수 있다.
 

HOC 패턴 적용하기

그럼 로딩상태를 관리하는 함수를 만들어보자. 핵심 포인트는 원본 컴포넌트를 받아서, 로딩 로직을 추가한 후, 새로운 컴포넌트를 반환하면 된다.
JavaScript
function withLoading(Component, delay = 2000) {
  return function WithLoading(props) {
    const [isLoading, setIsLoading] = useState(true);

    useEffect(() => {
      const timer = setTimeout(() => {
        setIsLoading(false);
      }, delay);

      return () => clearTimeout(timer);
    }, []);

    if (isLoading) {
      return (
        <div>
          <h2>{props.title}</h2>
          <div className="spinner" />
        </div>
      );
    }

    return <Component {...props} />;
  };
}
  1. 파라메터로 Component와 각기 다른 딜레이 값을 받을 수 있도록 2개의 파라메터를 받는다.
    1. JSON
      function withLoading(Component, delay = 2000) {
      • 이 부분이 HOC를 생성하는 팩토리 함수라고 할 수 있겠다.
      • 여기서 함수명이 카멜케이스임을 확인할 수 있는데, 함수임을 명확하게 하기 위해서이다
       
  1. return은 실제로 렌더링이 되는 리액트 컴포넌트를 리턴을 한다.
    1. JSON
      return function WithLoading(props) {
      • 여기서 사용되는 함수명은 파스칼케이스를 사용한다. 왜냐하면 여기서 반환되는 값은 리액트 컴포넌트임을 명확하게 하기 위해서이다.
 
  1. 그리고 실제 loading로직을 작성한다.
  1. WithLoading컴포넌트의 return은 전달받은 리액트 컴포넌트를 그대로 반환 해준다.
 
이렇게 로딩로직을 HOC패턴으로 구현한다면, 실제 렌더링되는 Profile과 Dashboard 컴포넌트는 온전하게 관심사가 분리되어서 핵심 UI로직만 남길 수 있게 된다.
JavaScript
function Profile() {
  const user = { name: "John Doe", email: "john@example.com" };

  return (
    <div>
      <h2>Profile</h2>
      <pre>{JSON.stringify(user, null, 2)}</pre>
    </div>
  );
}

function Dashboard() {
  const data = { key: "value" };

  return (
    <div>
      <h2>Dashboard</h2>
      <pre>{JSON.stringify(data, null, 2)}</pre>
    </div>
  );
}
 
이렇게 분리된 관심사를 횡단 관심사 분리 (cross-cutting-concerns)라고 하는데, 어느 한곳에 종속된 관심사가 아닌 여러 함수들이 같이 고민하는 관심사이기 때문에 종속 관심사가 아니라 횡단 관심사가 되기 떄문이다.
 
이제 이렇게 나누어진 관심사를 묶어서 선언해주면 최종 HOC패턴이 적용된 컴포넌트가 된다.
JavaScript
const ProfileWithLoading = withLoading(Profile, 2000);
const DashboardWithLoading = withLoading(Dashboard, 3000);
  • 여기서 의미를 명확하게 하기위해서 ProfileLoadingWrapper 로 이름을 정할 수도 있을 것이다. 이건 본인의 네이밍 컨벤션대로 적용해보면 좋을 것 같다.
 
마지막으로 사용하는 페이지에서 선언적으로 사용하면 된다.
JavaScript
function WithHoc() {
  return (
    <main className="container">
      <ProfileWithLoading title="Profile" />
      <DashboardWithLoading title="Dashboard" />
    </main>
  );
}

HOC 패턴 전체 코드 펼쳐보기

JavaScript
import { useState, useEffect } from "react";

function withLoading(Component, delay = 2000) {
  return function WithLoading(props) {
    const [isLoading, setIsLoading] = useState(true);

    useEffect(() => {
      const timer = setTimeout(() => {
        setIsLoading(false);
      }, delay);

      return () => clearTimeout(timer);
    }, []);

    if (isLoading) {
      return (
        <div>
          <h2>{props.title}</h2>
          <div className="spinner" />
        </div>
      );
    }

    return <Component {...props} />;
  };
}

function Profile() {
  const user = { name: "John Doe", email: "john@example.com" };

  return (
    <div>
      <h2>Profile</h2>
      <pre>{JSON.stringify(user, null, 2)}</pre>
    </div>
  );
}

function Dashboard() {
  const data = { key: "value" };

  return (
    <div>
      <h2>Dashboard</h2>
      <pre>{JSON.stringify(data, null, 2)}</pre>
    </div>
  );
}

const ProfileWithLoading = withLoading(Profile, 2000);
const DashboardWithLoading = withLoading(Dashboard, 3000);

function WithHoc() {
  return (
    <main className="container">
      <ProfileWithLoading title="Profile" />
      <DashboardWithLoading title="Dashboard" />
    </main>
  );
}

export default WithHoc;
 
 
 
 
이렇게 적용함으로서 얻게된 장점을 다시 상기시켜 보자면,
  • 로딩 로직을 여러 컴포넌트에 쉽게 적용할 수 있게 되어 재사용성이 높아졌다.
  • 원본 컴포넌트는 자신의 관심사와 로딩 로직의 관심사가 분리되어 Single Responsilbility Principle을 적용할 수 있게 되었다.
  • 그리고 원본코드를 수정하지 않고 추가로직을 구현함으로서 Open-Closed Principle이 적용 되었다.
 

결론

어플리케이션은 돌아가게 만드는 것은 누구나 할 수있지만, 시간이 갈수록 유지비용이 줄어들고 변경이 쉬워지는 아키텍처를 지키며 코드를 작성하는 것이 아마추어와 프로를 나누는 핵심이라고 생각이 든다. 마지막으로 클린 아키텍처의 저자인 로버트 C. 마틴이 남긴 말을 한 번 곱씹어 보면서 마무리 해보려고 한다.
 
“소프트웨어 아키텍처의 목표는 필요한 시스템을 만들고 유지보수하는 데 투입되는 인력을 최소화 하는데 있다.” - Robert C. Martin
 
 

참고