Notice
Recent Posts
Recent Comments
Link
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | |||
5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 20 | 21 | 22 | 23 | 24 | 25 |
26 | 27 | 28 | 29 | 30 | 31 |
Tags
- javascript
- Redux
- go
- java
- css
- designpatterns
- hook
- react-hook-form
- backend
- React
- typescript
- test
- JavaSpring
- golang
- 오블완
- Spring
- Chakra
- tanstackquery
- component
- 웹애플리케이션서버
- springboot
- storybook
- 티스토리챌린지
- frontend
- RTK
- JPA
- ReactHooks
- satisfiles
- Gin
Archives
- Today
- Total
bkdragon's log
합성 컴포넌트 패턴과 React Children API 를 활용한 컴포넌트 설계 본문
Compound Components 패턴이란? (합성 컴포넌트)
- 여러 컴포넌트를 논리적으로 그룹화하여 관련 컴포넌트들을 함께 사용하는 패턴
- HTML의
<select>
와<option>
관계와 유사
React Children API란?
React Children API는 컴포넌트의 자식 요소들을 다루기 위한 유틸리티 메서드들의 집합이다.
주요 메서드
- React.Children.map
- 각 자식 요소에 대해 함수를 실행하고 새로운 배열을 반환한다.
- null이나 undefined는 무시된다.
- React.Children.forEach
- map과 비슷하지만 새로운 배열을 반환하지 않는다.
- 각 자식에 대해 함수 실행만 한다.
- React.Children.count
- 자식 컴포넌트의 총 개수를 반환한다.
- null과 undefined도 포함해서 계산한다.
- React.Children.only
- 자식이 단 하나만 있는지 확인한다.
- 여러 개면 에러를 발생한다.
- React.Children.toArray
- 자식들을 배열로 변환한다.
- key가 자동으로 할당된다.
탭 컴포넌트
탭에 따라 보여지는 내용이 보여지는 컴포넌트를 합성 컴포넌트 패턴과 React Children API를 통해 만들어보자.
index 로 내용을 보여줄지 판단하기 때문에 React Children API 가 필요하다.
주요 컴포넌트 구조
아코디언 컴포넌트의 주요 구조이다.
- Tab (메인)
- Tab.List - Item들이 들어가는 컴포넌트
- Tab.Item - 선택되는 탭
- Tab.Panels - Panel이 들어가는 컴포넌트
- Tab.Panel - 보여지는 컨텐츠
핵심 기술
사용되는 핵심 개념이다.
- Context API
- activeTab 상태 관리
- 메인과 자식 컴포넌트의 상태 공유
- Children API
- Children.map으로 자식 컴포넌트 순회
- props 주입
- isValidElement로 유효성 검사
import clsx from "clsx";
import React, { createContext, FC, isValidElement, useContext, useState } from "react";
interface TabProps {
children: React.ReactNode;
activeTab?: number;
onTabChange?: (tab: number) => void;
}
const TabContext = createContext<{
activeTab: number;
setActiveTab: (tab: number) => void;
}>({
activeTab: 0,
setActiveTab: () => {},
});
const List: FC<{ children: React.ReactNode }> = ({ children }) => {
return (
<div className="flex flex-row gap-4">
{React.Children.map(children, (child, index) => {
if (isValidElement(child) && child.type === Tab.Item) {
return React.cloneElement(child as React.ReactElement, { index });
}
})}
</div>
);
};
const Item: FC<{ children: React.ReactNode; index?: number }> = ({ children, index }) => {
const { activeTab, setActiveTab } = useContext(TabContext);
if (index === undefined) {
throw new Error("index is required");
}
return (
<button className={clsx(activeTab === index && "text-blue-600")} onClick={() => setActiveTab(index)}>
{children}
</button>
);
};
const Panels: FC<{ children: React.ReactNode }> = ({ children }) => {
return (
<div>
{React.Children.map(children, (child, index) => {
if (isValidElement(child) && child.type === Tab.Panel) {
return React.cloneElement(child as React.ReactElement, { index });
}
})}
</div>
);
};
const Panel: FC<{ children: React.ReactNode; index?: number }> = ({ children, index }) => {
const { activeTab } = useContext(TabContext);
return activeTab === index ? <div>{children}</div> : null;
};
const Tab: FC<TabProps> & {
List: typeof List;
Item: typeof Item;
Panels: typeof Panels;
Panel: typeof Panel;
} = ({ children, activeTab: controlledActiveTab, onTabChange }) => {
const [internalActiveTab, setInternalActiveTab] = useState(0);
const isControlled = controlledActiveTab !== undefined;
const activeTab = isControlled ? controlledActiveTab : internalActiveTab;
const setActiveTab = (tab: number) => {
if (isControlled && onTabChange) {
onTabChange(tab);
} else {
setInternalActiveTab(tab);
}
};
return <TabContext.Provider value={{ activeTab, setActiveTab }}>{children}</TabContext.Provider>;
};
Tab.List = List;
Tab.Item = Item;
Tab.Panels = Panels;
Tab.Panel = Panel;
export default Tab;
List 와 Panels 에서 Children.map 으로 children을 순회하여 index 를 넣어준다(Item과 Panel에).
이로 인해 Item과 Panel 에서 activeTab 상태를 업데이트 하거나 렌더링 여부를 결정할 수 있게 된다..
사이드바 컴포넌트
예제를 하나 더 살펴볼건데, 자식 컴포넌트로 Logo와 Content를 만들것이다.
Sidebar 안에서 Sidebar.Logo 와 Sidebar.Content 를 썻을 때, 지정한 위치에 나오게하는 것이다. 만약 그외에 다른걸 쓴다면 렌더링이 되지 않을 것이다. 이번엔 굳이 상태가 필요하진 않다. 조건부로 Logo가 들어가거나 Content가 들어가는 식에 동작이 있을 수는 있다.
import clsx from "clsx";
import React, { Children, FC, isValidElement } from "react";
import { FiChevronsRight, FiX } from "react-icons/fi";
export interface SidebarProps {
isOpen: boolean;
setIsOpen: (isOpen: boolean) => void;
children: React.ReactNode;
}
const Logo: FC<{ onClick?: () => void; children: React.ReactNode }> = ({ onClick, children }) => (
<button onClick={onClick} className="p-4 border-b border-gray-200">
{children}
</button>
);
const Content: FC<{ children: React.ReactNode }> = ({ children }) => <div className="w-full">{children}</div>;
const Sidebar: FC<SidebarProps> & {
Logo: typeof Logo;
Content: typeof Content;
} = ({ isOpen, setIsOpen, children }) => {
const logoElement = Children.toArray(children).find(
(child) => isValidElement(child) && child.type === Sidebar.Logo
);
const contentElement = Children.toArray(children).find(
(child) => isValidElement(child) && child.type === Sidebar.Content
);
return (
<aside className="flex flex-col">
<div className="border-b-2 border-solid border-gray-500">
<div>{logoElement}</div>
</div>
{contentElement}
</aside>
);
};
Sidebar.Logo = Logo;
Sidebar.Content = Content;
export default Sidebar;
Children.toArray 로 배열로 만들고 적절한 요소를 찾는다.
Content와 Logo 순서에 상관없이 지정된 위치에 렌더링 될 것이다.
결론
합성 컴포넌트 패턴과 React Children API를 활용하면 컴포넌트의 구조를 유연하게 설계할 수 있다. 컴포넌트 간의 논리적인 관계를 명확히 하고, 재사용성을 높일 수 있다.
'React' 카테고리의 다른 글
드래그 가능한 모달창 만들기 (0) | 2024.11.11 |
---|---|
Storybook Decorator 로 전역 상태 사용하기 (0) | 2024.11.07 |
Storybook 으로 협업하기 (1) | 2024.10.30 |
useReducer Action 객체 타입 (1) | 2024.10.08 |
BottomSheet (0) | 2024.10.07 |