1. Radix UI와 shadcn/ui를 활용한 Dropdown 컴포넌트 조각 만들기
처음에는 Radix UI의 DropdownMenu 프리미티브를 기반으로 드롭다운 컴포넌트의 기본 조각을 만들었다. Radix UI는 접근성과 유연성을 갖춘 컴포넌트를 제공하여 효율적인 개발이 가능했다. dropdown-primitive.tsx 파일을 생성하고 Radix UI를 통한 shacn/ui에서 제공하는 다양한 컴포넌트를 커스터마이징하여 스타일을 적용했다.
// dropdown-primitive.tsx 파일
"use client"
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"
import {
CheckIcon,
ChevronRightIcon,
DotFilledIcon,
} from "@radix-ui/react-icons"
import * as React from "react"
import { cn } from "@/utils"
const DropdownMenu = DropdownMenuPrimitive.Root
const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger
// ... 기타 컴포넌트들 커스터마이징 ...
export {
DropdownMenu,
DropdownMenuCheckboxItem,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuPortal,
DropdownMenuRadioGroup,
DropdownMenuRadioItem,
DropdownMenuSeparator,
DropdownMenuShortcut,
DropdownMenuSub,
DropdownMenuSubContent,
DropdownMenuSubTrigger,
DropdownMenuTrigger
}
2. 컴포넌트 조각을 활용한 Custom Dropdown 구조 만들기
초기 구조
기본 드롭다운 조각을 바탕으로 커스터마이징된 드롭다운 컴포넌트를 구현했다. custom-dropdown.tsx 파일을 생성하고 Radix UI의 프리미티브를 조합하여 하나의 통합된 드롭다운 컴포넌트를 완성했다. 트리거와 콘텐츠를 분리하여 재사용성을 높였다.
// Dropdown 컴포넌트의 타입 정의
interface DropdownProps {
trigger: React.ReactNode;
children: React.ReactNode;
}
export const Dropdown: React.FC<DropdownProps> = ({ trigger, children }) => {
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
{trigger}
</DropdownMenuTrigger>
<DropdownMenuContent>
{children}
</DropdownMenuContent>
</DropdownMenu>
);
};
Dropdown.displayName = "Dropdown";
트리거를 Children으로 받고, 콘텐츠를 items 배열로 받도록 수정
드롭다운의 유연성을 높이기 위해 트리거를 children으로 받고, 콘텐츠를 배열 형태로 받아 렌더링하도록 수정했다. 이를 통해 다양한 트리거 버튼과 드롭다운 아이템 구성을 손쉽게 변경할 수 있게 되었다. 예를 들어, Button 컴포넌트를 트리거로 사용하여 일관된 디자인을 유지하면서도 드롭다운 기능을 추가할 수 있었다. 드롭다운 콘텐츠를 배열로 받아 동적으로 렌더링함으로써 복잡한 아이템 구조를 손쉽게 관리할 수 있게 되었다.
// custom-dropdown.tsx 파일 수정 부분
export const Dropdown: React.FC<DropdownProps> = ({
children,
items,
defaultValue = [],
defaultOpen,
onOpenChange,
onChange,
className
}) => {
// ... 기존 코드 ...
return (
<DropdownMenu defaultOpen={defaultOpen} onOpenChange={onOpenChange}>
<DropdownMenuTrigger asChild className={className}>
{children} {/* 트리거를 children으로 받음 */}
</DropdownMenuTrigger>
<DropdownMenuContent className={className}>
{items.map((item, index) => (
// 렌더링 로직
))}
</DropdownMenuContent>
</DropdownMenu>
)
}
3. 초기 DropdownItems 타입 설정
드롭다운 아이템의 다양한 유형을 지원하기 위해 TypeScript 타입을 설정했다. 이를 통해 각 아이템의 속성을 명확히 정의하고, 컴파일 타임에 타입 체크를 통해 오류를 방지할 수 있었다.
// src/components/ui/dropdown.tsx
// 드롭다운 아이템 타입 정의
type DropdownItem =
| {
type: "item"
label: string
onSelect?: () => void
shortcut?: string
}
| {
type: "separator"
}
| {
type: "label"
label: string
inset?: boolean
}
| {
type: "checkbox"
label: string
checked: boolean
onCheckedChange?: (checked: boolean) => void
shortcut?: string
}
| {
type: "radio-group"
label?: string
items: {
type: "radio"
label: string
value: string
shortcut?: string
}[]
}
| {
type: "sub"
label: string
items: DropdownItem[]
inset?: boolean
}
4. DropdownItemRenderer 컴포넌트 도입
드롭다운 아이템을 재귀적으로 렌더링하기 위해 별도의 DropdownItemRenderer 컴포넌트를 도입했다. 이를 통해 코드의 중복을 줄이고, 각 아이템 타입에 맞는 렌더링 로직을 명확하게 분리할 수 있었다.
interface DropdownItemRendererProps {
item: DropdownItem
handleSelect: (selectedItem: SelectedItem) => void
handleDeselect: (selectedItem: SelectedItem) => void
selectedItems: SelectedItem[]
}
const DropdownItemRenderer: React.FC<DropdownItemRendererProps> = ({
item,
handleSelect,
handleDeselect,
selectedItems,
}) => {
switch (item.type) {
case "item":
return (
<DropdownMenuItem onSelect={item.onSelect}>
{item.label}
</DropdownMenuItem>
)
case "separator":
return <DropdownMenuSeparator />
case "label":
return <DropdownMenuLabel inset={item.inset}>{item.label}</DropdownMenuLabel>
case "checkbox":
const isChecked = selectedItems.some(
(selected) =>
selected.value === item.value &&
selected.type === "checkbox"
)
return (
<DropdownMenuCheckboxItem
checked={isChecked}
onCheckedChange={(checked) =>
checked
? handleSelect({ type: "checkbox", label: item.label, value: item.value })
: handleDeselect({ type: "checkbox", label: item.label, value: item.value })
}
>
{item.label}
</DropdownMenuCheckboxItem>
)
case "radio-group":
return (
<DropdownMenuRadioGroup
value={
selectedItems.find(
(selected) => selected.type === "radio" && selected.value
)?.value
}
onValueChange={(value) =>
handleSelect({ type: "radio", label: value, value })
}
>
{item.items.map((radioItem, index) => (
<DropdownMenuRadioItem key={index} value={radioItem.value}>
{radioItem.label}
</DropdownMenuRadioItem>
))}
</DropdownMenuRadioGroup>
)
case "sub":
return (
<DropdownMenuSub>
<DropdownMenuSubTrigger>{item.label}</DropdownMenuSubTrigger>
<DropdownMenuSubContent>
{item.items.map((subItem, index) => (
<DropdownItemRenderer
key={index}
item={subItem}
handleSelect={handleSelect}
handleDeselect={handleDeselect}
selectedItems={selectedItems}
/>
))}
</DropdownMenuSubContent>
</DropdownMenuSub>
)
default:
return null
}
}
export default DropdownItemRenderer
Dropdown 컴포넌트 내에서 DropdownItemRenderer를 사용하여 각 아이템을 렌더링하도록 수정했다. 이를 통해 각 아이템 타입에 맞는 렌더링 로직이 명확히 분리되었고, 코드의 재사용성과 유지보수성이 향상되었다. 서브 메뉴도 재귀적으로 렌더링할 수 있게 되었다.
// DropdownItemRenderer을 적용한 custom-dropdown.tsx
export const Dropdown: React.FC<DropdownProps> = ({
children,
items,
defaultValue = [],
defaultOpen,
onOpenChange,
onChange,
className
}) => {
// ... 기존 코드 ...
return (
<DropdownMenu defaultOpen={defaultOpen} onOpenChange={onOpenChange}>
<DropdownMenuTrigger asChild className={className}>
{children}
</DropdownMenuTrigger>
<DropdownMenuContent className={className}>
{items.map((container, index) => (
<DropdownContainerRenderer
key={index}
container={container}
handleSelect={handleSelect}
handleDeselect={handleDeselect}
selectedItems={selectedItems}
/>
))}
</DropdownMenuContent>
</DropdownMenu>
)
}
5. 드롭다운 고도화
드롭다운 컴포넌트의 기능을 강화하기 위해 선택된 항목을 관리하는 상태관리 기능을 추가했다. 드롭다운 컴포넌트의 상태 관리가 체계적이고 유연해졌다. 사용자가 선택한 항목들을 효과적으로 추적하고 외부 컴포넌트와의 연동도 용이해졌다.
1. electedItems 상태를 도입하고, onChange 프로퍼티를 통해 외부에서 선택 상태를 추적할 수 있도록 했다.
// custom-dropdown.tsx 파일 내 handleSelect 함수
const handleSelect = (selectedItem: SelectedItem) => {
setSelectedItems((prev) => {
if (prev.some(item => item.value === selectedItem.value)) {
return prev.filter(item => !(item.value === selectedItem.value))
} else {
return [
...prev.filter(item => item.type !== "default"),
selectedItem
]
}
}
}
2. DropdownProps 인터페이스에 onChange 프로퍼티를 추가하여 외부에서 선택 상태를 추적할 수 있게 했다.
// DropdownProps 인터페이스 수정
export interface DropdownProps {
children: React.ReactNode
items: DropdownContainer[]
defaultValue?: SelectedItem[]
defaultOpen?: boolean
onOpenChange?: (open: boolean) => void
onChange?: (selectedItems: SelectedItem[]) => void
className?: string
}
3. useEffect를 사용하여 selectedItems 상태가 변경될 때마다 onChange 핸들러를 호출하도록 했다.
// custom-dropdown.tsx 파일 내 useEffect 추가
React.useEffect(() => {
if (onChange) {
onChange(selectedItems)
}
}, [selectedItems, onChange])
6. 타입고도화, 공통 타입 설정
드롭다운 아이템의 다양한 유형을 통합적으로 관리하기 위해 공통 타입을 설정했다.
- 모든 아이템은 label과 value 속성을 공통으로 가지도록 정의하여 선택된 항목을 일관된 형태로 관리할 수 있게 했다.
- 공통 타입을 설정함으로써 드롭다운 아이템 간의 일관성을 유지할 수 있었다.
- 컴포넌트의 로직을 단순화하고, 선택된 항목을 관리하는 데 혼란을 줄일 수 있었다.
- 타입을 최적화하면서 중복되는 정의를 줄이고, 각 아이템 타입별로 필요한 속성을 명확히 구분했다.
export type DropdownType = "item" | "separator" | "label" | "checkbox" | "radio-group" | "sub"
export interface SelectedItem {
type: DropdownType
label: string
value: string
}
export interface DropdownItemBase {
type: DropdownType
}
export interface DropdownItemItem extends DropdownItemBase {
type: "item"
label: string
value: string
disabled?: boolean
}
export interface DropdownItemSeparator extends DropdownItemBase {
type: "separator"
}
export interface DropdownItemLabel extends DropdownItemBase {
type: "label"
label: string
inset?: boolean
}
export interface DropdownItemCheckbox extends DropdownItemBase {
type: "checkbox"
label: string
value: string
disabled?: boolean
}
export interface RadioOption {
label: string
value: string
disabled?: boolean
}
export interface DropdownItemRadioGroup extends DropdownItemBase {
type: "radio-group"
label?: string
options: RadioOption[]
}
export interface DropdownItemSub extends DropdownItemBase {
type: "sub"
label: string
items: DropdownItem[]
inset?: boolean
}
export type DropdownItem =
| DropdownItemItem
| DropdownItemSeparator
| DropdownItemLabel
| DropdownItemCheckbox
| DropdownItemRadioGroup
| DropdownItemSub
export interface DropdownProps {
children: React.ReactNode
items: DropdownItem[]
defaultValue?: SelectedItem[]
defaultOpen?: boolean
onOpenChange?: (open: boolean) => void
onChange?: (selectedItems: SelectedItem[]) => void
}
export interface DropdownItemRendererProps {
item: DropdownItem
handleSelect?: (selectedItem: SelectedItem) => void
handleDeselect?: (selectedItem: SelectedItem) => void
selectedItems: SelectedItem[]
}
7. 타입 재설계 및 DropdownContainerRenderer
배경
- 기존에는 DropdownItemRenderer 컴포넌트를 사용하여 각각의 드롭다운 아이템 타입을 개별적으로 렌더링.
- 연관성 관리의 복잡성: 라벨과 아이템 간의 관계를 명확히 유지하기 어렵게 되었고, 특히 서브 메뉴와 같은 복잡한 구조에서는 라벨과 아이템 간의 연관성을 추적하기 힘들어졌다.
- 코드 중복: 유사한 렌더링 로직이 여러 컴포넌트에 중복되면서 코드의 가독성과 유지보수성이 저하되었다.
- 확장성 제한: 새로운 아이템 타입을 추가하거나 기존 타입을 수정할 때, DropdownItemRenderer 내에서 모든 경우를 처리해야 하므로 코드가 복잡해지고 확장성이 떨어졌다.
DropdownContainer 기반 타입 재설계
이러한 문제점을 해결하기 위해 드롭다운 아이템을 보다 체계적으로 관리할 수 있는 DropdownContainer 타입을 도입하고, 타입 정의를 다음과 같이 재설계했다
이 재설계를 통해 드롭다운 아이템을 컨테이너 단위로 그룹화할 수 있게 되었고, 각 컨테이너는 특정 타입(default, checkbox, radio)을 가지며, 관련된 아이템들을 포함하게 되었다. 이를 통해 라벨과 아이템 간의 연관성을 명확히 유지할 수 있었고, 코드의 중복을 줄이며, 확장성을 크게 향상시킬 수 있었다.
// src/components/ui/type.ts
import React from "react"
export type DropdownContainerType = "default" | "checkbox" | "radio"
export type DropdownItemType = "default" | "checkbox" | "radio" | "separator" | "title" | "sub"
// 선택된 아이템 타입
export interface SelectedItem {
containerType: DropdownContainerType
containerKey: string
label: string
value: string
}
// 컨테이너 타입
export interface DropdownContainer {
containerType: DropdownContainerType // 컨테이너 타입
containerKey: string // 컨테이너 타이틀 키 (radio 그룹 식별자)
containerTitle: string // 컨테이너 타이틀
isShowTitle?: boolean // 컨테이너 타이틀 표시 여부
items: DropdownItem[] // 컨테이너 안에 들어갈 아이템들
isShowSeparator?: boolean // 컨테이너 구분선 표시 여부
}
// 드롭다운 아이템 기본 타입
export interface DropdownItemBase {
type: DropdownItemType
}
// 선택 가능한 드롭다운 아이템 타입
export interface DropdownSelectableItem extends DropdownItemBase {
type: "default" | "checkbox" | "radio"
label: string
value: string // 추가된 속성
disabled?: boolean // 추가된 속성
icLeft?: React.ReactNode
icRight?: React.ReactNode
onSelect?: () => void
}
// 드롭다운 구분선 아이템 타입
export interface DropdownSeparatorItem extends DropdownItemBase {
type: "separator"
}
// 드롭다운 타이틀 아이템 타입
export interface DropdownTitleItem extends DropdownItemBase {
type: "title"
label: string
inset?: boolean
}
// 드롭다운 서브 메뉴 아이템 타입
export interface DropdownSubItem extends DropdownItemBase {
type: "sub"
label: string
items: DropdownContainer[]
inset?: boolean
icLeft?: React.ReactNode
icRight?: React.ReactNode
}
export type DropdownItem =
| DropdownSelectableItem
| DropdownSeparatorItem
| DropdownTitleItem
| DropdownSubItem
DropdownContainerRenderer 컴포넌트 설계
기존의 DropdownItemRenderer를 제거하고, 모든 드롭다운 아이템을 DropdownContainerRenderer 내에서 처리하도록 컴포넌트를 재설계했다. 새로운 설계는 컨테이너 기반 구조를 채택하여 드롭다운의 라벨과 아이템 간의 연관성을 명확히 하고, 코드 중복을 줄이며, 유지보수성을 향상시켰다.
주요 개선점:
- 컨테이너 기반 구조: 하나의 컨테이너를 기준으로 관련된 드롭다운 아이템들을 그룹화하여 라벨과 아이템 간의 연관성을 명확히 했다. 이는 코드의 가독성과 관리 용이성을 크게 향상시켰다.
- 재귀적 렌더링 지원: 서브 메뉴와 같이 다중 레벨의 메뉴 구조를 자연스럽게 지원할 수 있게 되었다. DropdownContainerRenderer는 재귀적으로 호출되어 복잡한 메뉴 구조도 손쉽게 구현할 수 있다.
- 코드 중복 감소: 공통된 렌더링 로직을 DropdownContainerRenderer 내에 통합함으로써 코드 중복을 줄이고, 유지보수성을 향상시켰다.
- 타입 안전성 강화: DropdownContainer 타입을 통해 각 컨테이너의 타입별로 적절한 아이템 렌더링 로직을 적용할 수 있게 했다. 이는 TypeScript의 타입 체크를 통해 오류를 사전에 방지할 수 있게 되었다.
// custom-dropdown.tsx 파일 내 DropdownContainerRenderer 컴포넌트
const DropdownContainerRenderer: React.FC<DropdownContainerRendererProps> = ({
container,
handleSelect,
handleDeselect,
selectedItems,
}) => {
// ...코드 생략
return (
<React.Fragment>
{isShowTitle && (
<DropdownMenuLabel inset={false}>
{containerTitle}
</DropdownMenuLabel>
)}
{containerType === "radio" ? (
<DropdownMenuRadioGroup>
// ...DropdownMenuRadioItem 렌더링
</DropdownMenuRadioGroup>
) : (
items.map((item, index) => {
switch (item.type) {
case "default":
return (
<DropdownMenuItem key={index}>{item.label}</DropdownMenuItem>
)
case "separator":
return <DropdownMenuSeparator key={index} className="bg-grey-200 px-3" />
case "label":
return (
<DropdownMenuLabel inset={item.inset} key={index}>
{item.label}
</DropdownMenuLabel>
)
case "checkbox":
return (
<DropdownMenuCheckboxItem key={index}>
{item.label}
</DropdownMenuCheckboxItem>
)
case "sub":
return (
<DropdownMenuSub key={index}>
// ... 생략
<DropdownMenuSubContent>
{item.items.map((subContainer, subIndex) => (
<DropdownContainerRenderer {...props} />
))}
</DropdownMenuSubContent>
</DropdownMenuSub>
)
default:
return null
}
})
)}
{isShowSeparator && <DropdownMenuSeparator className="h-1" />}
</React.Fragment>
)
}
8. 사용성 개선
selectedItems 관리 로직 handleSelect 개선
- 각 아이템 타입별로 선택 가능 여부와 취소 기능을 세분화했다.
- 체크박스는 다중 선택을, 라디오는 단일 선택과 취소 불가를, 기본 아이템은 단일 선택과 취소를 지원하도록 했다.
// custom-dropdown.tsx 파일 내 handleSelect 함수 수정
const handleSelect = (selectedItem: SelectedItem) => {
setSelectedItems((prev) => {
switch (selectedItem.type) {
case "checkbox":
if (prev.some(item => item.value === selectedItem.value && item.type === "checkbox")) {
return prev.filter(item => !(item.value === selectedItem.value && item.type === "checkbox"))
} else {
return [...prev, selectedItem]
}
case "radio":
return [
...prev.filter(item => !(item.type === "radio" && item.containerKey === selectedItem.containerKey)),
selectedItem
]
case "default":
if (prev.some(item => item.type === "default" && item.value === selectedItem.value && item.containerKey === selectedItem.containerKey)) {
return prev.filter(item => !(item.type === "default" && item.value === selectedItem.value && item.containerKey === selectedItem.containerKey))
} else {
return [
...prev.filter(item => item.type !== "default"),
selectedItem
]
}
default:
return prev
}
})
}
const handleDeselect = (selectedItem: SelectedItem) => {
setSelectedItems((prev) =>
prev.filter(
(item) =>
!(item.value === selectedItem.value && item.containerKey === selectedItem.containerKey)
)
)
}
드롭다운 자동 닫힘 방지
- 드롭다운 아이템을 선택할 때 자동으로 드롭다운이 닫히는 기본 동작을 변경했다.
- 체크박스나 라디오 타입의 아이템을 선택할 때는 드롭다운이 닫히지 않고, 사용자가 직접 트리거를 다시 클릭해야만 닫히도록 설정했다.
// custom-dropdown.tsx 파일 내 라디오 아이템 onSelect 이벤트 수정
<DropdownMenuRadioItem
key={index}
value={item.value}
disabled={item.disabled}
className={`${isSelected ? "bg-grey-200/50" : ""}`}
onSelect={event =>
event.preventDefault() // 라디오 취소 불가능
}
>
{item.icLeft && <span>{item.icLeft}</span>}
<span className="px-2 w-full">{item.label}</span>
{item.icRight && <span className="ml-auto">{item.icRight}</span>}
</DropdownMenuRadioItem>
DropdownItemType에 action 추가
- 드롭다운 컴포넌트에 새로운 action 타입을 추가하여 액션 버튼을 렌더링할 수 있도록 수정.
export interface DropdownActionItem extends DropdownItemBase {
type: "action"
label: string
onClick: () => void
icLeft?: React.ReactNode
icRight?: React.ReactNode
disabled?: boolean
}
case "action":
return (
<div
key={index}
className="flex justify-center px-2 py-1.5"
>
<Button>{item.label}</Button>
</div>
)
여러줄 드롭다운으로의 확장을 위한 column props 추가
export interface DropdownProps {
// ... 생략
**column?: number**
}
style={{
display: "grid",
gridTemplateColumns: `repeat(${column}, 1fr)`,
}}
9. 스토리북 등록
Storybook을 활용하여 다양한 드롭다운 스토리를 정의했다. 각 스토리는 다양한 아이템 타입과 구조를 포함하여 드롭다운 컴포넌트의 모든 기능을 테스트할 수 있게 했다.
각 스토리는 다양한 아이템 타입과 구조를 포함하여 드롭다운 컴포넌트의 모든 기능을 테스트할 수 있게 했다. 예를 들어, DefaultContainer 스토리는 기본 아이템 타입을, CheckboxContainer 스토리는 체크박스 아이템을, RadioContainer 스토리는 라디오 그룹 아이템을, SubmenuContainer 스토리는 서브 메뉴를 테스트할 수 있도록 구성했다. 이를 통해 드롭다운 컴포넌트가 다양한 시나리오에서 올바르게 동작하는지 검증할 수 있었다.
// src/components/ui/dropdown.stories.tsx 파일 전체 코드
"use client"
import { Button } from "@/ui/button" // 경로를 프로젝트 구조에 맞게 조정
import { Meta, StoryFn } from "@storybook/react"
import { useEffect, useState } from "react"
import { Dropdown, DropdownProps } from "."
import { Icon } from "../icons"
import { SelectedItem } from "./type"
// 예시 아이콘 컴포넌트
const ExampleIconLeft = () => <span>🔍</span>
const ExampleIconRight = () => <span>➡️</span>
// 스토리 메타데이터
export default {
title: "UI/Dropdown",
component: Dropdown,
argTypes: {},
} as Meta
// 기본 템플릿
const Template: StoryFn<DropdownProps> = (args) => {
const [selectedItems, setSelectedItems] = useState<SelectedItem[]>(args.defaultValue || [])
useEffect(() => {
console.log("Selected Items:", selectedItems)
}, [selectedItems])
// Button 텍스트 생성 함수
const getButtonText = () => {
if (selectedItems.length === 0) {
return "Open Dropdown"
} else if (selectedItems.length === 1) {
return selectedItems[0].label
} else {
return `${selectedItems[0].label} 외 ${selectedItems.length - 1}개`
}
}
return (
<div className="flex flex-col items-center justify-center py-10 space-y-8">
{/* 선택된 항목 표시 */}
<div className="flex flex-col items-center gap-2">
<h3 className="text-lg font-semibold">선택된 항목들:</h3>
<div className="min-h-20 min-w-[500px] bg-grey-200/50 rounded-lg p-4">
{selectedItems.length === 0 ? (
<p className="text-center">선택된 항목이 없습니다.</p>
) : (
<ul className="list-disc list-inside">
{selectedItems.map((item, index) => (
<li key={index}>
{`${item.type}: ${item.containerKey} - ${item.label} (${item.value})`}
</li>
))}
</ul>
)}
</div>
</div>
<Dropdown
{...args}
defaultValue={args.defaultValue}
onChange={setSelectedItems}
className="w-56"
>
<Button size="sm" variant="outline" theme="secondary" icRight={<Icon.ArrowDown size="12" />} className="text-medium14 min-w-fit justify-between">
{getButtonText()}
</Button>
</Dropdown>
</div>
)
}
// 'default' 컨테이너 타입 스토리
export const DefaultContainer = Template.bind({})
DefaultContainer.args = {
defaultOpen: true,
items: [
{
type: "default",
label: "Item 1",
value: "item1",
icLeft: <ExampleIconLeft />,
icRight: <ExampleIconRight />,
},
// ... 기타 아이템들 생략 ...
],
}
// 'checkbox' 컨테이너 타입 스토리
export const CheckboxContainer = Template.bind({})
CheckboxContainer.args = {
defaultOpen: true,
items: [
{
type: "checkbox",
label: "Enable feature",
value: "enable_feature",
icLeft: <ExampleIconLeft />,
icRight: <ExampleIconRight />,
},
// ... 기타 체크박스 아이템들 생략 ...
],
}
// 'radio' 컨테이너 타입 스토리
export const RadioContainer = Template.bind({})
RadioContainer.args = {
defaultOpen: true,
items: [
{
type: "radio-group",
label: "Choose an option",
items: [
{
type: "radio",
label: "Option 1",
value: "option1",
icLeft: <ExampleIconLeft />,
icRight: <ExampleIconRight />,
onSelect: () => console.log("Option 1 selected"),
},
// ... 기타 라디오 아이템들 생략 ...
],
},
],
}
// 'sub' 컨테이너 타입 스토리
export const SubmenuContainer = Template.bind({})
SubmenuContainer.args = {
defaultOpen: true,
items: [
{
type: "sub",
label: "More Options",
items: [
{
type: "default",
label: "Sub Item 1",
value: "sub_item1",
icLeft: <ExampleIconLeft />,
icRight: <ExampleIconRight />,
onSelect: () => console.log("Sub Item 1 selected"),
},
// ... 기타 서브 아이템들 생략 ...
],
icLeft: <ExampleIconLeft />,
icRight: <ExampleIconRight />,
},
],
}
// 'Default' 타입 스토리 (모든 타입 포함)
export const Default = Template.bind({})
Default.args = {
defaultOpen: true,
defaultValue: [
{
type: "checkbox",
label: "Enable feature",
value: "enable_feature",
},
{
type: "radio",
label: "Option 1",
value: "option1",
},
],
items: [
{
type: "default",
label: "Item 1",
value: "item1",
disabled: false,
icLeft: <ExampleIconLeft />,
icRight: <ExampleIconRight />,
onSelect: () => console.log("Item 1 selected"),
},
// ... 기타 아이템들 생략 ...
],
}
10. Dropdown 완성
11. 추가적인 개선과 리팩토링
https://kodywiththek.tistory.com/24
Dropdown 컴포넌트 개선: useImperativeHandle 훅으로 외부에서 드롭다운 상태 제어하기
이번 글에서는 React로 제작한 Dropdown 컴포넌트를 개선하여 외부에서 selectedItems 상태를 제어할 수 있도록 한 과정에 대한 내용을 공유하려한다. 지난 Dropdown 컴포넌트 제작기에 관련된 포스팅작
kodywiththek.tistory.com
'개발 일기' 카테고리의 다른 글
[Tailwind CSS] 2. 테일윈드를 더욱 효율적으로 사용하는 방법 (CVA, TailwindMerge, clsx) (2) | 2024.11.09 |
---|---|
[Tailwind CSS] 1. 테일윈드의 동작방식과 동적 스타일링 (0) | 2024.11.09 |
컴포넌트 주도 개발(CDD)을 도입해보면 어떨까? (feat: 디자인시스템 라이브러리 & 스토리북) (1) | 2024.11.06 |
노션 블록 데이터를 활용한 NotionRenderer 설계 및 구현 과정 (2) | 2024.11.03 |
음대생, 프론트엔드개발자, 스타트업에서의 1년, 회고 (4) | 2024.09.04 |