icon

메티의 블로그

리액트 디자인패턴: HOC, Render Props, Hooks
리액트 디자인패턴: HOC, Render Props, Hooks

리액트 디자인패턴: HOC, Render Props, Hooks

summary
이 글은 리액트의 디자인 패턴인 HOC(고차 컴포넌트), Render Props, 그리고 Hooks에 대해 설명하고, 각 패턴의 특징과 장단점을 비교합니다. 또한, 이들 패턴이 어떻게 컴포넌트 간의 로직 재사용을 가능하게 하는지에 대해 논의합니다.
Tags
Design Pattern
view_count
날짜
Aug 20, 2025
상태
공개

HOC 패턴이란?

Higher Order Component (고차 컴포넌트) 패턴은 컴포넌트를 인자로 받아 새로운 컴포넌트를 반환하는 방식의 리액트 디자인 패턴입니다. 여기서 컴포넌트는 함수형 뿐만 아니라 클래스형 컴포넌트도 포함입니다. HOC 의 특징은 Wrapper 함수가 JSX 를 리턴하는 것이 아니라 JSX 를 리턴하는 함수를 리턴하는 것 입니다.

왜 사용하나요?

HOC 는 같은 로직을 여러 컴포넌트에서 재사용 하는 방법 중 하나입니다. HOC 를 통해 공통된 스타일을 한번에 적용시키거나, 백엔드 fetch 후 데이터 적용, 로깅 등 공통 로직을 인자로 들어온 컴포넌트에 적용할 수 있습니다. 또한, Wrapper 함수가 인자로 들어온 컴포넌트의 props 를 가로채 변형 할 수 있습니다.

활용

export function withAuth<P extends object>(Wrapped: React.ComponentType<P>) { const withAuthHOC: React.FC<P> = (props) => { console.log("authenticated!"); console.log(props); const anotherProp = true; return <Wrapped {...props, anotherProp} />; }; return withAuthHOC; }
HOC 는 예시와 같이 인수로 받은 컴포넌트를 리턴하는 함수를 리턴하며, 그 리턴 함수의 첫번째 인자를 통해 인자로 들어온 컴포넌트의 props 에 접근 할 수 있습니다.
import { withAuth } from "../../shared"; type ProfileProps = { user: { id: string; name: string; email: string; }; }; function Profile({ user }: ProfileProps) { return ( <div> <h2>Profile</h2> <p>This is the profile component.</p> <p>User ID: {user.id}</p> <p>Name: {user.name}</p> <p>Email: {user.email}</p> </div> ); } export const AuthenticatedProfile = withAuth(Profile);
위의 가상의 auth 관련 로직을 가진 HOC 를 사용하면, Profile 의 props 에 접근하여 수정 할 수 있습니다.
export function withLoader<P extends object>(Wrapped: React.ComponentType<P>) { const withLoaderHOC: React.FC<P> = (props) => { console.log("Loading..."); console.log(props); return <Wrapped {...props} loaded={true} />; }; return withLoaderHOC; } ///////////// import { withAuth, withLoader } from "../../shared"; type ProfileProps = { user: { id: string; name: string; email: string; }; }; function Profile({ user }: ProfileProps) { return ( <div> <h2>Profile</h2> <p>This is the profile component.</p> <p>User ID: {user.id}</p> <p>Name: {user.name}</p> <p>Email: {user.email}</p> </div> ); } export const AuthenticatedProfile = withLoader(withAuth(Profile));
또한, 다음과 같이 HOC 를 중첩해 사용하는 HOC 합성 패턴도 존재합니다.

Hooks 는 대체 할 수 없는 부분

