์๋ฌธ: Headless Component: a pattern for composing React UIs
๋ฆฌ์กํธ UI ์ปจํธ๋กค์ด ๋ ์ ๊ตํด์ง์ ๋ฐ๋ผ ๋ณต์กํ ๋ก์ง์ด ์๊ฐ์ ํํ๊ณผ ์ฝํ๊ฒ ๋ ์ ์์ต๋๋ค. ์ด๋ก ์ธํด ์ปดํฌ๋ํธ์ ๋์์ ์ถ๋ก ํ๊ธฐ ์ด๋ ต๊ณ , ํ ์คํธํ๊ธฐ๋ ์ด๋ ค์์ง๋ฉฐ, ๋ค๋ฅธ ๋ชจ์์ด ํ์ํ ์ ์ฌํ ์ปดํฌ๋ํธ๋ฅผ ๊ตฌ์ถํด์ผ ํ ์๋ ์์ต๋๋ค. ํค๋๋ฆฌ์ค ์ปดํฌ๋ํธ๋ ๋ชจ๋ ๋น์๊ฐ์ ์ธ ๋ก์ง๊ณผ ์ํ ๊ด๋ฆฌ๋ฅผ ์ถ์ถํ์ฌ ์ปดํฌ๋ํธ์ ๋๋๋ฅผ UI์์ ๋ถ๋ฆฌํฉ๋๋ค.
๋ฆฌ์กํธ๋ UI ์ปดํฌ๋ํธ์ UI์ ์ํ ๊ด๋ฆฌ์ ๋ํ ์ฌ๊ณ ๋ฐฉ์์ ํ์ ์ ์ผ๋ก ๋ณํ์์ผฐ์ต๋๋ค. ๊ทธ๋ฌ๋ ์๋ก์ด ๊ธฐ๋ฅ ์์ฒญ์ด๋ ๊ฐ์ ์ด ์์ ๋๋ง๋ค, ๋ณด๊ธฐ์ ๋จ์ํด ๋ณด์ด๋ ์ปดํฌ๋ํธ๋ ์๋ก ์ฝํ ์ํ์ UI ๋ก์ง์ ๋ณต์ก์ฒด๋ก ๋น ๋ฅด๊ฒ ๋ฐ์ ํ ์ ์์ต๋๋ค.
๊ฐ๋จํ ๋๋กญ๋ค์ด ๋ชฉ๋ก์ ๊ตฌ์ถํ๋ค๊ณ ์์ํด ๋ณด์ธ์. ์ฒ์์๋ ์ด๊ธฐ/๋ซ๊ธฐ ์ํ๋ฅผ ๊ด๋ฆฌํ๊ณ ๋ชจ์์ ๋์์ธํ๋ ๋ฑ ๊ฐ๋จํด ๋ณด์ ๋๋ค. ํ์ง๋ง ์ ํ๋ฆฌ์ผ์ด์ ์ด ์ฑ์ฅํ๊ณ ๋ฐ์ ํจ์ ๋ฐ๋ผ ์ด ๋๋กญ๋ค์ด์ ๋ํ ์๊ตฌ ์ฌํญ๋ ๋ฐ์ ํฉ๋๋ค.
aria
์์ฑ์ ๊ด๋ฆฌํด์ผ ํ๊ณ , ๋๋กญ๋ค์ด์ด ์๋ฏธ์ ์ผ๋ก ์ฌ๋ฐ๋ฅธ์ง ํ์ธํด์ผ ํฉ๋๋ค.Enter
ํค๋ฅผ ์ฌ์ฉํ์ฌ ์ ํํ๊ฑฐ๋, Escape
ํค๋ฅผ ์ฌ์ฉํ์ฌ ๋๋กญ๋ค์ด์ ๋ซ์ ์ ์์ด์ผ ํฉ๋๋ค. ์ด๋ฅผ ์ํด์๋ ์ถ๊ฐ์ ์ธ ์ด๋ฒคํธ ๋ฆฌ์ค๋์ ์ํ ๊ด๋ฆฌ๊ฐ ํ์ํฉ๋๋ค.์ด๋ฌํ ๊ฐ ๊ณ ๋ ค ์ฌํญ์ ๋๋กญ๋ค์ด ์ปดํฌ๋ํธ์ ๋ณต์ก์ฑ์ ๋ํฉ๋๋ค. ์ํ, ๋ก์ง ๋ฐ UI ํํ์ด ์์ด๋ฉด ์ ์ง ๊ด๋ฆฌ๊ฐ ์ด๋ ต๊ณ ์ฌ์ฌ์ฉํ๊ธฐ ์ด๋ ต์ต๋๋ค. ์๋ก ์ฝํ ์์์๋ก ์๋ํ์ง ์์ ๋ถ์์ฉ ์์ด ๋ณ๊ฒฝํ๊ธฐ๊ฐ ๋ ์ด๋ ค์์ง๋๋ค.
์ด๋ฌํ ๋ฌธ์ ๋ฅผ ์ ๋ฉด์ผ๋ก ๋ง์ฃผํ ์ํฉ์์, ํค๋๋ฆฌ์ค ์ปดํฌ๋ํธ ํจํด์ ํด๊ฒฐ์ฑ ์ ์ ์ํฉ๋๋ค. ํค๋๋ฆฌ์ค ์ปดํฌ๋ํธ ํจํด์ ๊ณ์ฐ๊ณผ UI ํํ์ ๋ถ๋ฆฌํ์ฌ, ๊ฐ๋ฐ์๊ฐ ๋ค์ฌ๋ค๋ฅํ๊ณ ์ ์ง ๊ด๋ฆฌ๊ฐ ๊ฐ๋ฅํ๋ฉฐ ์ฌ์ฌ์ฉ ๊ฐ๋ฅํ ์ปดํฌ๋ํธ๋ฅผ ๊ตฌ์ถํ ์ ์๋๋ก ์ง์ํฉ๋๋ค.
ํค๋๋ฆฌ์ค ์ปดํฌ๋ํธ๋ ๋ฆฌ์กํธ ๋์์ธ ํจํด์ผ๋ก ์ผ๋ฐ์ ์ผ๋ก ๋ฆฌ์กํธ ํ ์ผ๋ก ๊ตฌํ๋๋ฉฐ, ์ปดํฌ๋ํธ๊ฐ ํน์ UI(์ฌ์ฉ์ ์ธํฐํ์ด์ค)๋ฅผ ๊ท์ ํ์ง ์๊ณ , ๋ก์ง๊ณผ ์ํ ๊ด๋ฆฌ๋ง์ ์ ์ ์ผ๋ก ์ฑ ์์ง๋ ์ปดํฌ๋ํธ์ ๋๋ค. ์ด๋ ์์ ์ โ๋๋โ๋ฅผ ์ ๊ณตํ์ง๋ง โ๊ฒ๋ชจ์ตโ์ ๊ตฌํํ๋ ๊ฐ๋ฐ์์๊ฒ ๋งก๊น๋๋ค. ๋ณธ์ง์ ์ผ๋ก ํน์ ์๊ฐ์ ํํ์ ๊ฐ์ํ์ง ์๊ณ ๊ธฐ๋ฅ์ฑ์ ์ ๊ณตํฉ๋๋ค.
ํค๋๋ฆฌ์ค ์ปดํฌ๋ํธ๋ฅผ ์๊ฐํํด๋ณด์๋ฉด, ํ์ชฝ์์๋ JSX ๋ทฐ์ ์ํธ์์ฉํ๊ณ ๋ค๋ฅธ ํ์ชฝ์์๋ ํ์์ ๋ฐ๋ผ ๊ธฐ๋ณธ ๋ฐ์ดํฐ ๋ชจ๋ธ๊ณผ ํต์ ํ๋ ๊ฐ๋๋ค๋ ๋ ์ด์ด๋ก ๋ํ๋ผ ์ ์์ต๋๋ค. ์ด ํจํด์ ์๊ฐ์ ํํ์์ ๋ก์ง์ ๋ถ๋ฆฌํ๊ธฐ ๋๋ฌธ์, UI์ ๋์ ๋๋ ์ํ ๊ด๋ฆฌ ์ธก๋ฉด๋ง์ ์ํ๋ ๊ฐ๋ฐ์์๊ฒ ํนํ ์ ์ฉํฉ๋๋ค.
์ฌ์ง 1: ํค๋๋ฆฌ์ค ์ปดํฌ๋ํธ ํจํด
์๋ฅผ ๋ค์ด ํค๋๋ฆฌ์ค ๋๋กญ๋ค์ด ์ปดํฌ๋ํธ๋ฅผ ์๊ฐํด ๋ด ์๋ค. ์ด ์ปดํฌ๋ํธ๋ ์ด๊ธฐ/๋ซ๊ธฐ ์ํ, ํญ๋ชฉ ์ ํ, ํค๋ณด๋ ํ์ ๋ฑ์ ๋ํ ์ํ ๊ด๋ฆฌ๋ฅผ ์ฒ๋ฆฌํฉ๋๋ค. ๋ ๋๋งํ ๋๊ฐ ๋๋ฉด ์์ฒด์ ์ผ๋ก ํ๋์ฝ๋ฉ๋ ๋๋กญ๋ค์ด UI๋ฅผ ๋ ๋๋งํ๋ ๋์ , ์ด ์ํ์ ๋ก์ง์ ์์ ํจ์๋ ์ปดํฌ๋ํธ์ ์ ๊ณตํ์ฌ ๊ฐ๋ฐ์๊ฐ ์๊ฐ์ ์ผ๋ก ์ด๋ป๊ฒ ํ์ํ ์ง ๊ฒฐ์ ํ ์ ์๋๋ก ํฉ๋๋ค.
์ด ๊ธ์์๋ ๋ณต์กํ ์ปดํฌ๋ํธ์ธ ๋๋กญ๋ค์ด ๋ชฉ๋ก์ ์ฒ์๋ถํฐ ๋ค์ ๊ตฌ์ฑํ์ฌ ์ค์ ์์ ๋ฅผ ์ดํด๋ณด๊ฒ ์ต๋๋ค. ์ปดํฌ๋ํธ์ ๋ ๋ง์ ๊ธฐ๋ฅ์ ์ถ๊ฐํ๋ฉด์ ๋ฐ์ํ๋ ๋ฌธ์ ๋ฅผ ๊ด์ฐฐํ ๊ฒ์ ๋๋ค. ์ด๋ฅผ ํตํด ํค๋๋ฆฌ์ค ์ปดํฌ๋ํธ ํจํด์ด ์ด๋ป๊ฒ ์ด๋ฌํ ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๊ณ , ์๋ก ๋ค๋ฅธ ๊ด์ฌ์ฌ๋ฅผ ๊ตฌ๋ถํ๋ฉฐ, ๋ณด๋ค ๋ค์ฌ๋ค๋ฅํ ์ปดํฌ๋ํธ๋ฅผ ์ ์ํ๋ ๋ฐ ๋์์ด ๋๋์ง ๋ณด์ฌ๋๋ฆฌ๊ฒ ์ต๋๋ค.
๋๋กญ๋ค์ด ๋ชฉ๋ก์ ๋ง์ ๊ณณ์์ ์ฌ์ฉ๋๋ ์ผ๋ฐ์ ์ธ ์ปดํฌ๋ํธ์ ๋๋ค. ๊ธฐ๋ณธ์ ์ธ ์ฌ์ฉ ์ฌ๋ก๋ฅผ ์ํ ๋ค์ดํฐ๋ธ select ์ปดํฌ๋ํธ๋ ์์ง๋ง, ๊ฐ ์ต์ ์ ๋ ์ ์ ์ดํ ์ ์๋ ๊ณ ๊ธ ๋ฒ์ ์ ๋ ๋์ ์ฌ์ฉ์ ๊ฒฝํ์ ์ ๊ณตํฉ๋๋ค.
์ฌ์ง 2: ๋๋กญ๋ค์ด ๋ชฉ๋ก ์ปดํฌ๋ํธ
์๋ฒฝํ ๊ตฌํ์ ์ํด ์ฒ์๋ถํฐ ์๋ก ๋ง๋ค๋ ค๋ฉด ๋ณด๊ธฐ๋ณด๋ค ๋ ๋ง์ ๋ ธ๋ ฅ์ด ํ์ํฉ๋๋ค. ํค๋ณด๋ ํ์, ์ ๊ทผ์ฑ(์: ์คํฌ๋ฆฐ ๋ฆฌ๋ ํธํ์ฑ), ๋ชจ๋ฐ์ผ ๋๋ฐ์ด์ค์์์ ์ฌ์ฉ์ฑ ๋ฑ์ ๊ณ ๋ คํด์ผ ํฉ๋๋ค.
๋ง์ฐ์ค ํด๋ฆญ๋ง ์ง์ํ๋ ๊ฐ๋จํ ๋ฐ์คํฌํฑ ๋ฒ์ ๋ถํฐ ์์ํ์ฌ, ์ ์ฐจ ๊ธฐ๋ฅ์ ์ถ๊ฐํ์ฌ ํ์ค์ ์ธ ๋๋กญ๋ค์ด์ ๋ง๋ค ๊ฒ์ ๋๋ค. ์ด ๊ธ์ ๋ชฉํ๋ ํ๋ก๋์ ์์ ์ฌ์ฉํ ๋๋กญ๋ค์ด ๋ชฉ๋ก์ ๋ง๋๋ ๋ฐฉ๋ฒ์ ์๋ ค๋๋ฆฌ๋ ๊ฒ์ด ์๋๋ผ ๋ช ๊ฐ์ง ์ํํธ์จ์ด ๋์์ธ ํจํด์ ์๊ฐํ๋ ๊ฒ์ ๋๋ค. ์ฌ์ค ์ฒ์๋ถํฐ ์ด ์์ ์ ์ํํ๋ ๊ฒ์ ๊ถ์ฅํ์ง ์์ผ๋ฉฐ, ๊ทธ ๋์ ์ ๋ง๋ค์ด์ง ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ฅผ ์ฌ์ฉํ๋ ๊ฒ์ ๊ถ์ฅํฉ๋๋ค.
๊ธฐ๋ณธ์ ์ผ๋ก ์ฌ์ฉ์๊ฐ ํด๋ฆญํ ์์(์์ผ๋ก๋ ํธ๋ฆฌ๊ฑฐ ์์๋ผ๊ณ ๋ถ๋ฅด๊ฒ ์ต๋๋ค)์ ๋ชฉ๋ก ํจ๋์ ํ์ ๋ฐ ์จ๊ธฐ๊ธฐ ๋์์ ์ ์ดํ ์ํ๊ฐ ํ์ํฉ๋๋ค. ์ฒ์์๋ ํจ๋์ ์จ๊ธฐ๊ณ ํธ๋ฆฌ๊ฑฐ ์์๊ฐ ํด๋ฆญ๋๋ฉด ๋ชฉ๋ก ํจ๋์ ํ์ํฉ๋๋ค.
import { useState } from 'react';
interface Item {
icon: string;
text: string;
description: string;
}
type DropdownProps = {
items: Item[];
};
const Dropdown = ({ items }: DropdownProps) => {
const [isOpen, setIsOpen] = useState(false);
const [selectedItem, setSelectedItem] = useState<Item | null>(null);
return (
<div className="dropdown">
<div className="trigger" tabIndex={0} onClick={() => setIsOpen(!isOpen)}>
<span className="selection">
{selectedItem ? selectedItem.text : 'Select an item...'}
</span>
</div>
{isOpen && (
<div className="dropdown-menu">
{items.map((item, index) => (
<div
key={index}
onClick={() => setSelectedItem(item)}
className="item-container"
>
<img src={item.icon} alt={item.text} />
<div className="details">
<div>{item.text}</div>
<small>{item.description}</small>
</div>
</div>
))}
</div>
)}
</div>
);
};
์์ ์ฝ๋์์๋ ๋๋กญ๋ค์ด ์ปดํฌ๋ํธ์ ๊ธฐ๋ณธ ๊ตฌ์กฐ๋ฅผ ์ค์ ํ์ต๋๋ค. useState
ํ
์ ์ฌ์ฉํ์ฌ isOpen
๋ฐ selectedItem
์ํ๋ฅผ ๊ด๋ฆฌํ์ฌ ๋๋กญ๋ค์ด์ ๋์์ ์ ์ดํฉ๋๋ค. ํธ๋ฆฌ๊ฑฐ ์์๋ฅผ ๊ฐ๋จํ ํด๋ฆญํ๋ฉด ๋๋กญ๋ค์ด ๋ฉ๋ด๊ฐ ํ ๊ธ๋๊ณ , ํญ๋ชฉ์ ์ ํํ๋ฉด selectedItem
์ํ๊ฐ ์
๋ฐ์ดํธ๋ฉ๋๋ค.
์ปดํฌ๋ํธ๋ฅผ ๋ ๋ช ํํ๊ฒ ๋ณด๊ธฐ ์ํด ๋ ์๊ณ ๊ด๋ฆฌํ๊ธฐ ์ฌ์ด ์กฐ๊ฐ์ผ๋ก ๋ถํดํด ๋ณด๊ฒ ์ต๋๋ค. ์ด ๋ถํด๋ ํค๋๋ฆฌ์ค ์ปดํฌ๋ํธ ํจํด์ ์ผ๋ถ๊ฐ ์๋์ง๋ง, ๋ณต์กํ UI ์ปดํฌ๋ํธ๋ฅผ ์ฌ๋ฌ ์กฐ๊ฐ์ผ๋ก ๋๋๋ ๊ฒ์ ๊ฐ์น ์๋ ํ๋์ ๋๋ค.
์ฌ์ฉ์ ํด๋ฆญ์ ์ฒ๋ฆฌํ๋ Trigger
์ปดํฌ๋ํธ๋ฅผ ์ถ์ถํ๋ ๊ฒ๋ถํฐ ์์ํ๊ฒ ์ต๋๋ค.
const Trigger = ({
label,
onClick,
}: {
label: string;
onClick: () => void;
}) => {
return (
<div className="trigger" tabIndex={0} onClick={onClick}>
<span className="selection">{label}</span>
</div>
);
};
Trigger
์ปดํฌ๋ํธ๋ ํด๋ฆญ ๊ฐ๋ฅํ ๊ธฐ๋ณธ UI ์์๋ก, ํ์ํ label
๊ณผ onClick
ํธ๋ค๋ฌ๋ฅผ ๋งค๊ฐ ๋ณ์๋ก ๋ฐ์ผ๋ฉฐ, ์ฃผ๋ณ ์ปจํ
์คํธ์ ๊ตฌ์ ๋ฐ์ง ์์ต๋๋ค. ๋ง์ฐฌ๊ฐ์ง๋ก ์ต์
ํญ๋ชฉ๋ค์ ๋ชฉ๋ก์ ๋ ๋๋งํ๋ DropdownMenu
์ปดํฌ๋ํธ๋ฅผ ์ถ์ถํ ์ ์์ต๋๋ค.
const DropdownMenu = ({
items,
onItemClick,
}: {
items: Item[];
onItemClick: (item: Item) => void;
}) => {
return (
<div className="dropdown-menu">
{items.map((item, index) => (
<div
key={index}
onClick={() => onItemClick(item)}
className="item-container"
>
<img src={item.icon} alt={item.text} />
<div className="details">
<div>{item.text}</div>
<small>{item.description}</small>
</div>
</div>
))}
</div>
);
};
DropdownMenu
์ปดํฌ๋ํธ๋ ๊ฐ ํญ๋ชฉ์ ์์ด์ฝ๊ณผ ์ค๋ช
์ด ํฌํจ๋ ํญ๋ชฉ๋ค์ ๋ชฉ๋ก์ ํ์ํฉ๋๋ค. ๊ฐ ํญ๋ชฉ์ ํด๋ฆญํ๋ฉด ์ ํ๋ ํญ๋ชฉ์ ์ธ์๋ก ์ ๊ณต๋ onItemClick
ํจ์๊ฐ ์คํ๋ฉ๋๋ค.
๊ทธ๋ฐ ๋ค์ Dropdown
์ปดํฌ๋ํธ ๋ด์์ Trigger
์ DropdownMenu
๋ฅผ ์กฐํฉํ๊ณ ํ์ํ ์ํ๋ฅผ ์ ๊ณตํฉ๋๋ค. ์ด ์ ๊ทผ ๋ฐฉ์์ Trigger
๋ฐ DropdownMenu
์ปดํฌ๋ํธ๊ฐ ์ํ์ ๊ตฌ์ ๋ฐ์ง ์๊ณ , ์ ๋ฌ๋ ํ๋กํผํฐ์๋ง ๋ฐ์ํ๋๋ก ๋ณด์ฅํฉ๋๋ค.
const Dropdown = ({ items }: DropdownProps) => {
const [isOpen, setIsOpen] = useState(false);
const [selectedItem, setSelectedItem] = useState<Item | null>(null);
return (
<div className="dropdown">
<Trigger
label={selectedItem ? selectedItem.text : 'Select an item...'}
onClick={() => setIsOpen(!isOpen)}
/>
{isOpen && <DropdownMenu items={items} onItemClick={setSelectedItem} />}
</div>
);
};
์ ๋ฐ์ดํธ๋ ์ด๋ฒ ์ฝ๋ ๊ตฌ์กฐ์์๋ ๋๋กญ๋ค์ด์ ๊ฐ ๋ถ๋ถ์ ๋ํด ํนํ๋ ์ปดํฌ๋ํธ๋ฅผ ์์ฑํ์ฌ ๊ด๋ จ ์ฌํญ์ ๋ถ๋ฆฌํจ์ผ๋ก์จ ์ฝ๋๋ฅผ ๋์ฑ ์ฒด๊ณ์ ์ด๊ณ , ์ฝ๊ฒ ๊ด๋ฆฌํ ์ ์๋๋ก ํ์ต๋๋ค.
์ฌ์ง 3: ๋ชฉ๋ก ๋ค์ดํฐ๋ธ ๊ตฌํ
์ ์ด๋ฏธ์ง์ ํ์๋ ๊ฒ์ฒ๋ผ โSelect an itemโฆโ ํธ๋ฆฌ๊ฑฐ ์์๋ฅผ ํด๋ฆญํ์ฌ ๋๋กญ๋ค์ด์ ์ด ์ ์์ต๋๋ค. ๋ชฉ๋ก์์ ๊ฐ์ ์ ํํ๋ฉด ํ์๋ ๊ฐ์ด ์ ๋ฐ์ดํธ๋๊ณ ๋๋กญ๋ค์ด ๋ฉ๋ด๊ฐ ๋ซํ๋๋ค.
์ด ์์ ์์ ๋ฆฌํฉํฐ๋ง๋ ์ฝ๋๋ ๊ฐ ์ธ๊ทธ๋จผํธ๊ฐ ๊ฐ๋จํ๊ณ ์ ์์ฑ์ด ๋ฐ์ด๋๋ฉฐ ๋ช
ํํฉ๋๋ค. Trigger
์ปดํฌ๋ํธ๋ฅผ ์์ ํ๊ฑฐ๋, ๋ค๋ฅธ ์ปดํฌ๋ํธ๋ฅผ ๋์
ํ๋ ๊ฒ์ ๋น๊ต์ ๊ฐ๋จํฉ๋๋ค. ํ์ง๋ง ๋ ๋ง์ ๊ธฐ๋ฅ์ ๋์
ํ๊ณ ์ถ๊ฐ ์ํ๋ฅผ ๊ด๋ฆฌํ ๋ ํ์ฌ ์ปดํฌ๋ํธ๋ค์ด ๊ทธ๋๋ก ์ ์ง๋ ์ ์์๊น์?
์์ธํ ๋ด์ฉ์ ํค๋ณด๋ ํ์ ๊ธฐ๋ฅ์ด๋ผ๋ ์ค์ํ ๊ฐ์ ์ฌํญ์ ๋์ ์ ํตํด ์์๋ด ์๋ค.
๋๋กญ๋ค์ด ๋ชฉ๋ก์ ํค๋ณด๋ ํ์ ๊ธฐ๋ฅ์ ํตํฉํ๋ฉด ๋ง์ฐ์ค๋ก ํด์ผ ํ๋ ์์
์ ๋์ฒดํ ์ ์์ด ์ฌ์ฉ์ ๊ฒฝํ์ด ํฅ์๋ฉ๋๋ค. ์ด๋ ์ ๊ทผ์ฑ์ ์ํด ํนํ ์ค์ํ๋ฉฐ ์น ํ์ด์ง์์ ์ํํ ํ์ ํ๊ฒฝ์ ์ ๊ณตํฉ๋๋ค. onKeyDown
์ด๋ฒคํธ ํธ๋ค๋ฌ๋ฅผ ์ฌ์ฉํ์ฌ ์ด๋ฅผ ๋ฌ์ฑํ๋ ๋ฐฉ๋ฒ์ ์ดํด๋ณด๊ฒ ์ต๋๋ค.
๋จผ์ Dropdown
์ปดํฌ๋ํธ์ onKeyDown
์ด๋ฒคํธ์ handleKeyDown
ํจ์๋ฅผ ์ฐ๊ฒฐํ๊ฒ ์ต๋๋ค. ์ฌ๊ธฐ์๋ switch ๋ฌธ์ ์ฌ์ฉํ์ฌ ํน์ ํค๊ฐ ๋๋ ธ๋์ง ํ์ธํ๊ณ ๊ทธ์ ๋ฐ๋ผ ๋์์ ์ํํฉ๋๋ค. ์๋ฅผ ๋ค์ด โEnterโ ๋๋ โSpaceโ ํค๋ฅผ ๋๋ฅด๋ฉด ๋๋กญ๋ค์ด์ด ํ ๊ธ๋ฉ๋๋ค. ๋ง์ฐฌ๊ฐ์ง๋ก โArrowDownโ ๋ฐ โArrowUpโ ํค๋ฅผ ์ฌ์ฉํ๋ฉด ๋ชฉ๋ก ํญ๋ชฉ์ ํ์ํ๊ณ ํ์ํ ๊ฒฝ์ฐ ๋ชฉ๋ก์ ์์ ๋๋ ๋์ผ๋ก ๋์๊ฐ ์ ์์ต๋๋ค.
const Dropdown = ({ items }: DropdownProps) => {
// ... ์ด์ ์ํ ๋ณ์ ...
const [selectedIndex, setSelectedIndex] = useState<number>(-1);
const handleKeyDown = (e: React.KeyboardEvent) => {
switch (
e.key
// ... ์ผ์ด์ค ๊ตฌ๋ฌธ ...
// ... Enter, Space, ArrowDown and ArrowUp ํค์ ๋ํ ํธ๋ค๋ง ...
) {
}
};
return (
<div className="dropdown" onKeyDown={handleKeyDown}>
{/* ... JSX์ ๋๋จธ์ง ๋ถ๋ถ ... */}
</div>
);
};
๋ํ selectedIndex
ํ๋กํผํฐ๋ฅผ ํ์ฉํ๋๋ก DropdownMenu
์ปดํฌ๋ํธ๋ฅผ ์
๋ฐ์ดํธํ์ต๋๋ค. ์ด ํ๋กํผํฐ๋ ๊ฐ์กฐ ํ์๋ CSS ์คํ์ผ์ ์ ์ฉํ๊ณ ํ์ฌ ์ ํ๋ ํญ๋ชฉ์ aria-selected
์์ฑ์ ์ค์ ํ๋ ๋ฐ ์ฌ์ฉ๋์ด ์๊ฐ์ ํผ๋๋ฐฑ๊ณผ ์ ๊ทผ์ฑ์ ํฅ์์ํต๋๋ค.
const DropdownMenu = ({
items,
selectedIndex,
onItemClick,
}: {
items: Item[];
selectedIndex: number;
onItemClick: (item: Item) => void;
}) => {
return (
<div className="dropdown-menu" role="listbox">
{/* ... JSX์ ๋๋จธ์ง ๋ถ๋ถ ... */}
</div>
);
};
์ด์ Dropdown
์ปดํฌ๋ํธ๋ ์ํ ๊ด๋ฆฌ ์ฝ๋์ ๋ ๋๋ง ๋ก์ง์ด ๋ชจ๋ ์ฝํ ์์ต๋๋ค. ์ฌ๊ธฐ์๋ selectedItem
, selectedIndex
, setSelectedItem
๋ฑ๊ณผ ๊ฐ์ ๋ชจ๋ ์ํ ๊ด๋ฆฌ ๊ตฌ์กฐ์ฒด์ ํจ๊ป ๊ด๋ฒ์ํ ์ค์์น ์ผ์ด์ค๊ฐ ๋ค์ด ์์ต๋๋ค.
์ด ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด useDropdown
์ด๋ผ๋ ์ปค์คํ
ํ
์ ํตํด ํค๋๋ฆฌ์ค ์ปดํฌ๋ํธ์ ๊ฐ๋
์ ์๊ฐํ๊ฒ ์ต๋๋ค. ์ด ํ
์ ์ํ ๋ฐ ํค๋ณด๋ ์ด๋ฒคํธ ์ฒ๋ฆฌ ๋ก์ง์ ํจ์จ์ ์ผ๋ก ๋ง๋ฌด๋ฆฌํ์ฌ ํ์ ์ํ์ ํจ์๋ก ์ฑ์์ง ๊ฐ์ฒด๋ฅผ ๋ฐํํฉ๋๋ค. Dropdown
์ปดํฌ๋ํธ์์ ์ด๋ฅผ ๋น๊ตฌ์กฐํํจ์ผ๋ก์จ ์ฝ๋๋ฅผ ๊น๋ํ๊ณ ์ง์ ๊ฐ๋ฅํ๊ฒ ์ ์งํ ์ ์์ต๋๋ค.
๋น๊ฒฐ์ ๋ฐ๋ก ํค๋๋ฆฌ์ค ์ปดํฌ๋ํธ์ ์ฃผ์ธ๊ณต์ธ useDropdown
ํ
์ ์์ต๋๋ค. ์ด ๋ค์ฌ๋ค๋ฅํ ์ ๋์๋ ๋๋กญ๋ค์ด์ด ์ด๋ ค ์๋์ง ์ฌ๋ถ, ์ ํ๋ ํญ๋ชฉ, ๊ฐ์กฐ ํ์๋ ํญ๋ชฉ, Enter ํค์ ๋ํ ๋ฐ์ ๋ฑ ๋๋กญ๋ค์ด์ ํ์ํ ๋ชจ๋ ๊ฒ์ด ๋ค์ด ์์ต๋๋ค. ๋ค์ํ ์๊ฐ์ ํ๋ ์ ํ
์ด์
, ์ฆ JSX ์์์ ๊ฒฐํฉํ ์ ์๋ ์ ์์ฑ์ด ์ฅ์ ์
๋๋ค.
const useDropdown = (items: Item[]) => {
// ... ์ํ ๋ณ์ ...
// ํฌํผ ํจ์๋ UI์ ๋ํ ์ผ๋ถ aria ์์ฑ์ ๋ฐํํ ์ ์์ต๋๋ค.
const getAriaAttributes = () => ({
role: 'combobox',
'aria-expanded': isOpen,
'aria-activedescendant': selectedItem ? selectedItem.text : undefined,
});
const handleKeyDown = (e: React.KeyboardEvent) => {
// ... switch ๊ตฌ๋ฌธ ...
};
const toggleDropdown = () => setIsOpen(isOpen => !isOpen);
return {
isOpen,
toggleDropdown,
handleKeyDown,
selectedItem,
setSelectedItem,
selectedIndex,
};
};
์ด์ Dropdown
์ปดํฌ๋ํธ๊ฐ ๋จ์ํ๋๊ณ ์งง์์ก์ผ๋ฉฐ ์ดํดํ๊ธฐ ์ฌ์์ก์ต๋๋ค. useDropdown
ํ
์ ํ์ฉํ์ฌ ์ํ๋ฅผ ๊ด๋ฆฌํ๊ณ ํค๋ณด๋ ์ํธ ์์ฉ์ ์ฒ๋ฆฌํ์ฌ, ๊ด์ฌ์ฌ๋ฅผ ๋ช
ํํ๊ฒ ๋ถ๋ฆฌํ๊ณ ์ฝ๋๋ฅผ ๋ ์ฝ๊ฒ ์ดํดํ๊ณ ๊ด๋ฆฌํ ์ ์์ต๋๋ค.
const Dropdown = ({ items }: DropdownProps) => {
const {
isOpen,
selectedItem,
selectedIndex,
toggleDropdown,
handleKeyDown,
setSelectedItem,
} = useDropdown(items);
return (
<div className="dropdown" onKeyDown={handleKeyDown}>
<Trigger
onClick={toggleDropdown}
label={selectedItem ? selectedItem.text : 'Select an item...'}
/>
{isOpen && (
<DropdownMenu
items={items}
onItemClick={setSelectedItem}
selectedIndex={selectedIndex}
/>
)}
</div>
);
};
์ด๋ฌํ ์์ ์ ํตํด ๋๋กญ๋ค์ด ๋ชฉ๋ก์ ํค๋ณด๋ ํ์ ๊ธฐ๋ฅ์ ์ฑ๊ณต์ ์ผ๋ก ๊ตฌํํ์ฌ ์ ๊ทผ์ฑ๊ณผ ์ฌ์ฉ์ ํธ์์ฑ์ ๋์์ต๋๋ค. ๋ํ ์ด ์์๋ ํ ์ ํ์ฉํ์ฌ ๋ณต์กํ ์ํ์ ๋ก์ง์ ๊ตฌ์กฐ์ ์ด๊ณ ๋ชจ๋ํ๋ ๋ฐฉ์์ผ๋ก ๊ด๋ฆฌํ๋ ๋ฐฉ๋ฒ์ ๋ณด์ฌ์ค์ผ๋ก์จ UI ์ปดํฌ๋ํธ๋ฅผ ๋์ฑ ๊ฐ์ ํ๊ณ ๊ธฐ๋ฅ์ ์ถ๊ฐํ ์ ์๋ ๊ธฐ๋ฐ์ ๋ง๋ จํ์ต๋๋ค.
์ด ๋์์ธ์ ์ฅ์ ์ ๋ก์ง๊ณผ ํ๋ ์ ํ ์ด์ ์ด ๋ช ํํ๊ฒ ๋ถ๋ฆฌ๋์ด ์๋ค๋ ์ ์ ๋๋ค. ์ฌ๊ธฐ์ โ๋ก์งโ์ด๋ ์ ํ ์ปดํฌ๋ํธ์ ํต์ฌ ๊ธฐ๋ฅ์ธ ์ด๊ธฐ/๋ซ๊ธฐ ์ํ, ์ ํ๋ ํญ๋ชฉ, ๊ฐ์กฐ ํ์๋ ์์, ๋ชฉ๋ก์์ ์ ํํ ๋ ํ์ดํ ์๋๋ก ๋๋ฅด๋ ๋ฑ์ ์ฌ์ฉ์ ์ ๋ ฅ์ ๋ํ ๋ฐ์ ๋ฑ์ ์๋ฏธํฉ๋๋ค. ์ด๋ฌํ ๋ถ๋ฆฌ๋ฅผ ํตํด ์ปดํฌ๋ํธ๋ ํน์ ์๊ฐ์ ํํ์ ์ฝ๋งค์ด์ง ์๊ณ ํต์ฌ ๋์์ ์ ์งํ๋ฏ๋ก โํค๋๋ฆฌ์ค ์ปดํฌ๋ํธโ๋ผ๋ ์ฉ์ด๋ฅผ ์ ๋นํํ ์ ์์ต๋๋ค.
์ปดํฌ๋ํธ์ ๋ก์ง์ด ์ค์ ์ง์คํ๋์ด ์์ด ๋ค์ํ ์๋๋ฆฌ์ค์์ ์ฌ์ฌ์ฉํ ์ ์๊ธฐ ๋๋ฌธ์ ์ด ๊ธฐ๋ฅ์ด ์์ ์ ์ผ๋ก ์๋ํ๋ ๊ฒ์ด ์ค์ํฉ๋๋ค. ๋ฐ๋ผ์ ํฌ๊ด์ ์ธ ํ ์คํธ๋ ํ์์ ๋๋ค. ์ข์ ์์์ ์ด๋ฌํ ๋์์ ํ ์คํธํ๋ ๊ฒ์ด ๊ฐ๋จํ๋ค๋ ๊ฒ์ ๋๋ค.
ํผ๋ธ๋ฆญ ๋ฉ์๋๋ฅผ ํธ์ถํ๊ณ ํด๋น ์ํ ๋ณํ๋ฅผ ๊ด์ฐฐํ์ฌ ์ํ ๊ด๋ฆฌ๋ฅผ ๊ฒ์ฆํ ์ ์์ต๋๋ค. ์๋ฅผ ๋ค์ด toggleDropdown
๊ณผ isOpen
์ํ ์ฌ์ด์ ๊ด๊ณ๋ฅผ ์ดํด๋ณผ ์ ์์ต๋๋ค.
const items = [{ text: 'Apple' }, { text: 'Orange' }, { text: 'Banana' }];
it('๋๋กญ๋ค์ด ์ด๊ธฐ/๋ซ๊ธฐ ์ํ๊ฐ ํธ๋ค๋ง๋๋ค.', () => {
const { result } = renderHook(() => useDropdown(items));
expect(result.current.isOpen).toBe(false);
act(() => {
result.current.toggleDropdown();
});
expect(result.current.isOpen).toBe(true);
act(() => {
result.current.toggleDropdown();
});
expect(result.current.isOpen).toBe(false);
});
ํค๋ณด๋ ํ์ ํ ์คํธ๋ ์ฃผ๋ก ์๊ฐ์ ์ธํฐํ์ด์ค๊ฐ ์๊ธฐ ๋๋ฌธ์ ์ฝ๊ฐ ๋ ๋ณต์กํฉ๋๋ค. ๋ฐ๋ผ์ ๋ณด๋ค ํตํฉ์ ์ธ ํ ์คํธ ์ ๊ทผ ๋ฐฉ์์ด ํ์ํฉ๋๋ค. ํ ๊ฐ์ง ํจ๊ณผ์ ์ธ ๋ฐฉ๋ฒ์ ๋์์ ์ธ์ฆํ๊ธฐ ์ํด ๊ฐ์ง ํ ์คํธ ์ปดํฌ๋ํธ๋ฅผ ๋ง๋๋ ๊ฒ์ ๋๋ค. ์ด๋ฌํ ํ ์คํธ๋ ํค๋๋ฆฌ์ค ์ปดํฌ๋ํธ ํ์ฉ์ ๋ํ ์ง์นจ ๊ฐ์ด๋๋ฅผ ์ ๊ณตํ๊ณ JSX๋ฅผ ์ฌ์ฉํ๊ธฐ ๋๋ฌธ์ ์ฌ์ฉ์ ์ํธ ์์ฉ์ ๋ํ ์ง์ ํ ์ธ์ฌ์ดํธ๋ฅผ ์ ๊ณตํ๋ ๋ ๊ฐ์ง ์ฉ๋๋ก ์ฌ์ฉ๋ฉ๋๋ค.
์ด์ ์ํ ๊ฒ์ฌ๋ฅผ ํตํฉ ํ ์คํธ๋ก ๋์ฒดํ๋ ๋ค์ ํ ์คํธ๋ฅผ ๊ณ ๋ คํด ๋ณด์ธ์.
it('ํ ๊ธ์ ์คํ์ํจ๋ค', async () => {
render(<SimpleDropdown />);
const trigger = screen.getByRole('button');
expect(trigger).toBeInTheDocument();
await userEvent.click(trigger);
const list = screen.getByRole('listbox');
expect(list).toBeInTheDocument();
await userEvent.click(trigger);
expect(list).not.toBeInTheDocument();
});
์๋์ SimpleDropdown
์ ํ
์คํธ ์ ์ฉ์ผ๋ก ์ค๊ณ๋ ๊ฐ์ง 1 ์ปดํฌ๋ํธ์
๋๋ค. ํค๋๋ฆฌ์ค ์ปดํฌ๋ํธ๋ฅผ ๊ตฌํํ๋ ค๋ ์ฌ์ฉ์๋ฅผ ์ํ ์ค์ต ์์ ๋ก๋ ์ฌ์ฉํ ์ ์์ต๋๋ค.
const SimpleDropdown = () => {
const {
isOpen,
toggleDropdown,
selectedIndex,
selectedItem,
updateSelectedItem,
getAriaAttributes,
dropdownRef,
} = useDropdown(items);
return (
<div tabIndex={0} ref={dropdownRef} {...getAriaAttributes()}>
<button onClick={toggleDropdown}>Select</button>
<p data-testid="selected-item">{selectedItem?.text}</p>
{isOpen && (
<ul role="listbox">
{items.map((item, index) => (
<li
key={index}
role="option"
aria-selected={index === selectedIndex}
onClick={() => updateSelectedItem(item)}
>
{item.text}
</li>
))}
</ul>
)}
</div>
);
};
SimpleDropdown
์ ํ
์คํธ์ฉ์ผ๋ก ์ ์๋ ๋๋ฏธ ์ปดํฌ๋ํธ์
๋๋ค. useDropdown
์ ์ค์ ์ง์ค์ ๋ก์ง์ ์ฌ์ฉํ์ฌ ๋๋กญ๋ค์ด ๋ชฉ๋ก์ ์์ฑํฉ๋๋ค. โSelectโ ๋ฒํผ์ ํด๋ฆญํ๋ฉด ๋ชฉ๋ก์ด ๋ํ๋๊ฑฐ๋ ์ฌ๋ผ์ง๋๋ค. ์ด ๋ชฉ๋ก์๋ ์ผ๋ จ์ ํญ๋ชฉ(Apple, Orange, Banana)์ด ํฌํจ๋์ด ์์ผ๋ฉฐ, ์ฌ์ฉ์๋ ํด๋น ํญ๋ชฉ์ ํด๋ฆญํ์ฌ ์ํ๋ ํญ๋ชฉ์ ์ ํํ ์ ์์ต๋๋ค. ์์ ํ
์คํธ๋ ์ด ๋์์ด ์๋ํ ๋๋ก ์๋ํ๋์ง ํ์ธํฉ๋๋ค.
SimpleDropdown
์ปดํฌ๋ํธ๋ฅผ ์ฌ์ฉํ๋ฉด ๋ณด๋ค ๋ณต์กํ๋ฉด์๋ ํ์ค์ ์ธ ์๋๋ฆฌ์ค๋ฅผ ํ
์คํธํ ์ ์์ต๋๋ค.
it('ํค๋ณด๋ ํ์์ ํตํด ํญ๋ชฉ์ ์ ํํ๋ค.', async () => {
render(<SimpleDropdown />);
const trigger = screen.getByRole('button');
expect(trigger).toBeInTheDocument();
await userEvent.click(trigger);
const dropdown = screen.getByRole('combobox');
dropdown.focus();
await userEvent.type(dropdown, '{arrowdown}');
await userEvent.type(dropdown, '{enter}');
await expect(screen.getByTestId('selected-item')).toHaveTextContent(
items[0].text
);
});
์ด ํ
์คํธ๋ ์ฌ์ฉ์๊ฐ ํค๋ณด๋ ์
๋ ฅ์ ์ฌ์ฉํ์ฌ ๋๋กญ๋ค์ด์์ ํญ๋ชฉ์ ์ ํํ ์ ์๋์ง ํ์ธํฉ๋๋ค. SimpleDropdown
์ ๋ ๋๋งํ๊ณ ํธ๋ฆฌ๊ฑฐ ๋ฒํผ์ ํด๋ฆญํ๋ฉด ๋๋กญ๋ค์ด์ ์ด์ ์ด ๋ง์ถฐ์ง๋๋ค. ๊ทธ ํ ํ
์คํธ๋ ํค๋ณด๋ ํ์ดํ ํค๋ฅผ ๋๋ฌ ์ฒซ ๋ฒ์งธ ํญ๋ชฉ์ผ๋ก ์ด๋ํ๊ณ ์ํฐ ํค๋ฅผ ๋๋ฌ ํญ๋ชฉ์ ์ ํํ๋ ๊ฒ์ ์๋ฎฌ๋ ์ด์
ํฉ๋๋ค. ๊ทธ๋ฐ ๋ค์ ํ
์คํธ๋ ์ ํํ ํญ๋ชฉ์ ์์ ํ
์คํธ๊ฐ ํ์๋๋์ง ํ์ธํฉ๋๋ค.
ํค๋๋ฆฌ์ค ์ปดํฌ๋ํธ์ ์ปค์คํ ํ ์ ํ์ฉํ๋ ๊ฒ์ด ์ผ๋ฐ์ ์ด์ง๋ง, ์ด๊ฒ์ด ์ ์ผํ ์ ๊ทผ ๋ฐฉ์์ ์๋๋๋ค. ์ฌ์ค ํ ์ด ๋ฑ์ฅํ๊ธฐ ์ ์๋ ๊ฐ๋ฐ์๋ค์ด ๋ ๋ ํ๋กํผํฐ๋ HOC(Higher-Order Component)๋ฅผ ์ฌ์ฉํ์ฌ ํค๋๋ฆฌ์ค ์ปดํฌ๋ํธ๋ฅผ ๊ตฌํํ์ต๋๋ค. ์ค๋๋ ์๋ HOC๊ฐ ์์ ๋งํผ์ ์ธ๊ธฐ๋ฅผ ์ป์ง๋ ๋ชปํ์ง๋ง, ๋ฆฌ์กํธ ์ปจํ ์คํธ๋ฅผ ์ฌ์ฉํ๋ ์ ์ธ์ API๋ ์ฌ์ ํ ์๋นํ ์ ํธ๋๊ณ ์์ต๋๋ค.
์ด ์์์์๋ ๋ฆฌ์กํธ ์ปจํ ์คํธ API๋ฅผ ์ฌ์ฉํด ๋น์ทํ ๊ฒฐ๊ณผ๋ฅผ ์ป์ ์ ์๋ ๋ค๋ฅธ ์ ์ธ์ ๋ฐฉ๋ฒ์ ๋ณด์ฌ๋๋ฆฌ๊ฒ ์ต๋๋ค. ์ปดํฌ๋ํธ ํธ๋ฆฌ ๋ด์ ๊ณ์ธต ๊ตฌ์กฐ๋ฅผ ์ค์ ํ๊ณ ๊ฐ ์ปดํฌ๋ํธ๋ฅผ ๊ต์ฒดํ ์ ์๋๋ก ํจ์ผ๋ก์จ ํค๋ณด๋ ํ์, ์ ๊ทผ์ฑ ๋ฑ์ ์ง์ํ๋ ๋ฑ ํจ๊ณผ์ ์ผ๋ก ๋์ํ ๋ฟ๋ง ์๋๋ผ, ์ฌ์ฉ์๊ฐ ์์ ๋ง์ ์ปดํฌ๋ํธ๋ฅผ ์ปค์คํฐ๋ง์ด์งํ ์ ์๋ ์ ์ฐ์ฑ์ ์ ๊ณตํ๋ ๊ฐ์น ์๋ ์ธํฐํ์ด์ค๋ฅผ ์ ๊ณตํ ์ ์์ต๋๋ค.
import { HeadlessDropdown as Dropdown } from './HeadlessDropdown';
const HeadlessDropdownUsage = ({ items }: { items: Item[] }) => {
return (
<Dropdown items={items}>
<Dropdown.Trigger as={Trigger}>Select an option</Dropdown.Trigger>
<Dropdown.List as={CustomList}>
{items.map((item, index) => (
<Dropdown.Option
index={index}
key={index}
item={item}
as={CustomListItem}
/>
))}
</Dropdown.List>
</Dropdown>
);
};
HeadlessDropdownUsage
์ปดํฌ๋ํธ๋ Item
๋ฐฐ์ด ํ์
์ items
ํ๋กํผํฐ๋ฅผ ๋ฐ์ Dropdown
์ปดํฌ๋ํธ๋ฅผ ๋ฐํํฉ๋๋ค. Dropdown
๋ด๋ถ์๋ CustomTrigger
์ปดํฌ๋ํธ๋ฅผ ๋ ๋๋งํ๋ Dropdown.Trigger
์ CustomList
์ปดํฌ๋ํธ๋ฅผ ๋ ๋๋งํ๋ Dropdown.List
๋ฅผ ์ ์ํ๊ณ , items
๋ฐฐ์ด์ ๋งคํํ์ฌ ๊ฐ ํญ๋ชฉ์ ๋ํ Dropdown.Option
์ ์์ฑํ๊ณ , CustomListItem
์ปดํฌ๋ํธ๋ฅผ ๋ ๋๋งํฉ๋๋ค.
์ด ๊ตฌ์กฐ๋ฅผ ์ฌ์ฉํ๋ฉด ์ปดํฌ๋ํธ ๊ฐ์ ๋ช
ํํ ๊ณ์ธต์ ๊ด๊ณ๋ฅผ ์ ์งํ๋ฉด์ ๋๋กญ๋ค์ด ๋ฉ๋ด์ ๋ ๋๋ง๊ณผ ๋์์ ์ ์ฐํ๊ณ ์ ์ธ์ ์ธ ๋ฐฉ์์ผ๋ก ์ฌ์ฉ์ ์ ์ํ ์ ์์ต๋๋ค. Dropdown.Trigger
, Dropdown.List
, Dropdown.Option
์ปดํฌ๋ํธ๋ ์คํ์ผ์ด ์ง์ ๋์ง ์์ ๊ธฐ๋ณธ HTML ์์(๊ฐ๊ฐ button, ul, li)๋ฅผ ์ ๊ณตํ๋ค๋ ์ ์ ์ ์ํ์ธ์. ์ด ์ปดํฌ๋ํธ๋ค์ ๊ฐ๊ฐ ํ๋กํผํฐ๋ฅผ ํ์ฉํ์ฌ ์ฌ์ฉ์๊ฐ ์์ ๋ง์ ์คํ์ผ๊ณผ ๋์์ผ๋ก ์ปดํฌ๋ํธ๋ฅผ ์ปค์คํฐ๋ง์ด์งํ ์ ์๋๋ก ํฉ๋๋ค.
์๋ฅผ ๋ค์ด ์ด๋ฌํ ์ฌ์ฉ์ ์ ์ ์ปดํฌ๋ํธ๋ฅผ ์ ์ํ๊ณ ์์ ๊ฐ์ด ์ฌ์ฉํ ์ ์์ต๋๋ค.
const CustomTrigger = ({ onClick, ...props }) => (
<button className="trigger" onClick={onClick} {...props} />
);
const CustomList = ({ ...props }) => (
<div {...props} className="dropdown-menu" />
);
const CustomListItem = ({ ...props }) => (
<div {...props} className="item-container" />
);
์ฌ์ง 4: ์ปค์คํฐ๋ง์ด์ฆ๋ ์์๋ฅผ ํตํ ์ ์ธ์ ์ธ UI
๊ตฌํ์ ๋ณต์กํ์ง ์์ต๋๋ค. Dropdown
(๋ฃจํธ ์์)์ ์ปจํ
์คํธ๋ฅผ ์ ์ํ๊ณ ๊ด๋ฆฌํด์ผ ํ๋ ๋ชจ๋ ์ํ๋ฅผ ๊ทธ ์์ ๋ฃ๊ณ , ์์ ๋
ธ๋์์ ํด๋น ์ปจํ
์คํธ๋ฅผ ์ฌ์ฉํ์ฌ ์ํ์ ์ก์ธ์คํ๊ฑฐ๋ ์ปจํ
์คํธ์ API๋ฅผ ํตํด ์ํ๋ฅผ ๋ณ๊ฒฝํ๋ฉด ๋ฉ๋๋ค.
type DropdownContextType<T> = {
isOpen: boolean;
toggleDropdown: () => void;
selectedIndex: number;
selectedItem: T | null;
updateSelectedItem: (item: T) => void;
getAriaAttributes: () => any;
dropdownRef: RefObject<HTMLElement>;
};
function createDropdownContext<T>() {
return createContext<DropdownContextType<T> | null>(null);
}
const DropdownContext = createDropdownContext();
export const useDropdownContext = () => {
const context = useContext(DropdownContext);
if (!context) {
throw new Error('์ปดํฌ๋ํธ๋ <Dropdown/> ๋ด์์ ์ฌ์ฉํด์ผ ํฉ๋๋ค.');
}
return context;
};
์ด ์ฝ๋๋ ์ผ๋ฐ DropdownContextType
ํ์
๊ณผ ์ด ํ์
์ผ๋ก ์ปจํ
์คํธ๋ฅผ ์์ฑํ๋ createDropdownContext
ํจ์๋ฅผ ์ ์ํฉ๋๋ค. ์ด ํจ์๋ฅผ ์ฌ์ฉํ์ฌ DropdownContext
๊ฐ ์์ฑ๋ฉ๋๋ค. useDropdownContext
๋ ์ด ์ปจํ
์คํธ์ ์ ๊ทผํ๋ ์ปค์คํ
ํ
์ผ๋ก, <Dropdown/>
์ปดํฌ๋ํธ ์ธ๋ถ์์ ์ฌ์ฉ๋ ๊ฒฝ์ฐ ์ค๋ฅ๋ฅผ ๋ฐ์์์ผ ์ํ๋ ์ปดํฌ๋ํธ ๊ณ์ธต๊ตฌ์กฐ ๋ด์์ ์ ์ ํ๊ฒ ์ฌ์ฉ๋๋๋ก ํฉ๋๋ค.
๊ทธ๋ฐ ๋ค์ ์ปจํ ์คํธ๋ฅผ ์ฌ์ฉํ๋ ์ปดํฌ๋ํธ๋ฅผ ์ ์ํ ์ ์์ต๋๋ค. ์ปจํ ์คํธ ํ๋ก๋ฐ์ด๋๋ถํฐ ์์ํ ์ ์์ต๋๋ค.
const HeadlessDropdown = <T extends { text: string }>({
children,
items,
}: {
children: React.ReactNode;
items: T[];
}) => {
const {
//... ํ
์ ๋ชจ๋ ์ํ์ ์ํ ์ค์ ํจ์
} = useDropdown(items);
return (
<DropdownContext.Provider
value={{
isOpen,
toggleDropdown,
selectedIndex,
selectedItem,
updateSelectedItem,
}}
>
<div
ref={dropdownRef as RefObject<HTMLDivElement>}
{...getAriaAttributes()}
>
{children}
</div>
</DropdownContext.Provider>
);
};
HeadlessDropdown
์ปดํฌ๋ํธ๋ children
๊ณผ items
๋ผ๋ ๋ ๊ฐ์ง ํ๋กํผํฐ๋ฅผ ์ทจํ๊ณ , ์ปค์คํ
ํ
useDropdown
์ ์ฌ์ฉํด ์ํ์ ๋์์ ๊ด๋ฆฌํฉ๋๋ค. ์ด ์ปดํฌ๋ํธ๋ ํ์ ์ปดํฌ๋ํธ์ ์ํ ๋ฐ ๋์์ ๊ณต์ ํ๊ธฐ ์ํด DropdownContext.Provider
๋ฅผ ํตํด ์ปจํ
์คํธ๋ฅผ ์ ๊ณตํฉ๋๋ค. div
๋ด์์ ์ฐธ์กฐ๋ฅผ ์ค์ ํ๊ณ ์ ๊ทผ์ฑ์ ์ํด ARIA ์์ฑ์ ์ ์ฉํ ๋ค์ children
์ ๋ ๋๋งํ์ฌ ์ค์ฒฉ๋ ์ปดํฌ๋ํธ๋ฅผ ํ์ํจ์ผ๋ก์จ ๊ตฌ์กฐํ๋๊ณ ์ฌ์ฉ์ ์ ์ ๊ฐ๋ฅํ ๋๋กญ๋ค์ด ๊ธฐ๋ฅ์ ๊ตฌํํฉ๋๋ค.
์ด์ ์น์
์์ ์ ์ํ useDropdown
ํ
์ ์ฌ์ฉํ ๋ค์ ์ด ๊ฐ์ HeadlessDropdown
์ ์์์๊ฒ ์ ๋ฌํ๋ ๋ฐฉ๋ฒ์ ์ฃผ๋ชฉํ์ธ์. ๊ทธ๋ฐ ๋ค์ ์์ ์ปดํฌ๋ํธ๋ฅผ ์ ์ํ ์ ์์ต๋๋ค.
HeadlessDropdown.Trigger = function Trigger({
as: Component = 'button',
...props
}) {
const { toggleDropdown } = useDropdownContext();
return <Component tabIndex={0} onClick={toggleDropdown} {...props} />;
};
HeadlessDropdown.List = function List({ as: Component = 'ul', ...props }) {
const { isOpen } = useDropdownContext();
return isOpen ? <Component {...props} role="listbox" tabIndex={0} /> : null;
};
HeadlessDropdown.Option = function Option({
as: Component = 'li',
index,
item,
...props
}) {
const { updateSelectedItem, selectedIndex } = useDropdownContext();
return (
<Component
role="option"
aria-selected={index === selectedIndex}
key={index}
onClick={() => updateSelectedItem(item)}
{...props}
>
{item.text}
</Component>
);
};
์ถ๊ฐ ํ๋กํผํฐ์ ํจ๊ป ์ปดํฌ๋ํธ ๋๋ HTML ํ๊ทธ๋ฅผ ์ฒ๋ฆฌํ๊ธฐ ์ํด GenericComponentType
ํ์
์ ์ ์ํ์ต๋๋ค. ๋๋กญ๋ค์ด ๋ฉ๋ด์ ๊ฐ ๋ถ๋ถ์ ๋ ๋๋งํ๋ ์ธ ๊ฐ์ง ํจ์ HeadlessDropdown.Trigger
, HeadlessDropdown.List
, HeadlessDropdown.Option
์ด ์ ์๋์ด ์์ต๋๋ค. ๊ฐ ํจ์๋ ์ปดํฌ๋ํธ์ ์ปค์คํ
๋ ๋๋ง์ ํ์ฉํ๊ธฐ ์ํด as
ํ๋กํผํฐ๋ฅผ ํ์ฉํ๊ณ ๋ ๋๋ง๋ ์ปดํฌ๋ํธ์ ์ถ๊ฐ ํ๋กํผํฐ๋ฅผ ํผ์นฉ๋๋ค. ์ด๋ค ํจ์๋ ๋ชจ๋ useDropdownContext
๋ฅผ ํตํด ๊ณต์ ๋ ์ํ ๋ฐ ๋์์ ์ ๊ทผํฉ๋๋ค.
HeadlessDropdown.Trigger
๋ ๊ธฐ๋ณธ์ ์ผ๋ก ๋๋กญ๋ค์ด ๋ฉ๋ด๋ฅผ ํ ๊ธํ๋ ๋ฒํผ์ ๋ ๋๋งํฉ๋๋ค.HeadlessDropdown.List
๋ ๋๋กญ๋ค์ด์ด ์ด๋ ค ์์ผ๋ฉด ๋ชฉ๋ก ์ปจํ
์ด๋๋ฅผ ๋ ๋๋งํฉ๋๋ค.HeadlessDropdown.Option
์ ๊ฐ๋ณ ํญ๋ชฉ์ ๋ชฉ๋ก์ ๋ ๋๋งํ๊ณ ํด๋ฆญ ์ ์ ํํ ํญ๋ชฉ์ ์
๋ฐ์ดํธํฉ๋๋ค.์ด๋ฌํ ๊ธฐ๋ฅ์ ์ข ํฉํ๋ฉด ์ปค์คํ ๊ฐ๋ฅํ๋ฉฐ ์ ๊ทผ ๊ฐ๋ฅํ ๋๋กญ๋ค์ด ๋ฉ๋ด ๊ตฌ์กฐ๋ฅผ ๋ง๋ค ์ ์์ต๋๋ค.
์ด๋ ์ฝ๋๋ฒ ์ด์ค์์ ํค๋๋ฆฌ์ค ์ปดํฌ๋ํธ๋ฅผ ์ด๋ป๊ฒ ํ์ฉํ ์ง์ ๋ํ ์ฌ์ฉ์ ์ ํธ๋์ ๋ฐ๋ผ ํฌ๊ฒ ๋ฌ๋ผ์ง๋๋ค. ํ ์ DOM(๋๋ ๊ฐ์ DOM) ์ํธ ์์ฉ์ ํฌํจํ์ง ์์ ๊ณต์ ์ํ ๋ก์ง๊ณผ UI ์ฌ์ด์ ์ ์ผํ ์ ์ ์ ref ๊ฐ์ฒด๋ผ๋ ์ ์์ ์ ๋ ํ ์ ์ ํธํฉ๋๋ค. ๋ฐ๋ฉด์ ์ปจํ ์คํธ ๊ธฐ๋ฐ ๊ตฌํ์์๋ ์ฌ์ฉ์๊ฐ ์ปค์คํฐ๋ง์ด์งํ์ง ์์ ๋ ๊ธฐ๋ณธ ๊ตฌํ์ด ์ ๊ณต๋ฉ๋๋ค.
๋ค์ ์์ ์์๋ useDropdown
ํ
์ ์ฌ์ฉํ์ฌ ํต์ฌ ๊ธฐ๋ฅ์ ์ ์งํ๋ฉด์, ๋ค๋ฅธ UI๋ก ์ผ๋ง๋ ์ฝ๊ฒ ์ ํํ ์ ์๋์ง ๋ณด์ฌ๋๋ฆฌ๊ฒ ์ต๋๋ค.
๋ฒํผ์ ํธ๋ฆฌ๊ฑฐ ์์๋ก ์ฌ์ฉํ๊ณ ๋๋กญ๋ค์ด ๋ชฉ๋ก์ ํ
์คํธ์ ํจ๊ป ์๋ฐํ๋ฅผ ํ์ํ๋ ์๋ก์ด ๋์์ธ์ด ํ์ํ ์๋๋ฆฌ์ค๋ฅผ ์๊ฐํด ๋ด
์๋ค. useDropdown
ํ
์ ์ด๋ฏธ ๋ก์ง์ด ์บก์ํ๋์ด ์์ผ๋ฏ๋ก, ์๋ก์ด UI์ ์ ์ํ๋ ๊ฒ์ ๊ฐ๋จํฉ๋๋ค.
์๋์ ์๋ก์ด DropdownTailwind
์ปดํฌ๋ํธ์์๋ ์์์ ์คํ์ผ์ ์ง์ ํ๊ธฐ ์ํด Tailwind CSS(Tailwind CSS๋ ์ฌ์ฉ์ ์ ์ ์ฌ์ฉ์ ์ธํฐํ์ด์ค๋ฅผ ๋น ๋ฅด๊ฒ ๊ตฌ์ถํ๊ธฐ ์ํ ์ ํธ๋ฆฌํฐ ์ฐ์ CSS ํ๋ ์์ํฌ์
๋๋ค)๋ฅผ ์ฌ์ฉํ์ต๋๋ค. ๋ฒํผ์ด ํธ๋ฆฌ๊ฑฐ ์์๋ก ์ฌ์ฉ๋๊ณ ๋๋กญ๋ค์ด ๋ชฉ๋ก์ ๊ฐ ํญ๋ชฉ์ ์ด๋ฏธ์ง๊ฐ ํฌํจ๋๋ ๋ฑ ๊ตฌ์กฐ๊ฐ ์ฝ๊ฐ ์์ ๋์์ต๋๋ค. ์ด๋ฌํ UI ๋ณ๊ฒฝ์๋ ๋ถ๊ตฌํ๊ณ useDropdown
ํ
๋๋ถ์ ํต์ฌ ๊ธฐ๋ฅ์ ๊ทธ๋๋ก ์ ์ง๋ฉ๋๋ค.
const DropdownTailwind = ({ items }: DropdownProps) => {
const {
isOpen,
selectedItem,
selectedIndex,
toggleDropdown,
handleKeyDown,
setSelectedItem,
} = useDropdown<Item>(items);
return (
<div
className="relative"
onClick={toggleDropdown}
onKeyDown={handleKeyDown}
>
<button className="btn p-2 border ..." tabIndex={0}>
{selectedItem ? selectedItem.text : 'Select an item...'}
</button>
{isOpen && (
<ul className="dropdown-menu ..." role="listbox">
{items.map((item, index) => (
<li key={index} role="option">
{/* ... JSX์ ๋จ์ ๋ถ๋ถ ... */}
</li>
))}
</ul>
)}
</div>
);
};
์ด ๋ ๋๋ง์์ DropdownTailwind
์ปดํฌ๋ํธ๋ useDropdown
ํ
๊ณผ ์ธํฐํ์ด์คํ์ฌ ์ํ์ ์ํธ์์ฉ์ ๊ด๋ฆฌํฉ๋๋ค. ์ด๋ฌํ ์ค๊ณ ๋๋ถ์ UI๋ฅผ ์์ ํ๊ฑฐ๋, ๊ฐ์ ํ ๋ ๊ธฐ๋ณธ ๋ก์ง์ ๋ค์ ๊ตฌํํ ํ์๊ฐ ์์ด ์๋ก์ด ๋์์ธ ์๊ตฌ ์ฌํญ์ ์ฝ๊ฒ ์ ์ํ ์ ์์ต๋๋ค.
๋ํ React Devtools๋ฅผ ์ฌ์ฉํ๋ฉด ์ฝ๋๋ฅผ ์ข ๋ ์ ์๊ฐํํ ์ ์์ผ๋ฉฐ, hooks
์น์
์ ๋ชจ๋ ์ํ๊ฐ ๋์ด๋์ด ์์ต๋๋ค.
์ฌ์ง 5: ๋ฐ๋ธํด
๋ชจ๋ ๋๋กญ๋ค์ด ๋ชฉ๋ก์ ์ธํ์ ๊ด๊ณ์์ด ๋ด๋ถ์ ์ผ๋ก ์ผ๊ด๋ ๋์์ ๊ณต์ ํ๋ฉฐ, ์ด ๋ชจ๋ ๋์์ useDropdown
ํ
(ํค๋๋ฆฌ์ค ์ปดํฌ๋ํธ)์ ์บก์ํ๋์ด ์์ต๋๋ค. ํ์ง๋ง ์๊ฒฉ ํ๊ฒฝ์์ ๋ฐ์ดํฐ๋ฅผ ๊ฐ์ ธ์์ผ ํ๊ฑฐ๋ ๋น๋๊ธฐ ์ํ์ ๊ฐ์ด ๋ ๋ง์ ์ํ๋ฅผ ๊ด๋ฆฌํด์ผ ํ๋ ๊ฒฝ์ฐ์๋ ์ด๋ป๊ฒ ํด์ผ ํ ๊น์?
๋๋กญ๋ค์ด ์ปดํฌ๋ํธ๋ฅผ ์งํํ๋ฉด์ ์๊ฒฉ ๋ฐ์ดํฐ๋ฅผ ์ฒ๋ฆฌํ ๋ ๋ฐ์ํ๋ ์ข ๋ ๋ณต์กํ ์ํ๋ฅผ ์ดํด๋ณด๊ฒ ์ต๋๋ค. ์๊ฒฉ ์์ค์์ ๋ฐ์ดํฐ๋ฅผ ๊ฐ์ ธ์ค๋ ์๋๋ฆฌ์ค์์๋ ๋ก๋ฉ, ์ค๋ฅ ๋ฐ ๋ฐ์ดํฐ ์ํ๋ฅผ ์ฒ๋ฆฌํด์ผ ํ๋ ๋ฑ ๋ช ๊ฐ์ง ์ํ๋ฅผ ๋ ๊ด๋ฆฌํด์ผ ํ ํ์์ฑ์ด ์๊น๋๋ค.
์ฌ์ง 6: ๋ค๋ฅธ ์ํ
์๊ฒฉ ์๋ฒ์์ ๋ฐ์ดํฐ๋ฅผ ๋ก๋ํ๋ ค๋ฉด loading
, error
, data
์ ์ธ ๊ฐ์ง ์๋ก์ด ์ํ๋ฅผ ์ ์ํด์ผ ํฉ๋๋ค. ์ผ๋ฐ์ ์ผ๋ก useEffect
ํธ์ถ์ ํตํด ์ด๋ฅผ ์ํํ๋ ๋ฐฉ๋ฒ์ ๋ค์๊ณผ ๊ฐ์ต๋๋ค.
//...
const [loading, setLoading] = useState<boolean>(false);
const [data, setData] = useState<Item[] | null>(null);
const [error, setError] = useState<Error | undefined>(undefined);
useEffect(() => {
const fetchData = async () => {
setLoading(true);
try {
const response = await fetch('/api/users');
if (!response.ok) {
const error = await response.json();
throw new Error(`Error: ${error.error || response.status}`);
}
const data = await response.json();
setData(data);
} catch (e) {
setError(e as Error);
} finally {
setLoading(false);
}
};
fetchData();
}, []);
//...
์ด ์ฝ๋๋ loading
, data
, error
์ ์ธ ๊ฐ์ง ์ํ ๋ณ์๋ฅผ ์ด๊ธฐํํฉ๋๋ค. ์ปดํฌ๋ํธ๊ฐ ๋ง์ดํธ๋๋ฉด ๋น๋๊ธฐ ํจ์๋ฅผ ์คํ์์ผ, โ/api/usersโ ์๋ํฌ์ธํธ์์ ๋ฐ์ดํฐ๋ฅผ ๊ฐ์ ธ์ต๋๋ค. ์ด ํจ์๋ loading
์ ํ์นญ ์ ์๋ true
๋ก ์ค์ ํ๊ณ , ์ดํ์๋ false
๋ก ์ค์ ํฉ๋๋ค. ๋ฐ์ดํฐ๊ฐ ์ฑ๊ณต์ ์ผ๋ก ๋ถ๋ฌ์์ง๋ฉด data
์ํ์ ์ ์ฅ๋ฉ๋๋ค. ์ค๋ฅ๊ฐ ๋ฐ์ํ๋ฉด ์ค๋ฅ๊ฐ ์บก์ฒ๋์ด error
์ํ์ ์ ์ฅ๋ฉ๋๋ค.
์ปดํฌ๋ํธ ๋ด์ ์ง์ ํ์นญ ๋ก์ง์ ํตํฉํ๋ ๊ฒ๋ ๊ฐ๋ฅํ์ง๋ง, ๊ฐ์ฅ ์ฐ์ํ๊ฑฐ๋ ์ฌ์ฌ์ฉ ๊ฐ๋ฅํ ์ ๊ทผ ๋ฐฉ์์ ์๋๋๋ค. ์ฌ๊ธฐ์ ํค๋๋ฆฌ์ค ์ปดํฌ๋ํธ์ ์๋ฆฌ๋ฅผ ์ข ๋ ๋ฐ์ ์์ผ ๋ก์ง๊ณผ ์ํ๋ฅผ UI์์ ๋ถ๋ฆฌํ ์ ์์ต๋๋ค. ํ์นญ ๋ก์ง์ ๋ณ๋์ ํจ์๋ก ์ถ์ถํ์ฌ ์ด๋ฅผ ๋ฆฌํฉํฐ๋งํด ๋ณด๊ฒ ์ต๋๋ค.
const fetchUsers = async () => {
const response = await fetch('/api/users');
if (!response.ok) {
const error = await response.json();
throw new Error('๋ฌด์ธ๊ฐ ์๋ชป๋์์ต๋๋ค.');
}
return await response.json();
};
์ด์ fetchUsers
ํจ์๊ฐ ์ค๋น๋์์ผ๋ฏ๋ก ํ ๋จ๊ณ ๋ ๋์๊ฐ ๊ฐ์ ธ์ค๋ ๋ก์ง์ ์ผ๋ฐ ํ
์ผ๋ก ์ถ์ํํ ์ ์์ต๋๋ค. ์ด ํ
์ fetch ํจ์๋ฅผ ๋ฐ์๋ค์ด๊ณ ๊ด๋ จ๋ loading, error ๋ฐ data ์ํ๋ฅผ ๊ด๋ฆฌํฉ๋๋ค.
const useService = <T>(fetch: () => Promise<T>) => {
const [loading, setLoading] = useState<boolean>(false);
const [data, setData] = useState<T | null>(null);
const [error, setError] = useState<Error | undefined>(undefined);
useEffect(() => {
const fetchData = async () => {
setLoading(true);
try {
const data = await fetch();
setData(data);
} catch (e) {
setError(e as Error);
} finally {
setLoading(false);
}
};
fetchData();
}, [fetch]);
return {
loading,
error,
data,
};
};
์ด์ useService
ํ
์ ์ ํ๋ฆฌ์ผ์ด์
์ ์ฒด์์ ๋ฐ์ดํฐ๋ฅผ ๊ฐ์ ธ์ค๋ ๋ฐ ์ฌ์ฌ์ฉ ๊ฐ๋ฅํ ํด๊ฒฐ์ฑ
์ผ๋ก ๋ฑ์ฅํฉ๋๋ค. ์๋ ๊ทธ๋ฆผ๊ณผ ๊ฐ์ด ๋ค์ํ ์ ํ์ ๋ฐ์ดํฐ๋ฅผ ๊ฐ์ ธ์ค๋ ๋ฐ ์ฌ์ฉํ ์ ์๋ ๊น๋ํ ์ถ์ํ์
๋๋ค.
// ์ ํ ๋ฐ์ดํฐ๋ฅผ ๊ฐ์ ธ์ต๋๋ค.
const { loading, error, data } = useService(fetchProducts);
// ๋๋ ๋ค๋ฅธ ํ์
์ ์์์ ๊ฐ์ ธ์ต๋๋ค.
const { loading, error, data } = useService(fetchTickets);
์ด ๋ฆฌํฉํฐ๋ง์ ํตํด ๋ฐ์ดํฐ ํ์นญ ๋ก์ง์ ๋จ์ํํ์ ๋ฟ๋ง ์๋๋ผ ์ ํ๋ฆฌ์ผ์ด์ ์ ๋ค์ํ ์๋๋ฆฌ์ค์์ ์ฌ์ฌ์ฉํ ์ ์๋๋ก ๋ง๋ค์์ต๋๋ค. ์ด๋ฅผ ํตํด ๋๋กญ๋ค์ด ์ปดํฌ๋ํธ๋ฅผ ์ง์์ ์ผ๋ก ๊ฐ์ ํ๊ณ ๊ณ ๊ธ ๊ธฐ๋ฅ๊ณผ ์ต์ ํ์ ๋ํด ๋ ๊น์ด ํ๊ณ ๋ค ์ ์๋ ํํํ ํ ๋๋ฅผ ๋ง๋ จํ์ต๋๋ค.
useService
๋ฐ useDropdown
ํ
์ ์ถ์ํ๋ ๋ก์ง ๋๋ถ์ ์๊ฒฉ ๋ฐ์ดํฐ ํ์นญ์ ํตํฉํด๋ Dropdown
์ปดํฌ๋ํธ๊ฐ ๋ณต์กํด์ง์ง ์์์ต๋๋ค. ์ปดํฌ๋ํธ ์ฝ๋๋ ๊ฐ์ฅ ๋จ์ํ ํํ๋ก ์ ์ง๋์ด ํ์นญ ์ํ๋ฅผ ํจ๊ณผ์ ์ผ๋ก ๊ด๋ฆฌํ๊ณ , ์์ ๋ ๋ฐ์ดํฐ๋ฅผ ๊ธฐ๋ฐ์ผ๋ก ์ฝํ
์ธ ๋ฅผ ๋ ๋๋งํฉ๋๋ค.
const Dropdown = () => {
const { data, loading, error } = useService(fetchUsers);
const {
toggleDropdown,
dropdownRef,
isOpen,
selectedItem,
selectedIndex,
updateSelectedItem,
getAriaAttributes,
} = useDropdown<Item>(data || []);
const renderContent = () => {
if (loading) return <Loading />;
if (error) return <Error />;
if (data) {
return (
<DropdownMenu
items={data}
updateSelectedItem={updateSelectedItem}
selectedIndex={selectedIndex}
/>
);
}
return null;
};
return (
<div
className="dropdown"
ref={dropdownRef as RefObject<HTMLDivElement>}
{...getAriaAttributes()}
>
<Trigger
onClick={toggleDropdown}
text={selectedItem ? selectedItem.text : 'Select an item...'}
/>
{isOpen && renderContent()}
</div>
);
};
์ด ์
๋ฐ์ดํธ๋ Dropdown
์ปดํฌ๋ํธ์์๋ useService
์ ์ฌ์ฉํ์ฌ ๋ฐ์ดํฐ ํ์นญ ์ํ๋ฅผ ๊ด๋ฆฌํ๊ณ , useDropdown
ํ
์ ์ฌ์ฉํ์ฌ ๋๋กญ๋ค์ด ๊ด๋ จ ์ํ์ ์ํธ์์ฉ์ ๊ด๋ฆฌํฉ๋๋ค. renderContent
ํจ์๋ ํ์นญ ์ํ์ ๋ฐ๋ผ ๋ ๋๋ง ๋ก์ง์ ์ฐ์ํ๊ฒ ์ฒ๋ฆฌํ์ฌ ๋ก๋ฉ ์ค์ด๋ , ์ค๋ฅ๊ฐ ๋ฐ์ํ๋ , ๋ฐ์ดํฐ๊ฐ ์๋ ์ฌ๋ฐ๋ฅธ ์ฝํ
์ธ ๊ฐ ํ์๋๋๋ก ํฉ๋๋ค.
์์ ์์์์ ํค๋๋ฆฌ์ค ์ปดํฌ๋ํธ๊ฐ ํํธ ๊ฐ์ ๋์จํ ๊ฒฐํฉ์ ์ด๋ป๊ฒ ์ด์งํ๋์ง ์ดํด๋ณด์ธ์. ์ด๋ฌํ ์ ์ฐ์ฑ ๋๋ถ์ ๋ค์ํ ์กฐํฉ์ ์ํด ๋ถํ์ ๊ต์ฒดํ ์ ์์ต๋๋ค. Loading
๋ฐ Error
์ปดํฌ๋ํธ๋ฅผ ๊ณต์ ํ๋ฉด ๊ธฐ๋ณธ JSX์ ์คํ์ผ๋ง์ด ํฌํจ๋ UserDropdown
์ ์์ฝ๊ฒ ๋ง๋ค๊ฑฐ๋ ๋ค๋ฅธ API ์๋ํฌ์ธํธ์์ ๋ฐ์ดํฐ๋ฅผ ๊ฐ์ ธ์ค๋ TailwindCSS๋ฅผ ์ฌ์ฉํ์ฌ ProductDropdown
์ ์์ฝ๊ฒ ๋ง๋ค ์ ์์ต๋๋ค.
ํค๋๋ฆฌ์ค ์ปดํฌ๋ํธ ํจํด์ JSX ์ฝ๋๋ฅผ ๊ธฐ๋ณธ ๋ก์ง์์ ๊น๋ํ๊ฒ ๋ถ๋ฆฌํ ์ ์๋ ๊ฐ๋ ฅํ ๋ฐฉ๋ฒ์ ์ ์ํฉ๋๋ค. JSX๋ก ์ ์ธ์ UI๋ฅผ ๊ตฌ์ฑํ๋ ๊ฒ์ ์์ฐ์ค๋ฝ๊ฒ ์ด๋ฃจ์ด์ง์ง๋ง, ์ค์ ๋ฌธ์ ๋ ์ํ ๊ด๋ฆฌ์์ ๋ฐ์ํฉ๋๋ค. ๋ฐ๋ก ์ด ์ง์ ์์ ํค๋๋ฆฌ์ค ์ปดํฌ๋ํธ๊ฐ ๋ชจ๋ ์ํ ๊ด๋ฆฌ์ ๋ณต์ก์ฑ์ ํด๊ฒฐํ์ฌ ์ถ์ํ์ ์๋ก์ด ์งํ์ ์ด์์ต๋๋ค.
๋ณธ์ง์ ์ผ๋ก ํค๋๋ฆฌ์ค ์ปดํฌ๋ํธ๋ ๋ก์ง์ ์บก์ํํ์ง๋ง ๊ทธ ์์ฒด๋ก๋ ๋ ๋๋งํ์ง ์๋ ํจ์ ๋๋ ๊ฐ์ฒด์ ๋๋ค. ๋ ๋๋ง ๋ถ๋ถ์ ์๋น์์๊ฒ ๋งก๊ธฐ๋ฏ๋ก UI ๋ ๋๋ง ๋ฐฉ์์ ๋์ ์์ค์ ์ ์ฐ์ฑ์ ์ ๊ณตํฉ๋๋ค. ์ด ํจํด์ ์ฌ๋ฌ ์๊ฐ์ ํํ์์ ์ฌ์ฌ์ฉํ๋ ค๋ ๋ณต์กํ ๋ก์ง์ด ์์ ๋ ๋งค์ฐ ์ ์ฉํ ์ ์์ต๋๋ค.
function useDropdownLogic() {
// ... ๋ชจ๋ ๋๋กญ๋ค์ด ๋ก์ง
return {
// ... ๋
ธ์ถ๋ ๋ก์ง
};
}
function MyDropdown() {
const dropdownLogic = useDropdownLogic();
return (
// ... dropdownLogic์ ๋ก์ง์ ์ฌ์ฉํ์ฌ UI๋ฅผ ๋ ๋๋งํฉ๋๋ค.
);
}
ํค๋๋ฆฌ์ค ์ปดํฌ๋ํธ๋ ์ฌ๋ฌ ์ปดํฌ๋ํธ์์ ๊ณต์ ํ ์ ์๋ ๋ก์ง์ ์บก์ํํ์ฌ ์ฌ์ฌ์ฉ์ฑ์ ํฅ์์ํค๊ณ , DRY(๋ฐ๋ณตํ์ง ์๊ธฐ) ์์น์ ์ค์ํ๋ ๋ฑ ์ฌ๋ฌ ๊ฐ์ง ์ด์ ์ ์ ๊ณตํฉ๋๋ค. ์ ์ง ๊ด๋ฆฌ ๊ฐ๋ฅํ ์ฝ๋๋ฅผ ์์ฑํ๊ธฐ ์ํ ๊ธฐ๋ณธ์ ์ธ ์ค์ฒ ๋ฐฉ๋ฒ์ธ, ๋ก์ง๊ณผ ๋ ๋๋ง์ ๋ช ํํ๊ฒ ๊ตฌ๋ถํ์ฌ ๋ฌธ์ ๋ฅผ ๋ช ํํ๊ฒ ๋ถ๋ฆฌํ๋ ๊ฒ์ ๊ฐ์กฐํฉ๋๋ค. ๋ํ ๊ฐ๋ฐ์๊ฐ ๋์ผํ ํต์ฌ ๋ก์ง์ ์ฌ์ฉํ์ฌ ๋ค์ํ UI ๊ตฌํ์ ์ฑํํ ์ ์๊ฒ ํจ์ผ๋ก์จ ์ ์ฐ์ฑ์ ์ ๊ณตํ๋ฉฐ, ์ด๋ ๋ค์ํ ๋์์ธ ์๊ตฌ ์ฌํญ์ ์ฒ๋ฆฌํ๊ฑฐ๋ ๋ค์ํ ํ๋ ์์ํฌ๋ก ์์ ํ ๋ ํนํ ์ ์ฉํฉ๋๋ค.
ํ์ง๋ง ๋ถ๋ณ๋ ฅ์ ๊ฐ์ง๊ณ ์ ๊ทผํ๋ ๊ฒ์ด ์ค์ํฉ๋๋ค. ๋ค๋ฅธ ๋์์ธ ํจํด๊ณผ ๋ง์ฐฌ๊ฐ์ง๋ก ๋์ ๊ณผ์ ๊ฐ ์์ต๋๋ค. ์ต์ํ์ง ์์ ์ฌ์ฉ์์๊ฒ๋ ์ด๊ธฐ ํ์ต ๊ณก์ ์ด ์์ด ์ผ์์ ์ผ๋ก ๊ฐ๋ฐ ์๋๊ฐ ๋๋ ค์ง ์ ์์ต๋๋ค. ๋ํ ํค๋๋ฆฌ์ค ์ปดํฌ๋ํธ๋ฅผ ์ ์คํ๊ฒ ์ ์ฉํ์ง ์์ผ๋ฉด ํค๋๋ฆฌ์ค ์ปดํฌ๋ํธ๊ฐ ๋์ ํ ์ถ์ํ๊ฐ ์ด๋ ์ ๋์ ๋ฐฉํฅ์ฑ์ ์ถ๊ฐํ์ฌ ์ฝ๋์ ๊ฐ๋ ์ฑ์ ๋ณต์กํ๊ฒ ๋ง๋ค ์ ์์ต๋๋ค.
์ด ํจํด์ ๋ค๋ฅธ ํ๋ฐํธ์๋ ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ ํ๋ ์์ํฌ์๋ ์ ์ฉ๋ ์ ์๋ค๋ ์ ์ ์ฃผ๋ชฉํ๊ณ ์ถ์ต๋๋ค. ์๋ฅผ ๋ค์ด Vue์์๋ ์ด ๊ฐ๋
์ renderless
์ปดํฌ๋ํธ๋ผ๊ณ ๋ถ๋ฆ
๋๋ค. ์ด๋ ๋์ผํ ์๋ฆฌ๋ฅผ ๊ตฌํํ์ฌ ๊ฐ๋ฐ์๊ฐ ๋ก์ง๊ณผ ์ํ ๊ด๋ฆฌ๋ฅผ ๋ณ๋์ ์ปดํฌ๋ํธ๋ก ๋ถ๋ฆฌํ์ฌ ์ฌ์ฉ์๊ฐ ์ด๋ฅผ ์ค์ฌ์ผ๋ก UI๋ฅผ ํฉ์ฑํ ์ ์๋๋ก ํฉ๋๋ค.
์ต๊ทค๋ฌ ๋๋ ๋ค๋ฅธ ํ๋ ์์ํฌ์์์ ๊ตฌํ์ด๋ ํธํ์ฑ์ ๋ํด์๋ ํ์คํ์ง ์์ง๋ง, ํน์ ์ํฉ์์ ์ ์ฌ์ ์ธ ์ด์ ์ ๊ณ ๋ คํ๋ ๊ฒ์ด ์ข์ต๋๋ค.
์ ๊ณ์ ์ค๋ ์ข ์ฌํ๊ฑฐ๋ ๋ฐ์คํฌํฑ ์ค์ ์์ GUI ์ ํ๋ฆฌ์ผ์ด์ ์ ์ฌ์ฉํด ๋ณธ ๊ฒฝํ์ด ์๋ค๋ฉด ํค๋๋ฆฌ์ค ์ปดํฌ๋ํธ ํจํด์ ์ต์ํ ๊ฒ์ ๋๋ค. ์๋ง๋ ๋ค๋ฅธ ์ด๋ฆ์ผ๋ก ๋ถ๋ฆฌ๊ธฐ๋ ํ์ง๋ง MVVM์ View-Modal, ํ๋ ์ ํ ์ด์ ๋ชจ๋ธ ๋๋ ๋ ธ์ถ ์ ๋์ ๋ฐ๋ผ ๋ค๋ฅธ ์ฉ์ด๋ก๋ ๋ถ๋ฆด ์ ์์ต๋๋ค. ๋งํด ํ์ธ๋ฌ๋ ๋ช ๋ ์ ํฌ๊ด์ ์ธ ์ํฐํด์ ํตํด ์ด๋ฌํ ์ฉ์ด์ ๋ํ ์ฌ์ธต์ ์ธ ๋ถ์์ ์ ๊ณตํ๋๋ฐ, ์ด ์ํฐํด์์๋ MVC, Model-View-Presenter ๋ฑ GUI ์ธ๊ณ์์ ๋๋ฆฌ ์ฌ์ฉ๋๋ ๋ง์ ์ฉ์ด๋ฅผ ๋ช ํํ ์ ์ํ์ต๋๋ค.
ํ๋ ์ ํ ์ด์ ๋ชจ๋ธ์ ๋ทฐ์ ์ํ ๋ฐ ๋์์ ํ๋ ์ ํ ์ด์ ๊ณ์ธต ๋ด์ ๋ชจ๋ธ ํด๋์ค๋ก ์ถ์ํํฉ๋๋ค. ์ด ๋ชจ๋ธ์ ๋๋ฉ์ธ ๊ณ์ธต๊ณผ ์กฐ์ ํ๊ณ ๋ทฐ์ ๋ํ ์ธํฐํ์ด์ค๋ฅผ ์ ๊ณตํ์ฌ ๋ทฐ์์ ์์ฌ ๊ฒฐ์ ์ ์ต์ํํฉ๋๋คโฆ
โ Martin Fowler
๊ทธ๋ผ์๋ ๋ถ๊ตฌํ๊ณ ์ ๋ ์ด ํ๋ฆฝ๋ ํจํด์ ์กฐ๊ธ ๋ ํ์ฅํ์ฌ ๋ฆฌ์กํธ ๋๋ ํ๋ฐํธ์๋ ์ธ๊ณ์์ ์ด๋ป๊ฒ ์๋ํ๋์ง ์ดํด๋ณผ ํ์๊ฐ ์๋ค๊ณ ์๊ฐํฉ๋๋ค. ๊ธฐ์ ์ด ๋ฐ์ ํจ์ ๋ฐ๋ผ ๊ธฐ์กด GUI ์ ํ๋ฆฌ์ผ์ด์ ์ด ์ง๋ฉดํ ๋ฌธ์ ์ค ์ผ๋ถ๋ ๋ ์ด์ ํ๋นํ์ง ์์ ์ ์์ผ๋ฉฐ, ํน์ ํ์ ์์๋ ์ด์ ์ ํ ์ฌํญ์ด ๋ ์ ์์ต๋๋ค.
์๋ฅผ ๋ค์ด, UI์ ๋ก์ง์ ๋ถ๋ฆฌํ ์ด์ ์ค ํ๋๋ ํนํ ํค๋๋ฆฌ์ค CI/CD ํ๊ฒฝ์์ ์ด ๋์ ์กฐํฉ์ ํ ์คํธํ๊ธฐ ์ด๋ ต๋ค๋ ์ ์ด์์ต๋๋ค. ๋ฐ๋ผ์ ํ ์คํธ ํ๋ก์ธ์ค๋ฅผ ๊ฐ์ํํ๊ธฐ ์ํด ๊ฐ๋ฅํ ํ ๋ง์ ๋ถ๋ถ์ UI๊ฐ ์๋ ์ฝ๋๋ก ์ถ์ถํ๋ ๊ฒ์ ๋ชฉํ๋ก ํ์ต๋๋ค. ํ์ง๋ง ์ด๋ ๋ฆฌ์กํธ์ ๋ค๋ฅธ ๋ง์ ์น ํ๋ ์์ํฌ์์๋ ํฐ ๋ฌธ์ ๊ฐ ๋์ง ์์ต๋๋ค. ์ฐ์ UI ๋์, DOM ์กฐ์ ๋ฑ์ ํ ์คํธํ ์ ์๋ jsdom๊ณผ ๊ฐ์ ๊ฐ๋ ฅํ ์ธ๋ฉ๋ชจ๋ฆฌ ํ ์คํธ ๋ฉ์ปค๋์ฆ์ด ์์ต๋๋ค. ์ด๋ฌํ ํ ์คํธ๋ ํค๋๋ฆฌ์ค CI/CD ์๋ฒ์ ๊ฐ์ ๋ชจ๋ ํ๊ฒฝ์์ ์คํํ ์ ์์ผ๋ฉฐ, ์ธ๋ฉ๋ชจ๋ฆฌ ๋ธ๋ผ์ฐ์ (์: ํค๋๋ฆฌ์ค Chrome)์์ Cypress๋ฅผ ์ฌ์ฉํ์ฌ ์ค์ ๋ธ๋ผ์ฐ์ ํ ์คํธ๋ฅผ ์ฝ๊ฒ ์คํํ ์ ์๋๋ฐ, ์ด๋ MVC/MVP๋ฅผ ๊ตฌ์ํ ๋น์ ๋ฐ์คํฌํฑ ์ ํ๋ฆฌ์ผ์ด์ ์์๋ ๋ถ๊ฐ๋ฅํ๋ ์ผ์ ๋๋ค.
MVC๊ฐ ์ง๋ฉดํ ๋ ๋ค๋ฅธ ์ฃผ์ ๊ณผ์ ๋ ๋ฐ์ดํฐ ๋๊ธฐํ๋ก, ํ๋ ์ ํฐ ๋๋ ํ๋ ์ ํ ์ด์ ๋ชจ๋ธ์ด ๊ธฐ์ด ๋ฐ์ดํฐ์ ๋ณ๊ฒฝ ์ฌํญ์ ์กฐ์จํ๊ณ ๋ค๋ฅธ ๋ ๋๋ง ๋ถ๋ถ์ ์๋ ค์ผ ํ์ต๋๋ค. ๋ํ์ ์ธ ์๊ฐ ์๋ ๊ทธ๋ฆผ๊ณผ ๊ฐ์ต๋๋ค.
์ฌ์ง 7: ํ๋์ ๋ชจ๋ธ์ ์ฌ๋ฌ ํ๋ ์ ํ ์ด์ ์ด ์์ต๋๋ค.
์ ๊ทธ๋ฆผ์์ ์ธ ๊ฐ์ง UI ์ปดํฌ๋ํธ(ํ, ๊บพ์์ ํ ์ฐจํธ, ํํธ๋งต)๋ ์์ ํ ๋ ๋ฆฝ์ ์ด์ง๋ง ๋ชจ๋ ๋์ผํ ๋ชจ๋ธ ๋ฐ์ดํฐ๋ฅผ ๋ ๋๋งํ๊ณ ์์ต๋๋ค. ํ์์ ๋ฐ์ดํฐ๋ฅผ ์์ ํ๋ฉด ๋ค๋ฅธ ๋ ๊ทธ๋ํ๊ฐ ์๋ก ๊ณ ์ณ์ง๋๋ค. ๋ณ๊ฒฝ ์ฌํญ์ ๊ฐ์งํ๊ณ ํด๋น ์ปดํฌ๋ํธ๋ฅผ ์๋ก ๊ณ ์น๋๋ก ๋ณ๊ฒฝ ์ฌํญ์ ์ ์ฉํ๋ ค๋ฉด ์ด๋ฒคํธ ๋ฆฌ์ค๋๋ฅผ ์๋์ผ๋ก ์ค์ ํด์ผ ํฉ๋๋ค.
๊ทธ๋ฌ๋ ๋จ๋ฐฉํฅ ๋ฐ์ดํฐ ํ๋ฆ์ด ๋ฑ์ฅํ๋ฉด์ ๋ฆฌ์กํธ๋ ๋ค๋ฅธ ๋ง์ ์ต์ ํ๋ ์์ํฌ๋ฅผ ๋ฐ๋ผ์ ๋ค๋ฅธ ๊ธธ์ ๊ฑท๊ฒ ๋์์ต๋๋ค. ๊ฐ๋ฐ์๋ก์ ์ฐ๋ฆฌ๋ ๋ ์ด์ ๋ชจ๋ธ ๋ณ๊ฒฝ ์ฌํญ์ ๋ชจ๋ํฐ๋งํ ํ์๊ฐ ์์ต๋๋ค. ๊ธฐ๋ณธ ์์ด๋์ด๋ ๋ชจ๋ ๋ณ๊ฒฝ ์ฌํญ์ ์์ ํ ์๋ก์ด ์ธ์คํด์ค๋ก ์ทจ๊ธํ๊ณ ๋ชจ๋ ๊ฒ์ ์ฒ์๋ถํฐ ๋ค์ ๋ ๋๋งํ๋ ๊ฒ์ ๋๋ค. ์ฌ๊ธฐ์ ์ ์ฒด ํ๋ก์ธ์ค๋ฅผ ํ์ ํ๊ฒ ๊ฐ์ํํ๊ณ ๊ฐ์ DOM๊ณผ ์ฐจ๋ณํ ๋ฐ ์กฐ์ ํ๋ก์ธ์ค๋ฅผ ๊ฐ๊ณผํ๊ณ ์๋ค๋ ์ ์ ์ฃผ๋ชฉํ๋ ๊ฒ์ด ์ค์ํฉ๋๋ค. ์ฆ, ์ฝ๋๋ฒ ์ด์ค ๋ด์์ ๋ชจ๋ธ ๋ณ๊ฒฝ ํ ๋ค๋ฅธ ์ธ๊ทธ๋จผํธ๋ฅผ ์ ํํ๊ฒ ์ ๋ฐ์ดํธํ๊ธฐ ์ํด ์ด๋ฒคํธ ๋ฆฌ์ค๋๋ฅผ ๋ฑ๋กํด์ผ ํ ํ์์ฑ์ด ์ฌ๋ผ์ก์์ ์๋ฏธํฉ๋๋ค.
์์ฝํ์๋ฉด, ํค๋๋ฆฌ์ค ์ปดํฌ๋ํธ๋ ๊ธฐ์กด UI ํจํด์ ์ฌ์ฐฝ์กฐํ๋ ๊ฒ์ด ๋ชฉ์ ์ด ์๋๋ผ ์ปดํฌ๋ํธ ๊ธฐ๋ฐ UI ์ํคํ ์ฒ ๋ด์์ ๊ตฌํํ๋ ์ญํ ์ ํฉ๋๋ค. ๋ก์ง๊ณผ ์ํ ๊ด๋ฆฌ๋ฅผ ๋ทฐ์์ ๋ถ๋ฆฌํ๋ ์์น์ ํนํ ๋ช ํํ ์ฑ ์์ ๋ช ์ํ๊ณ ํ ๋ทฐ๋ฅผ ๋ค๋ฅธ ๋ทฐ๋ก ๋์ฒดํ ๊ธฐํ๊ฐ ์๋ ์๋๋ฆฌ์ค์์ ๊ทธ ์ค์์ฑ์ ์ ์งํฉ๋๋ค.
ํค๋๋ฆฌ์ค ์ปดํฌ๋ํธ์ ๊ฐ๋ ์ ์๋ก์ด ๊ฒ์ด ์๋๋ฉฐ ํ๋์ ์กด์ฌํด ์์ง๋ง ๋๋ฆฌ ์ธ์ ๋ฐ๊ฑฐ๋ ํ๋ก์ ํธ์ ํตํฉ๋์ง๋ ๋ชปํ์ต๋๋ค. ํ์ง๋ง ์ฌ๋ฌ ๋ผ์ด๋ธ๋ฌ๋ฆฌ์์ ํค๋๋ฆฌ์ค ์ปดํฌ๋ํธ ํจํด์ ์ฑํํ์ฌ ์ ๊ทผ ๊ฐ๋ฅํ๊ณ ์ ์ ๊ฐ๋ฅํ๋ฉฐ ์ฌ์ฌ์ฉ ๊ฐ๋ฅํ ์ปดํฌ๋ํธ์ ๊ฐ๋ฐ์ ์ด์งํ๊ณ ์์ต๋๋ค. ์ด๋ฌํ ๋ผ์ด๋ธ๋ฌ๋ฆฌ ์ค ์ผ๋ถ๋ ์ด๋ฏธ ์ปค๋ฎค๋ํฐ ๋ด์์ ์๋นํ ์ฃผ๋ชฉ์ ๋ฐ๊ณ ์์ต๋๋ค.
์ด๋ฌํ ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ ๋ณต์กํ ๋ก์ง๊ณผ ๋์์ ์บก์ํํ์ฌ ํค๋๋ฆฌ์ค ์ปดํฌ๋ํธ ํจํด์ ๋ณธ์ง์ ๊ตฌํํ๋ฏ๋ก ์ธํฐ๋ํฐ๋ธํ๊ณ ์ ๊ทผ์ฑ์ด ๋ฐ์ด๋ UI ์ปดํฌ๋ํธ๋ฅผ ์ฝ๊ฒ ๋ง๋ค ์ ์์ต๋๋ค. ์ ๊ณต๋ ์์ ๋ ํ์ต์ ๋๋ค๋ ์ญํ ์ ํ์ง๋ง, ์ค์ ์๋๋ฆฌ์ค์์ ๊ฐ๋ ฅํ๊ณ ์ ๊ทผ ๊ฐ๋ฅํ๋ฉฐ ์ฌ์ฉ์ ์ง์ ๊ฐ๋ฅํ ์ปดํฌ๋ํธ๋ฅผ ๊ตฌ์ถํ๋ ค๋ฉด ์ด๋ฌํ ํ๋ก๋์ ์ง์ ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ฅผ ํ์ฉํ๋ ๊ฒ์ด ํ๋ช ํฉ๋๋ค.
์ด ํจํด์ ๋ณต์กํ ๋ก์ง๊ณผ ์ํ ๊ด๋ฆฌ์ ๋ํ ๊ต์ก์ ์ ๊ณตํ ๋ฟ๋ง ์๋๋ผ ํค๋๋ฆฌ์ค ์ปดํฌ๋ํธ ์ ๊ทผ ๋ฐฉ์์ ๊ฐ์ ํ์ฌ ์ค์ ํ๊ฒฝ์์ ์ฌ์ฉํ ์ ์๋ ๊ฐ๋ ฅํ๊ณ ์ ๊ทผ ๊ฐ๋ฅํ๋ฉฐ ์ฌ์ฉ์ ์ ์ ๊ฐ๋ฅํ ์ปดํฌ๋ํธ๋ฅผ ์ ๊ณตํ๋ ํ๋ก๋์ ์ง์ ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ฅผ ํ์ํ๋๋ก ์ ๋ํฉ๋๋ค.
์ด ๊ธ์์๋ ์ฌ์ฌ์ฉ ๊ฐ๋ฅํ UI ๋ก์ง์ ์ ์ํ ๋ ๊ฐ๊ณผํ๊ธฐ ์ฌ์ด ํจํด์ธ ํค๋๋ฆฌ์ค ์ปดํฌ๋ํธ์ ๊ฐ๋ ์ ์ดํด๋ด ๋๋ค. ๋ณต์กํ ๋๋กญ๋ค์ด ๋ชฉ๋ก ์์ฑ์ ์๋ก ๋ค์ด ๊ฐ๋จํ ๋๋กญ๋ค์ด๋ถํฐ ์์ํ์ฌ ํค๋ณด๋ ํ์, ๋น๋๊ธฐ ๋ฐ์ดํฐ ํ์นญ ๋ฑ์ ๊ธฐ๋ฅ์ ์ ์ง์ ์ผ๋ก ๋์ ํด ๋ณด์์ต๋๋ค. ์ด ์ ๊ทผ ๋ฐฉ์์ ์ฌ์ฌ์ฉ ๊ฐ๋ฅํ ๋ก์ง์ ํค๋๋ฆฌ์ค ์ปดํฌ๋ํธ๋ก ์ํํ๊ฒ ์ถ์ถํ๋ ๋ฐฉ๋ฒ์ ๋ณด์ฌ์ฃผ๋ฉฐ, ์๋ก์ด UI๋ฅผ ์ฝ๊ฒ ์ค๋ฒ๋ ์ดํ ์ ์๋ค๋ ์ ์ ๊ฐ์กฐํฉ๋๋ค.
์ค์ ์ฌ๋ก๋ฅผ ํตํด ์ด๋ฌํ ๋ถ๋ฆฌ๊ฐ ์ด๋ป๊ฒ ์ฌ์ฌ์ฉ ๊ฐ๋ฅํ๊ณ ์ ๊ทผ ๊ฐ๋ฅํ ๋ง์ถคํ ์ปดํฌ๋ํธ๋ฅผ ๊ตฌ์ถํ ์ ์๋ ๊ธธ์ ์ด์ด์ฃผ๋์ง ์กฐ๋ช ํฉ๋๋ค. ๋ํ ํค๋๋ฆฌ์ค ์ปดํฌ๋ํธ ํจํด์ ์นํธํ๋ React Table, Downshift, React UseGesture, React ARIA, ํค๋๋ฆฌ์ค UI์ ๊ฐ์ ์ ๋ช ํ ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ฅผ ์ง์ค ์กฐ๋ช ํฉ๋๋ค. ์ด๋ฌํ ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ ์ธํฐ๋ํฐ๋ธํ๊ณ ์ฌ์ฉ์ ์นํ์ ์ธ UI ์ปดํฌ๋ํธ๋ฅผ ๊ฐ๋ฐํ๊ธฐ ์ํด ๋ฏธ๋ฆฌ ๊ตฌ์ฑ๋ ์๋ฃจ์ ์ ์ ๊ณตํฉ๋๋ค.
์ด ์ฌ์ธต ๋ถ์์์๋ ํ์ฅ ๊ฐ๋ฅํ๊ณ ์ ๊ทผ ๊ฐ๋ฅํ๋ฉฐ ์ ์ง ๊ด๋ฆฌ๊ฐ ๊ฐ๋ฅํ ๋ฆฌ์กํธ ์ ํ๋ฆฌ์ผ์ด์ ์ ์ ์ํ๋ ๋ฐ ์์ด UI ๊ฐ๋ฐ ํ๋ก์ธ์ค์์์ ๊ด์ฌ์ฌ ๋ถ๋ฆฌ๊ฐ ๊ฐ์ง๋ ์ค์์ฑ์ ๊ฐ์กฐํฉ๋๋ค.
1: Fake๋ ์ธ๋ถ ์์คํ ์ด๋ ๋ฆฌ์์ค์ ๋ํ ์ก์ธ์ค๋ฅผ ์บก์ํํ๋ ๊ฐ์ฒด์ ๋๋ค. ๋ชจ๋ ์ฑํ ๋ก์ง์ ์ฝ๋๋ฒ ์ด์ค์ ๋ถ์ฐ์ํค๊ณ ์ถ์ง ์์ ๋ ์ ์ฉํ๋ฉฐ, ์ธ๋ถ ์์คํ ์ด ๋ณ๊ฒฝ๋ ๋ ํ ๊ณณ์์ ๋ณ๊ฒฝํ๊ธฐ๊ฐ ๋ ์ฌ์์ง ์ ์์ต๋๋ค.
๐ ํ๊ตญ์ด๋ก ๋ ํ๋ฐํธ์๋ ์ํฐํด์ ๋น ๋ฅด๊ฒ ๋ฐ์๋ณด๊ณ ์ถ๋ค๋ฉด Korean FE Article(https://kofearticle.substack.com/)์ ๊ตฌ๋ ํด์ฃผ์ธ์!