๐ช 14. Headless ์ปดํฌ๋ํธ (Custom Hooks์ ๋ํ์)
๐ ๊ฐ์
UI๋ ๋จ 1px๋ ๊ทธ๋ฆฌ์ง ์๊ณ , ์ค์ง '๊ธฐ๋ฅ๊ณผ ์ํ(๋)'๋ง ์บก์ํํ์ฌ ๋์์ธ๊ณผ ๋ก์ง์ ์๋ฒฝํ๊ฒ ๋ถ๋จ์ํค๋ ์ต์ ํธ๋ ๋ Headless ํจํด์ ์ ์๋ฅผ ๋ฐฐ์๋๋ค.
๐ฏ ์ด ์น์ ์ ์ฝ๊ณ ๋๋ฉด:
- '๋ชฉ ์๋(Headless) ์ปดํฌ๋ํธ'๋ ๋จ์ด๊ฐ ์๋ฏธํ๋ ์ง์ง ์ ์ฒด(Custom Hooks์ ์งํํ)๋ฅผ ์๋ฒฝํ ์ดํดํ๋ค.
- ๋์์ด๋๊ฐ ๋ฏธ์ณ ๋ ๋ฐ๋(...) ์ด๋ค ๊ธฐ๊ดดํ UI ์๊ตฌ์ฌํญ์๋, ๋ก์ง ์ฝ๋๋ฅผ ๋จ ํ ์ค๋ ๋ถ์์ง ์๊ณ ์ด์๋จ๋ ์ ์ค์ ์ธ ๋ถ๋ฆฌ(Separation) ์ค๊ณ๋ฅผ ํ ์ ์๋ค.
- ์ฌ๋ด ๊ณตํต ํ (Hooks) ๊ฐ๋ฐ ๋ฆฌ๋์ ์๋ฆฌ๋ฅผ ์์ทจํ ์ ์๋ค.
๐ ๋ชฉ์ฐจ
- ๐ค ์ ์์์ผ ํ๋๊ฐ: UI์ ๋ก์ง์ด ํ ๋ชธ์ผ ๋์ ๋ถ์น๋ณ
- ๐๏ธ ๋น์ ๋ก ๋จผ์ ์ดํดํ๊ธฐ
- ๐งฉ ๊ทน์ ์ฒด๋: ์ค์ ํ์ด๋จธ ํค๋๋ฆฌ์ค ํ ์ด์ ์์
- ๐ (์ฌํ ๋ณด๋์ค) ์์ฑ ์ฃผ์ ๊ธฐ(Prop Getters) ํจํด
๐ ์ด ๋ฌธ์๋ฅผ ์ฝ๊ธฐ ์ ์
โฑ๏ธ ์์ ์ฝ๊ธฐ ์๊ฐ: 10๋ถ / ํต์ฌ ํํธ: 6๋ถ
๐บ๏ธ ์ด ๋ฌธ์์ ๋ฐฐ๊ฒฝ ์ธ๊ณ๊ด: '์์๋ค ์ปค๋ฎค๋ํฐ'
- ์์(๋์์ด๋):"์์ฒ ์จ! ์ ๋ฒ์ ๋ง๋ ๊ทธ ๊ฒฐ์ ํ์ด๋จธ ํ์ด๋จธ์. ๋์์ธ ๊ฐํธํ๋ฉด์ ๋ชจ๋ฐ์ผ์์๋ ๋๊ทธ๋ผ๋ฏธ๋ก ๋น๊ธ๋น๊ธ ๋๋ฉด์ ๋จ์ ์ด๊ฐ ๋จ๊ฒ ํ ๊ฑฐ๊ณ ์, ๋ฉ์ธ์์๋ ํฌ๊ฒ ๊ธ์๋ก, ํค๋์์๋ ์๊ฒ ํ ์คํธ๋ก๋ง ๋์์ฃผ์ธ์. ์, ๊ธ์จ ์๊น๋ ๋จ์ ์๊ฐ์ ๋ฐ๋ผ ์๋ก๋ฌ๋กํ๊ฒ ๋ฐ๊ฟ์ฃผ์๊ณ ์!"
- ์์ฒ (์ ์
):"...๋ค? ํ์ด๋จธ ๋ก์ง์ด๋ CSS ๋์์ธ์ด๋ ํ ๋ฉ์ด๋ฆฌ๋ก ๊ตณํ๋จ๋๋ฐ... ์ด๊ฑธ ๋ค ๋ฏ์ด๊ณ ์ณ์ผ ํ๋์?
isMobileCircle,isHeaderSmallํ๋กญ์ค ๋ 500๊ฐ ๋ซ์ด์ผ๊ฒ ๋ค ์์..." - ์ํธ(๋ฆฌ๋): "์์ฒ ๋, ๊ทธ๋์ CSS๋ ๋ก์ง์ ๋ถ๋ฆฌํ๋ ๊ฒ ํ๋ก ํธ์ ์ฒ ํต ์์น์ ๋๋ค! ํต์ฌ ๋ก์ง(Raw Logic) ๋ง ๋จ๊ธฐ๊ณ ์คํ์ผ(UI) ์ ์น ๊ฑท์ด๋ด์ธ์. ์ง์ ํ ํค๋๋ฆฌ์ค(Headless) ๋ก ๊ฐ์์์์๋ค."
๐ค ์ ์์์ผ ํ๋๊ฐ: UI์ ๋ก์ง์ด ํ ๋ชธ์ผ ๋์ ๋ถ์น๋ณ
13๊ฐ์์ ๋ฐฐ์ด ์ปดํ์ด๋ ์ปดํฌ๋ํธ๋ ํ๋ฅญํ์ง๋ง, ์ด์จ๋ <div> ๊ป๋ฐ๊ธฐ ๊ฐ์ ๊ธฐ๋ณธ ๋ ์ด์์๊ณผ DOM ๋ผ๋๋ฅผ ๋ง๋ ์ปดํฌ๋ํธ๊ฐ ์ด๋ ์ ๋ ๋ค๊ณ ์์ด์ผ ํฉ๋๋ค.
ํ์ง๋ง ํ์
์์๋ "๋์(์ฒดํฌ๋ฐ์ค ๊ธฐ๋ฅ, ๋๋๊ทธ ๋๋กญ, ํ์ด๋จธ ๋ฑ)์ ์๋ฒฝํ ๋๊ฐ์๋ฐ ๊ป๋ฐ๊ธฐ ๋ชจ์์๋ ์ฐ์ฃผ ๋๊น์ง ๋ฌ๋ผ์ง๋" ๊ดด์ํ ์๊ตฌ๊ฐ ์์์ง๋๋ค.
๐ค ์ ๊น, ๋จผ์ ์๊ฐํด๋ด
"๋น๋ฐ๋ฒํธ ๊ท์น ๊ฒ์ฆ ํผ"์ด ์๋ค๊ณ ์ณ๋ณด์.
ํ์๊ฐ์ ํ์ด์ง์์ ๊ฑฐ๋ํ ํ์ ๋ฐ์ค ์์ ๋นจ๊ฐ ๊ธ์จ๋ก ๊ฒฝ๊ณ ๋ฅผ ๊ทธ๋ฆฌ์ง๋ง, ๋ง์ดํ์ด์ง ๋น๋ฐ๋ฒํธ ์์ ์ฐฝ์์๋ input ๋ฐ์ค ๋ฐ๋ก ์๋ ์์ฃผ ์์โ์์ด์ฝ์ผ๋ก ํํํด์ผ ํด.
๋ ๋ค **"๋น๋ฐ๋ฒํธ๊ฐ 8์ ์ด์์ด๊ณ ํน์๋ฌธ์๊ฐ ํฌํจ๋์๋์ง ์ฒดํฌํ๋ ๋(Logic)"**๋ ๊ฐ์์? ์ด๋ป๊ฒ ๊ทธ ๋๋ง ์ ๋นผ๋จน์ง?
๐๏ธ ๋น์ ๋ก ๋จผ์ ์ดํดํ๊ธฐ
๐ง 5์ด์๊ฒ ์ค๋ช ํ๋ค๋ฉด?
- ์์ฒ ์ด๋ ๋ก๋ด ๊ณต์์ ์ฌ์ฅ๋์ด์ผ. "๊ฑธ์ด ๋ค๋๋ ๋ก๋ด"์ ์ฃผ๋ฌธ๋ฐ์ผ๋ฉด ์ณ๋ฉ์ด๋ฆฌ๋ก ๋ชธ์ฒด๋ ๋ค ๊น๊ณ ๋ชจํฐ๋ ๋ฌ์์ ์์ฑ๋ ์์ ๋ก๋ด์ ๊ณ ๊ฐ์๊ฒ ๋์ ธ์คฌ์ด(์ผ๋ฐ ์ปดํฌ๋ํธ).
- ๊ณ ๊ฐ(์์) ์: "์ฌ์ฅ๋, ๋ค์ ์ฃผ๋ฌธ์! ์ด๋ฒ์ ๊ฑท๋ ์ธํ์ธ๋ฐ... ์ธํผ๋ ํฌ๊ทผํ ๊ณฐ ์ธํ์ผ๋ก, ๊ทธ ๋ค์ ๊ฑด ์ ๋ฆฌ๋ก ๋ ํฌ๋ช ๊ฑด๋ด์ผ๋ก, ๊ทธ ๋ค์๊ฑด ์ง๊ทธ๋ฌ์ด ์ด์ ์ธ๊ณ์ธ์ผ๋ก ๋ง๋ค์ด์ฃผ์ธ์!"
- ๋ฉํ์ด ๋๊ฐ ์์ฒ ์ด. ์ณ๋ฉ์ด ์์ ๋ก๋ด ๋ชธํต ๊ป์ง์ ์ผ์ผ์ด ๋ถ์๊ณ ๋ค์ ์ธํ์ ์กฐ๊ฐํ๋ค ์ฐ๋ฌ์ง.
- ์ด๋ ์ํธ ๋ฑ์ฅ: "์ฌ์ฅ๋, ๋ฐ๋ณด์ธ์? ๊ทธ ๊ธฐ๊ณ ๋(CPU ์นฉ)์ ๋ชจํฐ ๊ด์ ๋ผ๋๋ง(Headless) ๋ฑ ์กฐ๋ฆฝํด์ ๋ดํฌ์ ๋ด์ ๋๊ธฐ์ธ์. ๊ฒ ์คํจ, ํธ๊ฐ์ฃฝ, ์ ๋ฆฌ ๋ถํ ๊ฐ์ ์ผ๊ตด(Head / UI)์ ๊ณ ๊ฐ์ด ์ง์ ์ฐฐํ์ ์ฌ์ ๊ทธ ๋ผ๋ ์์ ๋น์ด ์ ํ๋๋ก ๋ง์ด์ฃ !"
์ด๊ฒ์ด Head(์ธํ UI/DOM ์์)๋ฅผ ๋ ๋ ค๋ฒ๋ฆฐ ๋ฌดํ์ ๋ก์ง ์ฝ์ด ์ฅ์น, Headless(ํค๋๋ฆฌ์ค) ํจํด์ ๋๋ค. React ์ํ๊ณ์์ ์ฃผ๋ก Custom Hook ์ด๋ผ๋ ์๋ฒฝํ ์๋จ์ผ๋ก ํํ๋ฅผ ๋ ๊ฒ ๋ฉ๋๋ค.
๐งฉ ๊ทน์ ์ฒด๋: ์ค์ ํ์ด๋จธ ํค๋๋ฆฌ์ค ํ ์ด์ ์์
ํ์ด๋จธ ์ปดํฌ๋ํธ๋ฅผ ์๋ก ๋ค์ด, ์ ๋ชฝ ๊ฐ์ ์ฝ๋๋ฅผ ๋์์ ๋ก ๋ถ๋ฆฌํด ๋ณด๊ฒ ์ต๋๋ค.
โ 1๋จ๊ณ: ๋ชธํต(UI)๊ณผ ๋(Logic)๊ฐ ์ตํฉ๋ ๋์ฐํ ๊ดด๋ฌผ ์๋ณธ
// ๋์ ์ผ๊ตด์ด ํ ๋ฉ์ด๋ฆฌ๋ก ์ ์ฐฉ๋ ์ปดํฌ๋ํธ (๋์์ธ ๋ณ๊ฒฝ ๋ถ๊ฐ!)
function OldDeathTimer({ initialSeconds }) {
// --- ๐ง ๋(๋ก์ง) ์์ญ ---
const [timeLeft, setTimeLeft] = useState(initialSeconds);
const isDanger = timeLeft <= 5;
useEffect(() => {
// setInterval 1์ด๋ง๋ค ๊น๋จน๋ ๋ก์ง... (๊ธธ์ด์ ์๋ต)
}, []);
// --- ๐ง ๋ ์์ญ ๋ ---
// --- ๐ค ์ผ๊ตด(UI/DOM) ์์ญ ---
// ๐ฅ ๋์์ด๋๊ฐ "์ํ์ผ๋ก ์ด์๊ฒ ๊ทธ๋ ค์ฃผ์ธ์!" ํ๋ฉด ์ด ๊ป๋ฐํค๋ ์ฐ๋ ๊ธฐํตํ.
return (
<div className="border border-black p-4">
<h2 style={{ color: isDanger ? 'red' : 'black' }}>
๋จ์ ์๊ฐ: {timeLeft}์ด!
</h2>
<button onClick={() => setTimeLeft(initialSeconds)}>๋ฆฌ์
</button>
</div>
);
// --- ๐ค ์ผ๊ตด ์์ญ ๋ ---
}โ 2๋จ๊ณ: ๋๋ง ์ ์ถํด์ ๋ดํฌ(Custom Hook)์ ๋ด๊ธฐ (Headless)
์ํธ ํ๋ก ํธ ์ธ๊ณผ ์ ๋ฌธ์์ ์ง๋ ์๋, UI ์ฝ๋๋ฅผ ๋จ ํ ์ค๋ ๋จ๊ธฐ์ง ์๊ณ ์์ ํจ์/์ํ ๋ฐฐ์ด ๊ฐ์ฒด๋ง ๋ฑ์ด๋ด๋ ํ ์ผ๋ก ๋ง๊ฐ์กฐํฉ๋๋ค.
// ๐ฏ Headless Hook: ์ค์ง ๊ธฐ๋ฅ(ํ์ด๋จธ)๊ณผ ์ํ(๋จ์ ์ด)๋ง ๊ด๋ฆฌํ๋ ์์ ๋
export function useCowndownTimer(initialSeconds) {
const [timeLeft, setTimeLeft] = useState(initialSeconds);
const isDanger = timeLeft <= 5;
// setInterval ๋ก์ง (์๊น๋ ๊ฐ์)
useEffect(() => { ... }, []);
const resetTimer = () => setTimeLeft(initialSeconds);
// ๐ฅ UI(div, span)๋ ๋จ ํ๋๋ ๋ฆฌํดํ์ง ์๋๋ค!
// ๊ทธ์ ๊ณ์ฐ๋ '๊ฒฐ๊ณผ ๋ฐ์ดํฐ'์ '์กฐ์ข
์ฉ ๋ฆฌ๋ชจ์ปจ ํจ์' ๋ฐ๊ตฌ๋๋ง ๋์ง๋ค.
return {
timeLeft,
isDanger,
resetTimer
};
}โ 3๋จ๊ณ: ๋์์ด๋ ๋ง์๋๋ก ๋น์(์คํจ ์ ํ๊ธฐ) ๊ฒฐํฉ ์๊ฐ
์ด์ ํ๋ก ํธ๋ ๋์์ด๋๊ฐ ์จ๊ฐ ์๊ตฌ๋ฅผ ๋์ ธ๋ ๋ฌด์์ ์ฐ์ต๋๋ค. useCountdownTimer๋ผ๋ ๋ฌดํ์ ์ํผ(๋)์ ๋ถ๋ฌ์์ ๊ฐ๊ธฐ ๋ค๋ฅธ ์ด์ ์ก์ฒด(UI)์ ๋น์์์ผ๋ฒ๋ฆฌ๋ฉด ๋์
๋๋ค!
// ๋น์ ๋ชธ์ฒด 1: ๊ด๋ฆฌ์์ฉ ์ด์ค๋ฌ์ด ํ
์คํธ UI
function AdminTextTimer() {
// ๋ฌดํ์ ๋๋ฅผ ํธ์ถํ์ฌ ์ด ๋ฐฉ์ ๊ฐ๋ฆผ์ํด
const { timeLeft, resetTimer } = useCowndownTimer(60);
return (
<div>
<p>[๊ด๋ฆฌ์ ๊ฒฝ๊ณ ] ์ธ์
๋ง๋ฃ๊น์ง {timeLeft}s ๋จ์.</p>
<button onClick={resetTimer}>์ฐ์ฅํ๊ธฐ</button>
</div>
);
}
// ๋น์ ๋ชธ์ฒด 2: ์์ ๋์์ด๋๊ฐ ๋นก์ธ๊ฒ ์๊ตฌํ ์์ ์๋ก๋ฌ๋ก ๋ฒ๋ธ UI
function MagicBubbleTimer() {
// ๋๊ฐ์ ๋ฌดํ์ ๋๋ฅผ ํธ์ถ! ๋ก์ง ์ค๋ณต ์ฝ๋ ์์ฑ 0์ค!
const { timeLeft, isDanger, resetTimer } = useCowndownTimer(60);
return (
<div className={`rounded-full p-10 ${isDanger ? 'bg-red-500' : 'bg-blue-300'}`}>
<span className="text-4xl text-white drop-shadow-lg">
{timeLeft}
</span>
<div onClick={resetTimer} className="hover:scale-110 cursor-pointer">
โจ ๋ง๋ฒ ์ด๊ธฐํ
</div>
</div>
);
}ํ์์ ์ด์ง ์์ต๋๊น? ์์ฒ ์ด๊ฐ isMobileCircle, isAdminText ๊ฐ์ ํ๋กญ์ค ์ง์ฅ์ ํ๋ฉด์ ์ปดํฌ๋ํธ๋ฅผ ์กฐ์ข
ํ๋ ค๋(IoC ์คํจ) ๊ณผ๊ฑฐ์๋ 180๋ ๋ค๋ฆ
๋๋ค. ์ด ํค๋๋ฆฌ์ค ํ
์ ๊ทธ ์ด๋ ํ ๋ฏธ๋์ ์๋ก์ด ๋์์ธ์๋ ์๋ฒฝํ ๋ฉด์ญ(Immune)๋ ์์์ ์ฝ๋๊ฐ ๋์์ต๋๋ค.
๐ (์ฌํ ๋ณด๋์ค) ์์ฑ ์ฃผ์ ๊ธฐ(Prop Getters) ํจํด
ํด์ธ ์ ๋ช
Headless UI ๋ผ์ด๋ธ๋ฌ๋ฆฌ(Tanstack Table, Downshift ๋ฑ)๋ฅผ ๊น๋ณด๋ฉด, ์ ๋ฌดํ์ ๋๊ฐ ๊ทธ๋ฅ timeLeft ์ซ์ ๊ฐ๋ง ๋์ ธ์ฃผ๋ ๊ฑธ ๋์ด, "์ฌ์ฉ์๊ฐ UI ํ๊ทธ ๊ป๋ฐ๊ธฐ ๋ฐ์ ๋ ์ด๋ฐ ์ ๊ทผ์ฑ(aria) ์์ฑ์ด๋ onClick ์ด๋ฒคํธ ํธ๋ค๋ฌ๋ฅผ ๋น ํธ๋ฆฌ์ง ๋ง๊ณ ๊ผญ ๊ฐ์ด ๋ฐ์๋ฃ์ผ๋ ด~" ํ๊ณ ์ต์
๋ชฝ์น(props ๊ฐ์ฒด)๋ฅผ ํต์งธ๋ก ๋์ ธ์ฃผ๋ ๊ธฐ๋ฒ์ ์๋๋ค.
// ๋(Hook) ์์ ์งํ: Prop Getters ๋ฐ์ฌ๊ธฐ
function useDropdownHeadless() {
const [isOpen, setIsOpen] = useState(false);
// ์ด ๋ฌดํ์ ๋๊ฐ, ๋ฏธ๋์ ๊ป๋ฐ๊ธฐ๊ฐ ๋ UI์ ์์ฑํ ๋ญ์น๋ฅผ ๋ฏธ๋ฆฌ ๋ง๋ค์ด์ค๋ค.
const getToggleButtonProps = () => ({
"aria-expanded": isOpen, // ์๊ฐ์ฅ์ ์ธ์ฉ ์ํํ์
id: "magic-dropdown-btn",
onClick: () => setIsOpen(!isOpen) // ๋ด๊ฐ ๋ง๋ ํ ๊ธ ํจ์ ๊ฐ์ ๊ฒฐํฉ
});
return { isOpen, getToggleButtonProps };
}
// ์ฌ์ฉ์(UI ์ฐฐํ ์ฅ์ธ)์ ๊ฒฐํฉ
function MyShinyDropdown() {
const { isOpen, getToggleButtonProps } = useDropdownHeadless();
// ๋ฒํผ์ getToggleButtonProps()๊ฐ ๋ฑ์ด๋ธ ๊ฐ์ฒด์ ํค-๊ฐ์ ์ค๋ฅด๋ฅต ํ์ด์ ํฉ๋ฟ๋ฆฐ๋ค!
return (
<>
<button className="shiny-gold-btn" {...getToggleButtonProps()}>
๋์ ํ ๊ธ
</button>
{isOpen && <div>๊น๊ฟ</div>}
</>
);
}์ด๊ฒ ์ ์ฉ๋๊ฐ?: ๊ป๋ฐ๊ธฐ๋ฅผ ๋น๋ ์ฌ๋์ ํ๋ คํ ๊ป๋ฐ๊ธฐ CSS ๋์์ธ์๋ง ๋ชฐ๋ํ๋ฉด ๋ฉ๋๋ค. ๋ด๋ถ์ ์ผ๋ก ์๊ฐ์ฅ์ ์ธ์ฉ ์ ๊ทผ์ฑ ์ฝ๋๋ฅผ ๋ฃ๊ฑฐ๋ ํค๋ณด๋ ์ด์ค์ผ์ดํ ์ง์ ์ด๋ฒคํธ ๋ฑ(ARIA ํ๊ทธ, onKeyDown)์ ๋ ์ปดํฌ๋ํธ(Hook ์ค๊ณ์)๊ฐ getButtonProps ์์ ์ฑ์น ์จ๊ฒจ๋๊ณ ์ธํ
์ ๋ค ํด๋จ์ผ๋๊น์! ์ญํ ๊ตฌ๋ถ์ด ๊ทน์น์ ๋ค๋ค๋ฆ
๋๋ค.
๐ ์ด๋ฒ์ ๋ฐฐ์ด ๋ด์ฉ ์ด์ ๋ฆฌ
| ๊ด์ | ์ผ๋ฐ UI ์ปดํฌ๋ํธ (<Modal /> ๋ฑ) | Headless (Custom Hook) |
|---|---|---|
| ํ์ฒด(DOM ์์) | div, button ๋ฑ์ ๋ฑ์ด๋ (UI ๊ท์) | ์์ JavaScript ๊ฐ์ฒด๋ ํจ์๋ง ๋ฑ์ (return {}) |
| ๋์์ธ ์์ ๋ | ํ์ ์ (Props๋ก ํ๋ฝ๋ฐ์ ๊ฒ๋ง ๋ฐ๊ฟ) | ๋ฌดํ๋ (Infinity). ๋์์ธ ํ๊ทธ๋ฅผ ์ง๋ ์ฌ๋ ๋ง๋๋ก ํฌ์ฅ์ง ๊ฐ์๋ผ์. |
| ์ถ์ฒ ์ฌ์ฉ ๋๋ฉ์ธ | ์ฌ๋ด ๊ณ ์ ๋ฒํผ, ๋ฒ์ฉ ๋ชจ๋ฌ์ฐฝ, ์ค์ผ๋ ํค | ๋ฌ๋ ฅ(Datepicker) ๋ก์ง, ๋๋๊ทธ ๋๋กญ ์ฒ๋ฆฌ, ๋ก์ปฌ์คํ ๋ฆฌ์ง ๋๊ธฐํ ํผ, ์ฝค๋ณด๋ฐ์ค ์ํ ๋ฑ (๋ก์ง์ ๋ณต์กํ๋ฐ ๋ทฐ๋ ํํธํ๋ ๊ฒ๋ค) |
๐ก ํ ์ค๋ก ๊ธฐ์ตํ๊ธฐ
"๋ณด์ด๋ ๊ฒ(UI)๊ณผ ๋ณด์ด์ง ์๋ ์๊ฐ(Logic)์ ๊ฒฐํฉ์ ๋ถ์ํ๋ผ."
์ฌ๋ฌ ํ๊ฒฝ์์ ๋ชจ์๋ง ๋ค๋ฅด๊ณ ๋๊ฐ์ ๊ธฐ๋ฅ์ ๋ฐ๋ณตํด์ ์ง๊ณ ์๋ค๋ฉด, ๋น์ ์ด ์ง๊ธ ๋น์ฅ ๋ฝ์๋ด์ ๊ฒฉ๋ฆฌํด์ผ ํ ๊ฒ์ UI ์กฐ๊ฐ์ด ์๋๋ผ 'Headless Custom Hook' ์นฉ์ ์ค๊ณ๋๋ค.
๐ฃ ์์ฒ ์ด์ ํด๊ทผ ์ผ๊ธฐ
๋์์ธ ๋ฐ๋ ๋๋ง๋ค ์ณ๋ฉ์ด ๋ก๋ด ์ธํผ๋ง ํ์ผ์์ด ๊ฐ์์น์ฐ๋ ๋๋ ๋ค์ด ๋ ์ค๋ฅธ๋ค. ๋๋ ์ด์ ์ ์์ ํ ๊ฒฉ๋ฆฌํ๋ ๋์์ ๋น์ ๋ ์ง์ง ์ญ๋๊ธ์ด์๋ค.
๐ก "UI ๊ป๋ฐ๊ธฐ๋ฅผ ๋ ๋ ค๋ผ(Headless). ์ค์ง ์์ํ ๊ธฐ๋ฅ๊ณผ ๋ฆฌ๋ชจ์ปจ(Hook)๋ง ๋ฑ์ด๋ด๊ณ , ๊ทธ ์ท ์ ํ๋ ๊ฑด ์ฐฐํ ์ฅ์ธ(์ฌ์ฉ์ ์ปดํฌ๋ํธ)์๊ฒ ์๋ฒฝํ ์์ํ๋ผ."
Prop Getters ๊ธฐ๋ฒ ์ด ๊ฑด ์์ ํ ์ถฉ๊ฒฉ์ ์ด์๋ค. ๋ด๊ฐ ์ปดํฌ๋ํธ์ ๋๋ฅผ ์งค ๋, ๋จ๋ค์ด ํด๋ฆญ ํธ๋ค๋ฌ๋ ์ ๊ทผ์ฑ ๊ณ ๋ ค ๋ชปํ ๊ฒ๊น์ง ๋ค ์ฅ์ฐฉํ ์ต์
๋ญํ
์ด๋ฅผ {...getSlotProps()}๋ก ํ ๋ฐฉ์ ์ด์ค๋ค๊ณ ? ์ด๊ฑด ๊ฑฐ์ ์์ ์ ๊ฒฝ์ง๋ค. ๋๋์ด ๋ฆฌ์กํธ ์ค๊ณ์ ์ ์ ์ ๋ดค๋ค๋ ์ฐํ ์ฑ์ทจ๊ฐ์ด ๋ชฐ๋ ค์จ๋ค. ์ํธ ์ ๋ฐฐ ์ต๊ณ !
๐ ๋ง๋ฌด๋ฆฌ ํด์ฆ
Q1. useWindowSize (๋ธ๋ผ์ฐ์ ๊ฐ๋ก ์ธ๋ก ํฝ์
์ ์ก๊ณ ๋ฆฌ์ฌ์ด์ฆ๋ฅผ ์ถ์ ํ๋ ํ
)๋ ์ฐ๋ฆฌ๊ฐ ๋ฐฐ์ด useCountdownTimer ๊ฐ์ ๋ก์ง๋ค์ ๊ตณ์ด React ์ผ๋ฐ ์ปดํฌ๋ํธ(DOM ์์ ๋ฆฌํด)๋ก ๋ง๋ค์ง ์๊ณ 'Headless ํ
'์ผ๋ก ๋ถ๋ฆฌํด์ผ ํ๋ ๊ฐ์ฅ ๊ฒฐ์ ์ ์ธ ์ด์ ๋ ๋ฌด์์ธ๊ฐ์?
- A) ํด๋์ค ๊ธฐ๋ฐ React ์ํ๊ณ์ ์์ฌ๋ฅผ ์ง์ฐ๊ธฐ ์ํด.
- B) ์ด ๋ก์ง๋ค ์์ฒด๋ ๋์ ๋ณด์ด๋ '๊ฐ์์ ํํ(๋์์ธ)'๋ฅผ ๋จ ํ์๊ฐ ์์ผ๋ฉฐ, ์ด๋ ๊ตฌ์์ ๋ถ๋ชจ ์ปจํ ์ด๋๋ , ๊ผฌํฌ๋ฆฌ ๋ง๋จ ํ ์คํธ ํ๊ทธ๋ ์์ ์์ฌ๋ก ๊ทธ ๊ณ์ฐ๋ '๋ฐ์ดํฐ ๊ฐ'๋ง ์ฃผ์ ๋ฐ์์ ์๊ธฐ ์ท์ ๋ง์ถฐ ์ ์ด์ผ ํ๋ ์์ ์ข ์์ฑ ์๋ ๋น์ฆ๋์ค/ํ๊ฒฝ ํจ์์ด๊ธฐ ๋๋ฌธ.
- C) Headless ํ ์ ๋น๋ ๊ณผ์ ์์ ์๋ฒ๋ฆฌ์ค(Serverless) ํด๋ผ์ฐ๋ ํ๊ฒฝ์ผ๋ก ์ปดํ์ผ๋์ด ์ฐ์ฐ ์๋๊ฐ ๋ฌดํ์ ์๋ ดํ๊ธฐ ๋๋ฌธ.
โ ์ ๋ต: B
๐ก ์์ธ ํด์ค: ์ฐฐ๋ก๊ฐ์ ์ค๋ช
์
๋๋ค! useWindowSize๋ฅผ ๋ง์ฝ ์ปดํฌ๋ํธ <WindowSizeReporter> ๋ผ๊ณ ์ง์ ์ด๋๊ฐ์ ๋ง์ดํธ์ํค๊ณ ๊ฑฐ๊ธฐ์๋ถํฐ ์์ํ ํ๋กญ์ค ๋๋ฆด๋ง์ผ๋ก ํฌ๊ธฐ ๊ฐ์ ๋ฟ๋ ค์ค์ผ ํ๋ค๋ฉด ๋์ฐํ๊ฒ ์ฃ . ๊ทธ๋ฅ ๋(๊ธฐ๋ฅ ๋ชจ๋ ๋ก์ง ๋ฌถ์)๋ก ์ง๋ฒ๋ฆฌ๊ณ ๊ทธ ๊ฒฐ๊ณผ ๋ฐ์ดํฐ ๊ฐ์ฒด({ width, height })๋ง ํ๊ณต์ ๋ฟ๋ ค๋๋ฉด, ์ด๋ค ๋ฏธ์น ๋์์ธ์ ์ปดํฌ๋ํธ๋ ๊ทธ ๋๋ฅผ ์ฅ์ฐฉ(useWindowSize())ํ์ฌ ๊ทธ ์ซ์ ๋ฆฌ๋ชจ์ปจ์ ๋ฐ์ ์๊ธฐ๊ฐ ์ธ ๋ฐ ํธํ๊ฒ ์ธ ์ ์์ต๋๋ค. ๊ทน๊ฐ์ ์ข
์์ฑ ํ์ถ(Decoupling)์
๋๋ค.
Q2. ํค๋๋ฆฌ์ค ํ
ํจํด์ 'Prop Getters(์ต์
๋ญ์น ๋ฐ์ฌ๊ธฐ)' ์ฌํ ๊ธฐ๋ฒ์ ์ฌ์ฉํ ๋, ๋ถ๋ชจ์์ ... (์คํ๋ ๋ ์ฐ์ฐ์) ๋ฌธ๋ฒ์ ํตํด props๋ฅผ ์ฃผ์
ํ๋ ์ฝ๋์ ์๋ ๋ฐฉ์์ ๋ฐ๋ฅด๊ฒ ์ดํดํ ๊ฒ์?
const { getSlotProps } = useSlotMachineHeadless();
<button {...getSlotProps()} />- A) ์๋ฐ์คํฌ๋ฆฝํธ๋ ์ ๊ตฌ๋ฌธ์ ํ์ฉํ์ง ์์ผ๋ฏ๋ก ์๋ฌ๊ฐ ํฐ์ง๋ค. ๋ฐ๋์
id={getSlotProps().id}์ฒ๋ผ ์ผ์ผ์ด ์ ์ด์ผ ํ๋ค. - B)
getSlotProps()๋ ์ฌ์ค์ ๋ฆฌ์กํธ ๊ฐ์ ๋ ์์ง์ ์ง์ ์ ์ํ๋ ํดํน ์คํฌ๋ฆฝํธ๋ฅผ ๋ฑ์ด๋ธ๋ค. - C) ํ
์ด ๋ด๋ถ์์ ๊ณ์ฐํ์ฌ ๋ฑ์ด์ค ๊ฐ์ฒด(์:
{ onClick: fn, "aria-hidden": true })์ ์์ฑ ํค์ ๊ฐ๋ค์ ๋ฆฌ์กํธ JSX ํ๊ทธ์ ์๋ง์ ํ๋กญ์ค ํ๋ผ๋ฏธํฐ๋ค๋ก ์ค๋ฅด๋ฅต ํด์ฒดํ์ฌ ํ ๋ฐฉ์ ์ ๋ถ ๋๋ ค ๋ฃ์ด์ฃผ๋ ์๋ฐ์คํฌ๋ฆฝํธ ์ต๊ฐ์ ๊ฐ์ฒด ๋ฐ์ธ๋ฉ ๋ง๋ฒ์ด๋ค.
โ ์ ๋ต: C
๐ก ์์ธ ํด์ค: Spread Attributes ๋ฌธ๋ฒ์ ๋ง๋ฒ์
๋๋ค. ์ด ๋ฌธ๋ฒ ๋๋ถ์ ํค๋๋ฆฌ์ค ํ
์ฐฝ์กฐ์๋ ๋ด๋ถ ์ ๊ทผ์ฑ(a11y) ์์ฑ์ด 20๊ฐ๋ , ๋ง์ฐ์ค ์ด๋ฒคํธ๊ฐ 5์ข
๋ฅ๋ ๊ทธ๋ฅ ํฐ ๊ฐ์ฒด ๋ญ์น ํ๋๋ฅผ ๋ฑ์ด๋ฒ๋ฆฌ๋ฉด ๋๋ฉ๋๋ค. UI๋ฅผ ์
ํ๋ ์ฌ์ฉ์(Front-end Dev) ์ธก์ ์์ ์ด ๋์์ธํ ์ด์งํธ ํ ํฉ๊ธ ๋ฒํผ ๊ป๋ฐ๊ธฐ๋ฅผ ๋ง๋ค์ด ๋๊ณ ๊ทธ ๊ณณ์ ...getSlotProps() ์ด ํฌ์ฅ์ง ํด์ฒด ์ฃผ๋ฌธ ๋ฑ ํ๋๋ฅผ ์ธ์ฐ๊ธฐ๋ง ํ๋ฉด, ํ
์ด ์ง๋ ๊ฑฐ๋ฏธ์ค ๊ฐ์ 25๊ฐ์ ์์ฑ๊ณผ ํจ์๋ค์ด ์์๊ฐ์ ์ ๊ป๋ฐ๊ธฐ ์์ ฏ ์์ผ๋ก ๊ฐ์ ๋ก ์ค๋ฉฐ๋ค์ด ์ ์ฐฉ(Bind)๋๋ ๊ธฐ์ ์ ๋ฐํํฉ๋๋ค. DX(๊ฐ๋ฐ ๊ฒฝํ)์ ์ ์ ์ด๋ผ ๋ถ๋ฆฌ๋ ๊ตฌ์กฐ์ฃ .
Q3. ์ํธ๊ฐ ํค๋๋ฆฌ์ค(Headless) ๊ตฌ์กฐ๋ก ๋ฌ๋ ฅ(Datepicker)์ ์๋ ์ฝ์ด ๋๋ฅผ ๋ถ๋ฆฌํด๋์์ ๋, ๋ค์ ์ค ์ด '๋ถ๋ฆฌ ์ํคํ ์ฒ' ๋๋ถ์ ํ์ฌ ์ ์ฒด์ ์ผ๋ก ์ป์ ์ ์๋ ์ด๋์ ๋ชจ๋ ์์ ํด ๋ณด์ธ์.
โ ์ ๋ต ๋ฐ ์ฃผ๊ด์ ํด์ค:
- ๊ทน์ ์ธ ์ฝ๋ ์ฌ์ฌ์ฉ์ ํ์ฅ (ํฌ๋ก์ค ํ๋ซํผ): ํ ๋ฒ ์ ์ง๋ ๋(Headless Hook)์ ๋ฌ๋ ฅ ๊ณ์ฐ ํจ์, ์ ๋์ด๊ฐ ๋ก์ง์ ํ์ฌ PC์น์ฉ ๋ฌ๋ ฅ์ ๊ทธ๋ฆฌ๋ ๋ฐ์๋ ๋๊ฐ์ด ์ฐ์ผ ์ ์๊ณ , ์์ ๊ป๋ฐ๊ธฐ UI ์ฒด๊ณ๊ฐ ๋ค๋ฅธ ๋ชจ๋ฐ์ผ(App) React Native ํ๊ฒฝ์ผ๋ก ์ง์ถํ ๋๋, ์ฌ์ง์ด ๋ชจ๋ฐ์ผ์ฉ UI ์ปดํฌ๋ํธ ๋ผ์ด๋ธ๋ฌ๋ฆฌ๊ฐ ํต์งธ๋ก ๋ฐ๋๋๋ผ๋ ์ด '๋ ๋ก์ง ํ์ผ'๋งํผ์ ๋จ ํ ๊ธ์ ๊น์ ์์ด ๊ทธ๋๋ก 100% ์ํฌํธํด์ ๋์์ ๋ก ๋ฐ์ ์ธ ์ ์๋ ๋ฒ์ฐ์ฃผ์ ๊ณตํต ์ฝ์ด๊ฐ ๋ฉ๋๋ค.
- ํ ์คํธ ๊ณ ๋ํ์ ์ฌ์: UI(๋ฒํผ ์์ ๊น๋ฆฌ๊ณ ์ ๋๋ฉ์ด์ ๋๊ณ ..)๋ฅผ ํ ์คํธํ๋ ค๋ฉด ๋ณต์กํ ํ ์คํ ๋๊ตฌ๊ฐ ํ์ํ์ง๋ง, Headless Hook์ ๋ณธ์ง์ด ์์ ๋ก์ง์ด๋ฏ๋ก (๊ฐ์ ๋ฃ์ด์ฃผ๋ฉด ๋ค์ ๋ฌ ๋ฐ์ดํฐ๋ฅผ ์ ๋ฑ๋์ง) ๋จ์ ํ ์คํธ(Unit Test)๋ฅผ ์ง๊ธฐ๊ฐ ๋ฐฑ๋ง ๋ฐฐ๋ ์ฝ๊ณ ๋น ๋ฆ ๋๋ค. UI ๋ณ๋ชฉ ์๋ ์์ ์ฝ์ด์ ํ์์ ๋๋ค!