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; }
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);
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));
Hooks 는 대체 할 수 없는 부분
HOC 는 공통된 로직 재사용이라는 점에서 커스텀 훅을 만들어 사용하는 것과 목적에 유사한 점이 많고, 실제로 많은 부분에서 커스텀 훅으로 대체 되었습니다. 하지만, HOC 가 아직도 남아있는 것은 커스텀 훅이 HOC를 완전히 대체 할 수는 없기 때문입니다. 디자인 패턴 적으로도 차이가 있지만, 기능적으로 차이가 많이 납니다. 구현상 완전 반대 상황이기 때문이죠.

- 인자로 들어오는 컴포넌트의 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>
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} />} />
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]); }
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 }
유의할 점

커스텀 훅이 HOC 나 Render props 와 같은 디자인 패턴을 많이 대체한 것은 맞지만, 모든 것을 전부 대체한 은탄환이라고 생각하는 것은 좋지 않은 것 같습니다. 사용상의 장단점이 있다고 생각하고 필요한 상황에서 적절히 사용하는 것이 중요하다고 생각합니다.