HOC 는 공통된 로직 재사용이라는 점에서 커스텀 훅을 만들어 사용하는 것과 목적에 유사한 점이 많고, 실제로 많은 부분에서 커스텀 훅으로 대체 되었습니다. 하지만, HOC 가 아직도 남아있는 것은 커스텀 훅이 HOC를 완전히 대체 할 수는 없기 때문입니다. 디자인 패턴 적으로도 차이가 있지만, 기능적으로 차이가 많이 납니다. 구현상 완전 반대 상황이기 때문이죠.
HOC 는 공통 로직을 사용 컴포넌트의 외부에서 제공하기 때문에 사용 컴포넌트는 공통 로직을 접근, 사용 할 수 없습니다. 하지만 커스텀 훅 같은 경우에는 사용 컴포넌트의 내부 상태나 props 를 커스텀 훅에 제공해 그 로직에 따른 결과값을 리턴 받아 직접 사용합니다.
HOC 는 공통 로직을 사용 컴포넌트의 외부에서 제공하기 때문에 사용 컴포넌트는 공통 로직을 접근, 사용 할 수 없습니다. 하지만 커스텀 훅 같은 경우에는 사용 컴포넌트의 내부 상태나 props 를 커스텀 훅에 제공해 그 로직에 따른 결과값을 리턴 받아 직접 사용합니다.
  • 인자로 들어오는 컴포넌트의 props 에 접근해 수정 할 수 있음
  • 공통 로직이 어떤 컴포넌트의 상태나 props 에도 변하지 않는다면, 이를 안전하게 격리할 수 있음

단점

Wrapper Hell

너무 많은 공통 로직이 존재한다면, 콜백 지옥 처럼 너무 많은 HOC 들이 존재할 수 있습니다. 이는, 라이브러리를 통해 좀 더 가독성이 좋게 하거나 할 수 있다고 합니다.

타입스크립트에서 불편

HOC 는 어떤 인자가 컴포넌트로 들어오던 props 에 대해 대응해야 하기 때문에, 타입 선언이 상당히 어렵습니다. (예시 코드도 사실 거의 any 나 다름 없게 작성이 되어있는 셈이죠.)

정적 프로퍼티로 추가된 props 는 누락됨

HOC 에서 props 를 사용할 때, 간혹 정적 프로퍼티 형태로 props 가 추가된 컴포넌트가 있을 수 있습니다. 이때, 정적 prop 은 런타임에서 추가되는 것이기 때문에 누락 될 수 있습니다.
type DialogProps = { children: React.ReactNode }; function Dialog({ children }: DialogProps) { return <div className="dialog">{children}</div>; } function Title({ children }: { children: React.ReactNode }) { return <h1>{children}</h1>; } function Content({ children }: { children: React.ReactNode }) { return <div>{children}</div>; } // 정적 프로퍼티 추가 Dialog.Title = Title; Dialog.Content = Content; // 사용예시 <Dialog> <Dialog.Title>알림</Dialog.Title> <Dialog.Content>내용입니다</Dialog.Content> </Dialog>
정적 프로퍼티가 추가된 Dialog 컴포넌트가 만약 HOC 로 감싸진다면, Title, Content 와 같은 프로퍼티는 HOC 내부에서 사용하지 못 합니다.

Render Props 패턴이란?

리액트 디자인 패턴 중 하나로, 렌더링 할 컴포넌트를 props 로 받아서 내부 로직을 통해 렌더링 하는 방식입니다.

왜 사용하나요?

UI 와 상태관리 로직을 분리하여, 상태관리 로직은 공유하면서, UI 표현 방식은 외부에서 주입 할 수 있습니다. 즉, UI 와 상태관리 로직을 분리하기 위해 사용합니다.

활용

render props 는 오래전 부터 존재하던 패턴이지만 현재도 꽤 사용됩니다. 실제로 리액트 기반 디자인시스템 라이브러리에서 자주 보이는 두가지 API 제공 방법 중 하나죠. (렌더링 하고싶은 컴포넌트를 규격에 맞게 만들어 render props 로 넘기는 방법, 내가 정의할 컴포넌트에 커스텀 훅을 직접 사용하는 방법)
<VirtualList items={rows} itemHeight={32} renderItem={(row, i) => <Row key={row.id} data={row} index={i} />} />
일반적인 사용 예시, VirtualList 는 renderItem 으로 들어온 컴포넌트를 내부에서 직접 실행해 렌더링 합니다.
render prop 이라고 해서 반드시 prop 이름이 render 일 필요는 없습니다. 위의 예시의 경우 VirtualList 컴포넌트는 내부적으로 props.renderItem() 와 같이 호출할 것 입니다.

