우리 서비스 크루루에서 PopOverMenu
는 다양한 자식 요소를 지원해야 했습니다. 특히, 메뉴 항목 내부에서 하위 메뉴를 트리거하는 SubTrigger
요소가 필요하다는 점을 인지하게 되었고, 이를 구성하기 위해 고민한 내용을 공유합니다.
기존 PopOverMenu
는 단일 레벨의 메뉴를 지원했으나, 단계적으로 이동하는 메뉴를 복합적으로 구성할 필요성이 제기되었습니다. 특히 사용자가 메뉴를 통해 여러 옵션을 계층적으로 탐색하는 상황에서, 하위 트리거(SubTrigger)를 제공하여 자연스럽게 동작하는 UI가 요구되었습니다.
2. SubTrigger 구현 방식의 선택: Prop Drilling vs 합성 컴포넌트 패턴
SubTrigger를 구현하는 방법으로 두 가지 접근 방식을 고려했습니다.
- Prop Drilling. 컴포넌트 간 데이터를 전달하기 위해 상위 컴포넌트에서 하위 컴포넌트로
props
를 계속 전달하는 방식입니다. 하지만 이 방식은 깊은 트리 구조에서 비효율적이며, 유지보수가 어려워질 가능성이 큽니다. - 합성 컴포넌트 패턴. Shadcn/ui의 Dropdown 컴포넌트처럼, 합성 컴포넌트를 활용하여 메뉴를 계층적으로 구성하는 방식을 참고했습니다. 이 방식은 유연성과 확장성이 뛰어나지만, 컴포넌트 설계 및 사용에 있어 복잡성이 증가할 수 있습니다.
3. 객체 기반 트리구조와 재귀 렌더링 선택
크루루 서비스에서는 Prop Drilling의 방식이지만, 유지보수가 조금 더 용이한 객체 기반의 트리구조를 활용한 재귀 렌더링 방식을 선택했습니다. 이 방식의 장점은 다음과 같습니다.
- 트리 형태의 데이터를 제공하면, 각 메뉴 항목이 자동으로 렌더링되도록 구현하여 사용성을 높였습니다. 이는 개발자 경험의 향상에 큰 도움을 줍니다.
- 트리구조 데이터를 바탕으로 동작하기 때문에, 데이터만 수정하면 메뉴 구조를 변경할 수 있습니다. 이는 일반적인 Prop Drilling방식의 유지보수성의 단점을 보완할 수 있는 문제라 생각했습니다.
4. DropdownItemRenderer: 객체 기반 재귀 렌더링 구조
DropdownItemRenderer
는 트리구조 데이터를 기반으로 Clickable
항목과 SubTrigger
항목을 구분하여 렌더링합니다. 하위 요소가 있을 경우 재귀적으로 호출되어 메뉴가 자동으로 중첩 구조를 가질 수 있습니다.
데이터 구조 정의
메뉴 항목은 다음과 같은 구조를 가집니다:
interface BaseItem {
id: number | string;
name: string;
isHighlight?: boolean;
hasSeparate?: boolean;
}
interface ClickableItem extends BaseItem {
type: 'clickable';
onClick: ({ targetProcessId }: { targetProcessId: number }) => void;
}
interface SubTrigger extends BaseItem {
type: 'subTrigger';
items: (ClickableItem | SubTrigger)[];
}
export type DropdownItemType = ClickableItem | SubTrigger;
DropdownItemRenderer
컴포넌트
이 컴포넌트는 items
데이터를 기반으로 항목을 렌더링합니다. 주요 역할은 다음과 같습니다:
Clickable
컴포넌트: 클릭 가능한 항목은DropdownItem
을 통해 렌더링됩니다.SubTrigger
컴포넌트: 하위 메뉴를 트리거하는 항목은DropdownSubTrigger
를 통해 렌더링되며, 내부에서 재귀 호출로 하위 항목을 처리합니다.- 화면 경계를 초과하지 않도록
checkElementPosition
속성을 활용해SubTrigger
의 위치를 동적으로 결정합니다.
function Clickable({ item, size }: { item: ClickableItem; size: 'sm' | 'md' }) {
return (
<DropdownItem
key={item.id}
size={size}
onClick={() => {
item.onClick({ targetProcessId: Number(item.id) });
}}
item={item.name}
isHighlight={item.isHighlight}
hasSeparate={item.hasSeparate}
/>
);
}
function SubTrigger({
item,
size,
subContentPlacement,
}: {
item: SubTrigger;
size: 'sm' | 'md';
subContentPlacement: 'left' | 'right';
}) {
return (
<DropdownSubTrigger
size={size}
key={item.id}
item={item.name}
placement={subContentPlacement}
>
<DropdownItemRenderer
size={size}
items={item.items}
/>
</DropdownSubTrigger>
);
}
export default function DropdownItemRenderer({ items, size = 'sm' }: DropdownItemRendererProps) {
const ref = useRef<HTMLDivElement>(null);
const [isRight, setIsRight] = useState(false);
useEffect(() => {
if (ref.current) {
const { isRight: _isRight } = checkElementPosition(ref.current);
setIsRight(_isRight);
}
}, []);
return (
<div ref={ref}>
{items.map((item: DropdownItemType, index: number) => {
if (item.type === 'clickable') {
return (
<Clickable
key={index}
item={item}
size={size}
/>
);
}
if (item.type === 'subTrigger') {
return (
<SubTrigger
key={index}
item={item}
size={size}
subContentPlacement={isRight ? 'left' : 'right'}
/>
);
}
})}
</div>
);
}
5. 결론
객체 기반 트리구조를 활용한 재귀 렌더링 방식을 통해 개발자 경험을 크게 향상시킬 수 있는 경험을 했습니다. 데이터 기반의 렌더링 구조를 가져가면서, 유지보수성을 높힐 수 있는 방법을 고안했다 생각합니다.
'Front End > React' 카테고리의 다른 글
[React] useLayoutEffect와 useEffect (0) | 2024.11.22 |
---|---|
Tanstack Query의 useQuery 캐싱 매커니즘 분석하기 (0) | 2024.11.21 |
[디버깅] 크루루 서비스의 자동 로그인 문제와 Tanstack Query 캐싱 이슈 해결기 (0) | 2024.11.13 |
React 서비스 SEO 성능 개선하기 (feat. SSR) (1) | 2024.09.22 |
[React] Drag & Drop (DnD) 직접 구현하기 (2). Mouse Event와 상태관리를 사용하여 구현하기 (0) | 2024.09.17 |