Render Delegation은 React와 같은 UI 프레임워크에서 렌더링 성능을 최적화하기 위한 디자인 패턴입니다.
기본 개념
- 부모 컴포넌트가 자식 컴포넌트에게 렌더링 책임을 위임하는 패턴
- 복잡한 렌더링 로직을 분리하여 관리하기 쉽게 만듦
- 불필요한 리렌더링을 방지하여 성능 향상
주요 장점
- 성능 최적화: 필요한 부분만 렌더링되어 전체 성능 향상
- 코드 분리: 렌더링 로직을 별도 컴포넌트로 분리하여 유지보수성 향상
- 재사용성: 위임받은 렌더링 로직을 여러 곳에서 재사용 가능
// 좋지 않은 예시
const ParentComponent = () => {
const [data, setData] = useState([]);
return (
<div>
{data.map(item => (
<div key={item.id}>
<h2>{item.title}</h2>
<p>{item.description}</p>
// 복잡한 렌더링 로직
</div>
))}
</div>
);
};
// Render Delegation을 적용한 좋은 예시
const ItemRenderer = ({ item }) => (
<div>
<h2>{item.title}</h2>
<p>{item.description}</p>
// 복잡한 렌더링 로직
</div>
);
const ParentComponent = () => {
const [data, setData] = useState([]);
return (
<div>
{data.map(item => (
<ItemRenderer key={item.id} item={item} />
))}
</div>
);
};
사용하면 좋은 상황
- 복잡한 리스트나 테이블을 렌더링할 때
- 조건부 렌더링 로직이 복잡할 때
- 특정 컴포넌트의 렌더링 로직을 재사용하고 싶을 때
주의사항
- 과도한 분리는 오히려 복잡성을 증가시킬 수 있음
- props drilling 문제가 발생할 수 있으므로 적절한 상태 관리 필요
- 불필요한 추상화는 피해야 함
비교 예시
다형성 컴포넌트
// 기본적인 Polymorphic 컴포넌트 예시
type PolymorphicProps<E extends React.ElementType> = {
as?: E;
children?: React.ReactNode;
} & React.ComponentPropsWithoutRef<E>;
const Text = <E extends React.ElementType = 'span'>({
as,
children,
...props
}: PolymorphicProps<E>) => {
const Component = as || 'span';
return <Component {...props}>{children}</Component>;
};
// 사용 예시
<Text as="h1">제목</Text>
<Text as="p">문단</Text>
장점
- 투명성: as props를 통한 명확한 흐름 파악
- 단순성: 런타임 로직이 적고 props 기반 동작
- 예측 가능성: 코드 흐름 추적이 용이
// ✅ 좋은 예시 - Atomic 레벨 컴포넌트
const Button = <E extends React.ElementType = 'button'>({
as,
...props
}: PolymorphicProps<E>) => {
const Component = as || 'button';
return <Component {...props} />;
};
// ❌ 피해야 할 예시 - 복잡한 컴포넌트
const ComplexCard = <E extends React.ElementType>({
as,
// 많은 비즈니스 로직과 상태...
}: PolymorphicProps<E>) => {
// 복잡한 로직은 polymorphic 패턴과 맞지 않음
};
// 유틸리티 타입으로 복잡성 관리
type AsProp<C extends React.ElementType> = {
as?: C;
};
type PropsToOmit<C extends React.ElementType, P> = keyof (AsProp<C> & P);
// 재사용 가능한 Polymorphic 타입
type PolymorphicComponentProp
C extends React.ElementType,
Props = {}
> = React.PropsWithChildren<Props & AsProp<C>> &
Omit<React.ComponentPropsWithoutRef<C>, PropsToOmit<C, Props>>;
Rendering Delegation Pattern
// Radix UI 스타일의 Rendering Delegation 예시
const Dialog = {
Root: ({ children, ...props }) => (
<DialogPrimitive.Root {...props}>
{children}
</DialogPrimitive.Root>
),
Trigger: ({ children, asChild, ...props }) => (
<DialogPrimitive.Trigger asChild={asChild} {...props}>
{children}
</DialogPrimitive.Trigger>
),
};
// 사용 예시
<Dialog.Root>
<Dialog.Trigger asChild>
<CustomButton />
</Dialog.Trigger>
</Dialog.Root>
장점
- 유연성: 런타임에서 동적 컴포넌트 조합 가능
- 확장성: 기존 컴포넌트의 기능을 유연하게 확장
// ✅ 좋은 예시 - 명확한 기능 정의
const Select = {
// 기본 동작과 API가 명확히 정의됨
Root: ({ children, ...props }) => {
const [value, setValue] = useState(null);
return (
<SelectContext.Provider value={{ value, setValue }}>
{children}
</SelectContext.Provider>
);
},
// 각 서브컴포넌트의 역할이 명확함
Trigger: ({ asChild, ...props }) => {},
Content: ({ asChild, ...props }) => {},
};
// ❌ 피해야 할 예시 - 불명확한 기능 정의
const AmbiguousComponent = {
Root: ({ asChild }) => {}, // 기능이 불명확
// 확장 포인트가 불분명
};
// 단순한 사용 사례
const SimpleButton = ({ children, ...props }) => (
<button {...props}>{children}</button>
);
// Rendering Delegation이 필요한 경우
const ComplexDialog = {
Root: ({ children }) => (
<DialogContext.Provider>
{children}
</DialogContext.Provider>
),
// 실제로 필요한 경우에만 asChild 패턴 사용
Content: ({ asChild, ...props }) => {
if (asChild) {
return cloneElement(children, props);
}
return <div {...props} />;
},
};
비교 결론
Polymorphic
- Atomic 레벨 컴포넌트에 적합
- 타입 시스템 구축이 선행되어야 함
- 코드 흐름이 명확하고 예측 가능
Rendering Delegation
- 명확한 기능 정의가 선행되어야 함
- 불필요한 복잡성 주의
- 실제 필요한 경우에만 선별적으로 적용