React 디자인 패턴 / HOC 패턴
Source: Dev.to
Higher‑Order Component (HOC) Overview
Higher‑Order Component (HOC)는 컴포넌트를 인수로 받아 일부 로직을 추가하고, 그 로직을 포함하는 새로운 컴포넌트를 반환하는 함수입니다.
HOC를 사용하면 코드를 반복하지 않고 여러 컴포넌트에 걸쳐 동작을 재사용할 수 있습니다.
1️⃣ HOC로 스타일 추가하기
컴포넌트마다 style 객체를 만들지 않고, 전달하는 모든 컴포넌트에 동일한 스타일을 주입하는 HOC를 만들 수 있습니다.
// withStyles.tsx
function withStyles(Component: React.ComponentType) {
return (props: T) => {
const style = { padding: '0.6rem', margin: '4rem' };
return ;
};
}
사용법
const Button = () => Click me!;
const Text = () =>
Hello World!
;
const StyledButton = withStyles(Button);
const StyledText = withStyles(Text);
StyledButton과 StyledText는 withStyles가 제공하는 style prop으로 강화된 원래 컴포넌트입니다.
2️⃣ HOC로 로딩 인디케이터 추가하기
데이터를 가져올 때 요청이 완료될 때까지 Loading… 화면을 보여주고 싶을 때가 많습니다.
여러 컴포넌트에 로딩 로직을 흩뿌리는 대신, withLoader 라는 HOC에 이를 캡슐화할 수 있습니다.
2.1 최소 골격
function withLoader(
Element: React.ComponentType,
url: string
) {
return (props: Omit) => {
// implementation will go here
return ;
};
}
2.2 전체 구현
import { useEffect, useState } from 'react';
function withLoader(
Element: React.ComponentType,
url: string
) {
return (props: Omit) => {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
fetch(url)
.then((res) => res.json())
.then((json) => {
setData(json);
setLoading(false);
})
.catch((err) => {
console.error(err);
setLoading(false);
});
}, [url]);
if (loading) {
return Loading…;
}
// Pass the fetched data as a `data` prop to the wrapped component
return ;
};
}
2.3 컴포넌트에 HOC 적용하기
// CatImages.tsx
type CatImagesProps = {
data: any; // replace `any` with a proper type if you have one
};
function CatImages({ data }: CatImagesProps) {
return (
{data.map((cat: any) => (
))}
);
}
// Export the wrapped component
export default withLoader(CatImages, 'https://api.thecatapi.com/v1/images/search?limit=10');
이제 CatImages는 data prop을 자동으로 받으며, 요청이 진행 중일 때 사용자는 Loading… 메시지를 보게 됩니다.
3️⃣ 여러 HOC 조합하기
HOC를 스택해서 동작을 결합할 수 있습니다.
아래에서는 hovering prop을 제공하는 withHover HOC를 만든 뒤, 이를 withLoader와 조합합니다.
3.1 withHover HOC
import { useState, useCallback } from 'react';
function withHover(Component: React.ComponentType) {
return (props: T) => {
const [hovering, setHovering] = useState(false);
const onMouseEnter = useCallback(() => setHovering(true), []);
const onMouseLeave = useCallback(() => setHovering(false), []);
return (
);
};
}
3.2 withHover + withLoader 조합하기
// WrappedCatImages.tsx
export default withHover(
withLoader(CatImages, 'https://api.thecatapi.com/v1/images/search?limit=10')
);
이제 CatImages 내부에서 withLoader에서 제공되는 data와 withHover에서 제공되는 hovering을 모두 사용하여 조건부 “Hovering” 박스를 렌더링할 수 있습니다.
4️⃣ HOC를 Hook(useHover)으로 교체하기
Hooks는 종종 HOC와 동일한 결과를 더 적은 중첩으로 달성할 수 있습니다.
4.1 useHover hook
import { useRef, useState, useEffect, RefObject } from 'react';
function useHover(): [RefObject, boolean] {
const ref = useRef(null);
const [hovering, setHovering] = useState(false);
useEffect(() => {
const node = ref.current;
if (!node) return;
const handleMouseEnter = () => setHovering(true);
const handleMouseLeave = () => setHovering(false);
node.addEventListener('mouseenter', handleMouseEnter);
node.addEventListener('mouseleave', handleMouseLeave);
return () => {
node.removeEventListener('mouseenter', handleMouseEnter);
node.removeEventListener('mouseleave', handleMouseLeave);
};
}, []);
return [ref, hovering];
}
4.2 CatImages 내부에서 Hook 사용하기
// CatImagesWithHook.tsx
import { useLoader } from './useLoader'; // a hook version of the loader logic
import { useHover } from './useHover';
function CatImagesWithHook() {
const [ref, hovering] = useHover();
const { data, loading } = useLoader('https://api.thecatapi.com/v1/images/search?limit=10');
if (loading) return Loading…;
return (
{hovering && Hovering}
{data.map((cat: any) => (
))}
);
}
Hook 방식을 사용하면 HOC가 도입하는 추가 컴포넌트 레이어를 없앨 수 있어 컴포넌트 트리를 더 평평하게 유지합니다.
5️⃣ HOC와 Hook을 언제 선호할까
- HOC는 횡단 관심사에 적합하며, 이는 어떤 컴포넌트 타입(클래스든 함수든)과도 작동해야 하고, 래핑된 컴포넌트의 API를 변경하지 않으려 할 때 유용합니다.
- Hook은 일반적으로 상태ful 로직에 대한 최신 선호 솔루션이며, 여러 개의 중첩된 HOC에서 발생할 수 있는 “래퍼 지옥”을 피할 수 있기 때문입니다.
React Docs: “대부분의 경우, Hook만으로 충분하며 트리의 중첩을 줄이는 데 도움이 됩니다.”
6️⃣ 깊게 중첩된 HOC 예시 (예시용)
동일한 동작은 훅과 컨텍스트 프로바이더를 조합하면 더 깔끔하게 표현할 수 있습니다.
TL;DR
- HOC는 컴포넌트에 props/로직을 주입합니다 (
withStyles,withLoader,withHover). - 여러 HOC를 조합하여 복잡한 동작을 구성할 수 있습니다.
- 훅(
useHover,useLoader)은 대부분의 경우에 대해 현대적이고 중첩이 적은 대안을 제공합니다.
위 스니펫을 프로젝트에 복사해 넣고 필요에 따라 타입을 조정해 사용하세요!
레이아웃 예시
Hook vs. Higher‑Order Component (HOC)
컴포넌트에 직접 훅을 추가하면 더 이상 HOC로 컴포넌트를 감싸야 할 필요가 없습니다.
HOC를 사용하면 동일한 로직을 여러 컴포넌트에 제공하면서 그 로직을 한 곳에 유지할 수 있습니다. 그러나 훅을 사용하면 컴포넌트 내부에서 커스텀 동작을 추가할 수 있어, 여러 컴포넌트가 이 동작에 의존할 경우 HOC 패턴에 비해 버그가 발생할 위험이 증가할 수 있습니다.
Higher‑Order Component에 적합한 사용 사례
- 해당 컴포넌트가 추가적인 커스텀 로직 없이 독립적으로 동작할 수 있을 때.
Hooks에 적합한 사용 사례
- 동작이 애플리케이션 전체에 퍼져 있지 않고, 하나 혹은 몇 개의 컴포넌트에서만 사용될 때.
- 해당 동작이 컴포넌트에 여러 속성을 추가할 때.