단점

대부분의 경우 커스텀 훅으로 대체 될 수 있다.

단점이라고 하기는 모호하지만, 좀 더 내부 상태 로직에 수정을 가할 수 있는 커스텀 훅이 대부분을 대체할 수 있습니다.

추가적인 이야기

render props 패턴은 디자인 시스템 라이브러리 형태에서는 이러한 내부 수정을 하지 못하는 방법이 오히려 장점으로 작용할 수 있겠습니다. 굳이 내부 상태 로직을 변경할 필요가 없게 된다면, 사용하기 쉽고 라이브러리 내부 로직을 모르는 상태에서 사용할 수 있다는 점이 있겠습니다. (일반적으로, 남이 만든 커스텀 훅을 사용하는 것이 render prop 을 지원하는 컴포넌트를 사용하는 것 보다 어렵습니다.)

Hooks 패턴이란?

리액트 16.8 부터 제공된 함수형 컴포넌트의 상태 관리 API 로, 이 hooks 함수들을 활용해 커스텀 hooks 를 만들 수 있게 되었습니다. 이 hooks 들을 통해 커스텀 hooks 를 만들어 상태 관리 로직을 분리하는 것을 hooks 패턴, 커스텀 훅 이라고 합니다.

왜 사용하나요?

HOC 나 Render props 와 비슷하게, 로직을 여러 컴포넌트에서 재사용하기 위해 사용합니다. 하지만, HOC 와 Render Props 와 다르게, 함수형 컴포넌트가 의존성을 받아와 내부에서 사용합니다.

활용

import { useEffect, type RefObject } from "react"; export function useOutsideClickEffect( ref: RefObject<HTMLElement | null>, onOutsideClick: () => void, ) { useEffect(() => { const handleClick = (event: MouseEvent) => { if (ref.current && !ref.current.contains(event.target as Node)) { onOutsideClick(); } }; document.addEventListener("click", handleClick); return () => { document.removeEventListener("click", handleClick); }; }, [ref, onOutsideClick]); }
useEffect 를 사용해 만든 외부 요소 클릭 감지 side effect 커스텀 훅
function useKeyPress(targetKey) { const [keyPressed, setKeyPressed] = React.useState(false) function handleDown({ key }) { if (key === targetKey) { setKeyPressed(true) } } function handleUp({ key }) { if (key === targetKey) { setKeyPressed(false) } } React.useEffect(() => { window.addEventListener('keydown', handleDown) window.addEventListener('keyup', handleUp) return () => { window.removeEventListener('keydown', handleDown) window.removeEventListener('keyup', handleUp) } }, []) return keyPressed }
useState 를 사용한 상태 훅

유의할 점

HOC 는 공통 로직을 사용 컴포넌트의 외부에서 제공하기 때문에 사용 컴포넌트는 공통 로직을 접근, 사용 할 수 없습니다. 하지만 커스텀 훅 같은 경우에는 사용 컴포넌트의 내부 상태나 props 를 커스텀 훅에 제공해 그 로직에 따른 결과값을 리턴 받아 직접 사용합니다.
HOC 는 공통 로직을 사용 컴포넌트의 외부에서 제공하기 때문에 사용 컴포넌트는 공통 로직을 접근, 사용 할 수 없습니다. 하지만 커스텀 훅 같은 경우에는 사용 컴포넌트의 내부 상태나 props 를 커스텀 훅에 제공해 그 로직에 따른 결과값을 리턴 받아 직접 사용합니다.
커스텀 훅이 HOC 나 Render props 와 같은 디자인 패턴을 많이 대체한 것은 맞지만, 모든 것을 전부 대체한 은탄환이라고 생각하는 것은 좋지 않은 것 같습니다. 사용상의 장단점이 있다고 생각하고 필요한 상황에서 적절히 사용하는 것이 중요하다고 생각합니다.