๐Ÿช 14. Headless ์ปดํฌ๋„ŒํŠธ (Custom Hooks์˜ ๋ํŒ์™•)

2026๋…„ 4์›” 30์ผ ์ˆ˜์ •๋จ

๐Ÿ“‹ ๊ฐœ์š”

UI๋Š” ๋‹จ 1px๋„ ๊ทธ๋ฆฌ์ง€ ์•Š๊ณ , ์˜ค์ง '๊ธฐ๋Šฅ๊ณผ ์ƒํƒœ(๋‡Œ)'๋งŒ ์บก์Аํ™”ํ•˜์—ฌ ๋””์ž์ธ๊ณผ ๋กœ์ง์„ ์™„๋ฒฝํ•˜๊ฒŒ ๋ถ„๋‹จ์‹œํ‚ค๋Š” ์ตœ์‹  ํŠธ๋ Œ๋“œ Headless ํŒจํ„ด์˜ ์ •์ˆ˜๋ฅผ ๋ฐฐ์›๋‹ˆ๋‹ค.

๐ŸŽฏ ์ด ์„น์…˜์„ ์ฝ๊ณ  ๋‚˜๋ฉด:

  • '๋ชฉ ์—†๋Š”(Headless) ์ปดํฌ๋„ŒํŠธ'๋ž€ ๋‹จ์–ด๊ฐ€ ์˜๋ฏธํ•˜๋Š” ์ง„์งœ ์ •์ฒด(Custom Hooks์˜ ์ง„ํ™”ํ˜•)๋ฅผ ์™„๋ฒฝํžˆ ์ดํ•ดํ•œ๋‹ค.
  • ๋””์ž์ธ ์š”๊ตฌ์‚ฌํ•ญ์ด ํฌ๊ฒŒ ๋‹ฌ๋ผ์ ธ๋„ ๋กœ์ง ์ฝ”๋“œ๋ฅผ ํ”๋“ค์ง€ ์•Š๋Š” ๋ถ„๋ฆฌ(Separation) ์„ค๊ณ„๋ฅผ ํ•  ์ˆ˜ ์žˆ๋‹ค.
  • ์‚ฌ๋‚ด ๊ณตํ†ต ํ›…(Hooks)์„ ๋ฆฌ๋ทฐํ•  ๋•Œ UI ์ฑ…์ž„๊ณผ ๋กœ์ง ์ฑ…์ž„์„ ๊ตฌ๋ถ„ํ•  ์ˆ˜ ์žˆ๋‹ค.

๐Ÿ“‹ ๋ชฉ์ฐจ


๐Ÿ“Œ ์ด ๋ฌธ์„œ๋ฅผ ์ฝ๊ธฐ ์ „์—

โฑ๏ธ ์˜ˆ์ƒ ์ฝ๊ธฐ ์‹œ๊ฐ„: 10๋ถ„ / ํ•ต์‹ฌ ํŒŒํŠธ: 6๋ถ„

๐Ÿ—บ๏ธ ์ด ๋ฌธ์„œ์˜ ๋ฐฐ๊ฒฝ ์„ธ๊ณ„๊ด€: '์˜์ˆ˜๋„ค ์ปค๋ฎค๋‹ˆํ‹ฐ'

  • ์˜์ˆ™(๋””์ž์ด๋„ˆ): "์˜์ฒ ์”จ, ๊ฒฐ์ œ ํƒ€์ด๋จธ ๋””์ž์ธ์„ ํ™”๋ฉด๋งˆ๋‹ค ๋‹ค๋ฅด๊ฒŒ ์“ฐ๊ณ  ์‹ถ์–ด์š”. ๋ชจ๋ฐ”์ผ์—์„œ๋Š” ์›ํ˜• ์ง„ํ–‰๋ฅ , ๋ฉ”์ธ์—์„œ๋Š” ํฐ ์ˆซ์ž, ํ—ค๋”์—์„œ๋Š” ์ž‘์€ ํ…์ŠคํŠธ๋กœ ๋ณด์—ฌ์ฃผ๊ณ  ์‹ถ์–ด์š”."
  • ์˜์ฒ (์‹ ์ž…): "ํƒ€์ด๋จธ ๋กœ์ง์ด๋ž‘ CSS๊ฐ€ ํ•œ ์ปดํฌ๋„ŒํŠธ์— ๋ฌถ์—ฌ ์žˆ์–ด์„œ ํ”„๋กญ์„ ๊ณ„์† ๋Š˜๋ ค์•ผ ํ•  ๊ฒƒ ๊ฐ™์•„์š”. ์ด ๋ฐฉ์‹์ด๋ฉด ๋‹ค์Œ ๊ฐœํŽธ ๋•Œ๋„ ๊ฐ™์€ ๋ฌธ์ œ๊ฐ€ ๋ฐ˜๋ณต๋˜๊ฒ ๋„ค์š”."
  • ์˜ํ˜ธ(๋ฆฌ๋“œ): "์ข‹์€ ์ง„๋‹จ์ด์—์š”. ํ•ต์‹ฌ ๋กœ์ง์€ Custom Hook์œผ๋กœ ๋นผ๊ณ , UI๋Š” ์‚ฌ์šฉํ•˜๋Š” ์ชฝ์—์„œ ์กฐ๋ฆฝํ•˜๊ฒŒ ๋งŒ๋“ญ์‹œ๋‹ค. ๊ทธ๊ฒŒ ํ—ค๋“œ๋ฆฌ์Šค(Headless) ํŒจํ„ด์˜ ํ•ต์‹ฌ์ด์—์š”."

๐Ÿค” ์™œ ์•Œ์•„์•ผ ํ•˜๋Š”๊ฐ€: UI์™€ ๋กœ์ง์ด ํ•œ ๋ชธ์ผ ๋•Œ์˜ ๋ถˆ์น˜๋ณ‘

13๊ฐ•์—์„œ ๋ฐฐ์šด ์ปดํŒŒ์šด๋“œ ์ปดํฌ๋„ŒํŠธ๋Š” ํ›Œ๋ฅญํ•˜์ง€๋งŒ, ์–ด์จŒ๋“  <div> ๊ป๋ฐ๊ธฐ ๊ฐ™์€ ๊ธฐ๋ณธ ๋ ˆ์ด์•„์›ƒ๊ณผ DOM ๋ผˆ๋Œ€๋ฅผ ๋งˆ๋” ์ปดํฌ๋„ŒํŠธ๊ฐ€ ์–ด๋А ์ •๋„ ๋“ค๊ณ  ์žˆ์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.
ํ•˜์ง€๋งŒ ํ˜„์—…์—์„œ๋Š” "๋™์ž‘(์ฒดํฌ๋ฐ•์Šค ๊ธฐ๋Šฅ, ๋“œ๋ž˜๊ทธ ๋“œ๋กญ, ํƒ€์ด๋จธ ๋“ฑ)์€ ๋˜‘๊ฐ™์€๋ฐ ํ™”๋ฉด ํ‘œํ˜„์€ ์ œํ’ˆ ๋งฅ๋ฝ๋งˆ๋‹ค ๋‹ฌ๋ผ์ง€๋Š”" ์š”๊ตฌ๊ฐ€ ์ž์ฃผ ๋‚˜์˜ต๋‹ˆ๋‹ค.

๐Ÿค” ์ž ๊น, ๋จผ์ € ์ƒ๊ฐํ•ด๋ด
"๋น„๋ฐ€๋ฒˆํ˜ธ ๊ทœ์น™ ๊ฒ€์ฆ ํผ"์ด ์žˆ๋‹ค๊ณ  ์ณ๋ณด์ž.
ํšŒ์›๊ฐ€์ž… ํŽ˜์ด์ง€์—์„  ๊ฑฐ๋Œ€ํ•œ ํŒ์—… ๋ฐ•์Šค ์•ˆ์˜ ๋นจ๊ฐ„ ๊ธ€์”จ๋กœ ๊ฒฝ๊ณ ๋ฅผ ๊ทธ๋ฆฌ์ง€๋งŒ, ๋งˆ์ดํŽ˜์ด์ง€ ๋น„๋ฐ€๋ฒˆํ˜ธ ์ˆ˜์ •์ฐฝ์—์„œ๋Š” input ๋ฐ•์Šค ๋ฐ”๋กœ ์•„๋ž˜ ์•„์ฃผ ์ž‘์€ โœ“ ์•„์ด์ฝ˜์œผ๋กœ ํ‘œํ˜„ํ•ด์•ผ ํ•ด.
๋‘˜ ๋‹ค **"๋น„๋ฐ€๋ฒˆํ˜ธ๊ฐ€ 8์ž ์ด์ƒ์ด๊ณ  ํŠน์ˆ˜๋ฌธ์ž๊ฐ€ ํฌํ•จ๋˜์—ˆ๋Š”์ง€ ์ฒดํฌํ•˜๋Š” ๋‡Œ(Logic)"**๋Š” ๊ฐ™์ž–์•„? ์–ด๋–ป๊ฒŒ ๊ทธ ๋‡Œ๋งŒ ์™ ๋นผ๋จน์ง€?


๐Ÿ—๏ธ ๋น„์œ ๋กœ ๋จผ์ € ์ดํ•ดํ•˜๊ธฐ

๐Ÿง’ 5์‚ด์—๊ฒŒ ์„ค๋ช…ํ•œ๋‹ค๋ฉด?

  1. ์˜์ฒ ์ด๋Š” ๋กœ๋ด‡ ๊ณต์ž‘์†Œ ์‚ฌ์žฅ๋‹˜์ด์•ผ. "๊ฑธ์–ด ๋‹ค๋‹ˆ๋Š” ๋กœ๋ด‡"์„ ์ฃผ๋ฌธ๋ฐ›์œผ๋ฉด ์‡ณ๋ฉ์–ด๋ฆฌ๋กœ ๋ชธ์ฒด๋„ ๋‹ค ๊นŽ๊ณ  ๋ชจํ„ฐ๋„ ๋‹ฌ์•„์„œ ์™„์„ฑ๋œ ์€์ƒ‰ ๋กœ๋ด‡์„ ๊ณ ๊ฐ์—๊ฒŒ ๊ฑด๋„ค์คฌ์–ด(์ผ๋ฐ˜ ์ปดํฌ๋„ŒํŠธ).
  2. ๊ณ ๊ฐ(์˜์ˆ™) ์™ˆ: "์‚ฌ์žฅ๋‹˜, ๋‹ค์Œ ์ฃผ๋ฌธ์š”! ์ด๋ฒˆ์—” ๊ฑท๋Š” ์ธํ˜•์ธ๋ฐ... ์ฒซ ๋ฒˆ์งธ๋Š” ํฌ๊ทผํ•œ ํ…Œ๋งˆ, ๋‘ ๋ฒˆ์งธ๋Š” ํˆฌ๋ช…ํ•œ ์œ ๋ฆฌ ํ…Œ๋งˆ, ์„ธ ๋ฒˆ์งธ๋Š” SF ๋А๋‚Œ์˜ ์ปค์Šคํ…€ ํ…Œ๋งˆ๋กœ ๋งŒ๋“ค์–ด์ฃผ์„ธ์š”!"
  3. ์˜์ฒ ์ด๋Š” ๋งค๋ฒˆ ์ƒˆ ์™ธํ˜•์„ ํ†ต์งธ๋กœ ๋‹ค์‹œ ๋งŒ๋“ค๋‹ค ๋ณด๋‹ˆ ์ ์  ์ง€์ณ๊ฐ”์–ด.
  4. ์ด๋•Œ ์˜ํ˜ธ ๋“ฑ์žฅ: "์‚ฌ์žฅ๋‹˜, ๊ทธ ๋ฐฉ์‹์€ ์œ„ํ—˜ํ•ด์š”. ๊ทธ ๊ธฐ๊ณ„ ๋‡Œ(CPU ์นฉ)์™€ ๋ชจํ„ฐ ๊ด€์ ˆ ๋ผˆ๋Œ€๋งŒ(Headless) ๋”ฑ ์กฐ๋ฆฝํ•ด์„œ ๋ด‰ํˆฌ์— ๋‹ด์•„ ๋„˜๊ธฐ์„ธ์š”. ๊ฒ‰ ์Šคํ‚จ, ์ƒ‰์ƒ, ๋ฐฐ์น˜ ๊ฐ™์€ ์–ผ๊ตด(Head / UI)์€ ๊ณ ๊ฐ์ด ์ง์ ‘ ๊ทธ ๋ผˆ๋Œ€ ์œ„์— ์ž…ํžˆ๋„๋ก ๋ง์ด์ฃ !"

์ด๊ฒƒ์ด Head(์™ธํ˜• UI/DOM ์š”์†Œ)๋ฅผ ๋‚ ๋ ค๋ฒ„๋ฆฐ ๋ฌดํ˜•์˜ ๋กœ์ง ์ฝ”์–ด ์žฅ์น˜, Headless(ํ—ค๋“œ๋ฆฌ์Šค) ํŒจํ„ด์ž…๋‹ˆ๋‹ค. React ์ƒํƒœ๊ณ„์—์„  ์ฃผ๋กœ Custom Hook ์ด๋ผ๋Š” ์™„๋ฒฝํ•œ ์ˆ˜๋‹จ์œผ๋กœ ํ˜•ํƒœ๋ฅผ ๋ ๊ฒŒ ๋ฉ๋‹ˆ๋‹ค.


๐Ÿงฉ ๊ทน์˜ ์ฒด๋“: ์‹ค์ „ ํƒ€์ด๋จธ ํ—ค๋“œ๋ฆฌ์Šค ํ›… ์ด์‹ ์ˆ˜์ˆ 

ํƒ€์ด๋จธ ์ปดํฌ๋„ŒํŠธ๋ฅผ ์˜ˆ๋กœ ๋“ค์–ด, UI์™€ ๋กœ์ง์„ ๋‹จ๊ณ„์ ์œผ๋กœ ๋ถ„๋ฆฌํ•ด ๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค.

โŒ 1๋‹จ๊ณ„: UI์™€ ๋กœ์ง์ด ๊ฐ•ํ•˜๊ฒŒ ๊ฒฐํ•ฉ๋œ ์›๋ณธ

// ๋‡Œ์™€ ์–ผ๊ตด์ด ํ•œ ๋ฉ์–ด๋ฆฌ๋กœ ์œ ์ฐฉ๋œ ์ปดํฌ๋„ŒํŠธ (๋””์ž์ธ ๋ณ€๊ฒฝ ๋ถˆ๊ฐ€!)
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)

๋จผ์ € ํ™”๋ฉด์„ ๊ทธ๋ฆฌ๋Š” JSX๋ฅผ ๊ฑท์–ด๋‚ด๊ณ , ํƒ€์ด๋จธ ์ƒํƒœ์™€ ์กฐ์ž‘ ํ•จ์ˆ˜๋งŒ ๋ฐ˜ํ™˜ํ•˜๋Š” ํ›…์„ ๋งŒ๋“ ๋‹ค.

// ๐ŸŽฏ Headless Hook: ์˜ค์ง ๊ธฐ๋Šฅ(ํƒ€์ด๋จธ)๊ณผ ์ƒํƒœ(๋‚จ์€ ์ดˆ)๋งŒ ๊ด€๋ฆฌํ•˜๋Š” ์ˆœ์ˆ˜ ๋‡Œ
export function useCountdownTimer(initialSeconds) {
  const [timeLeft, setTimeLeft] = useState(initialSeconds);
  const isDanger = timeLeft <= 5;
 
  // setInterval ๋กœ์ง (์•„๊นŒ๋ž‘ ๊ฐ™์Œ)
  useEffect(() => { ... }, []);
 
  const resetTimer = () => setTimeLeft(initialSeconds);
 
  // UI(div, span)๋Š” ๋ฆฌํ„ดํ•˜์ง€ ์•Š๋Š”๋‹ค.
  // ๊ณ„์‚ฐ๋œ ์ƒํƒœ์™€ ์กฐ์ž‘ ํ•จ์ˆ˜๋งŒ ๋„˜๊ฒจ์„œ ํ™”๋ฉด์€ ํ˜ธ์ถœ์ž๊ฐ€ ๊ฒฐ์ •ํ•˜๊ฒŒ ํ•œ๋‹ค.
  return {
    timeLeft,
    isDanger,
    resetTimer
  };
}

โœ… 3๋‹จ๊ณ„: ๊ฐ ํ™”๋ฉด์ด ์›ํ•˜๋Š” UI๋กœ ์กฐ๋ฆฝํ•˜๊ธฐ

์ด์ œ ๊ฐ ํ™”๋ฉด์€ ๊ฐ™์€ useCountdownTimer ๋กœ์ง์„ ๊ฐ€์ ธ์˜ค๋˜, ์ž์‹ ์—๊ฒŒ ๋งž๋Š” ๋งˆํฌ์—…๊ณผ ์Šคํƒ€์ผ์„ ์„ ํƒํ•œ๋‹ค.

// UI 1: ๊ด€๋ฆฌ์ž์šฉ ํ…์ŠคํŠธ ํƒ€์ด๋จธ
function AdminTextTimer() {
  const { timeLeft, resetTimer } = useCountdownTimer(60);
 
  return (
    <div>
      <p>[๊ด€๋ฆฌ์ž ๊ฒฝ๊ณ ] ์„ธ์…˜ ๋งŒ๋ฃŒ๊นŒ์ง€ {timeLeft}s ๋‚จ์Œ.</p>
      <button onClick={resetTimer}>์—ฐ์žฅํ•˜๊ธฐ</button>
    </div>
  );
}
 
// UI 2: ๋ชจ๋ฐ”์ผ์šฉ ์›ํ˜• ํƒ€์ด๋จธ
function MagicBubbleTimer() {
  const { timeLeft, isDanger, resetTimer } = useCountdownTimer(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 ๊ฐ™์€ ํ‘œํ˜„์šฉ ํ”„๋กญ์„ ๊ณ„์† ์ถ”๊ฐ€ํ•˜์ง€ ์•Š์•„๋„ ๋œ๋‹ค. ํƒ€์ด๋จธ ๊ทœ์น™์€ ํ›…์ด ์ฑ…์ž„์ง€๊ณ , ํ™”๋ฉด์˜ ๊ตฌ์กฐ์™€ ์Šคํƒ€์ผ์€ ํ˜ธ์ถœ์ž๊ฐ€ ์ฑ…์ž„์ง„๋‹ค. ์ด ๋ถ„๋ฆฌ๊ฐ€ ํ—ค๋“œ๋ฆฌ์Šค ํŒจํ„ด์˜ ์‹ค๋ฌด ๊ฐ€์น˜๋‹ค.


๐ŸŒŸ (์‹ฌํ™” ๋ณด๋„ˆ์Šค) ์†์„ฑ ์ฃผ์ž…๊ธฐ(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>}
    </>
  );
}

์ด๊ฒŒ ์™œ ์œ ์šฉํ•œ๊ฐ€?: UI๋ฅผ ๋งŒ๋“œ๋Š” ์‚ฌ๋žŒ์€ ๋งˆํฌ์—…๊ณผ ์Šคํƒ€์ผ์— ์ง‘์ค‘ํ•  ์ˆ˜ ์žˆ๊ณ , ํ›… ์„ค๊ณ„์ž๋Š” ์ ‘๊ทผ์„ฑ ์†์„ฑ, ํ‚ค๋ณด๋“œ ์ด๋ฒคํŠธ, ํด๋ฆญ ํ•ธ๋“ค๋Ÿฌ๋ฅผ getButtonProps ์•ˆ์— ํ•จ๊ป˜ ์ œ๊ณตํ•  ์ˆ˜ ์žˆ๋‹ค. ์—ญํ• ์ด ๋‚˜๋‰˜๋ฉด์„œ๋„ ํ•„์ˆ˜ ๋™์ž‘์„ ๋น ๋œจ๋ฆด ๊ฐ€๋Šฅ์„ฑ์ด ์ค„์–ด๋“ ๋‹ค.


๐Ÿ ์ด๋ฒˆ์— ๋ฐฐ์šด ๋‚ด์šฉ ์ด์ •๋ฆฌ

๊ด€์ ์ผ๋ฐ˜ UI ์ปดํฌ๋„ŒํŠธ (<Modal /> ๋“ฑ)Headless (Custom Hook)
ํ˜•์ฒด(DOM ์š”์†Œ)div, button ๋“ฑ์„ ๋ฑ‰์–ด๋ƒ„ (UI ๊ท€์†)์ˆœ์ˆ˜ JavaScript ๊ฐ์ฒด๋‚˜ ํ•จ์ˆ˜๋งŒ ๋ฑ‰์Œ (return {})
๋””์ž์ธ ์ž์œ ๋„ํ•œ์ •์  (Props๋กœ ํ—ˆ๋ฝ๋ฐ›์€ ๊ฒƒ๋งŒ ๋ฐ”๊ฟˆ)๋ฌดํ•œ๋Œ€ (Infinity). ๋””์ž์ธ ํƒœ๊ทธ๋ฅผ ์งœ๋Š” ์‚ฌ๋žŒ ๋ง˜๋Œ€๋กœ ํฌ์žฅ์ง€ ๊ฐˆ์•„๋ผ์›€.
์ถ”์ฒœ ์‚ฌ์šฉ ๋„๋ฉ”์ธ์‚ฌ๋‚ด ๊ณ ์ • ๋ฒ„ํŠผ, ๋ฒ”์šฉ ๋ชจ๋‹ฌ์ฐฝ, ์Šค์ผˆ๋ ˆํ†ค๋‹ฌ๋ ฅ(Datepicker) ๋กœ์ง, ๋“œ๋ž˜๊ทธ ๋“œ๋กญ ์ฒ˜๋ฆฌ, ๋กœ์ปฌ์Šคํ† ๋ฆฌ์ง€ ๋™๊ธฐํ™” ํผ, ์ฝค๋ณด๋ฐ•์Šค ์ƒํƒœ ๋“ฑ (๋กœ์ง์€ ๋ณต์žกํ•œ๋ฐ ๋ทฐ๋Š” ํŒŒํŽธํ™”๋œ ๊ฒƒ๋“ค)

๐Ÿ’ก ํ•œ ์ค„๋กœ ๊ธฐ์–ตํ•˜๊ธฐ
"๋ณด์ด๋Š” ๊ฒƒ(UI)๊ณผ ๋ณด์ด์ง€ ์•Š๋Š” ์ƒ๊ฐ(Logic)์˜ ๊ฒฐํ•ฉ์„ ๋ถ„์‡„ํ•˜๋ผ."
์—ฌ๋Ÿฌ ํ™˜๊ฒฝ์—์„œ ๋ชจ์–‘๋งŒ ๋‹ค๋ฅด๊ณ  ๋˜‘๊ฐ™์€ ๊ธฐ๋Šฅ์„ ๋ฐ˜๋ณตํ•ด์„œ ์งœ๊ณ  ์žˆ๋‹ค๋ฉด, ๋‹น์‹ ์ด ์ง€๊ธˆ ๋‹น์žฅ ๋ฝ‘์•„๋‚ด์„œ ๊ฒฉ๋ฆฌํ•ด์•ผ ํ•  ๊ฒƒ์€ UI ์กฐ๊ฐ์ด ์•„๋‹ˆ๋ผ 'Headless Custom Hook' ์นฉ์…‹ ์„ค๊ณ„๋„๋‹ค.

๐Ÿ“ ๋งˆ๋ฌด๋ฆฌ ํ€ด์ฆˆ

Q1. useWindowSize (๋ธŒ๋ผ์šฐ์ € ๊ฐ€๋กœ ์„ธ๋กœ ํ”ฝ์…€์„ ์žก๊ณ  ๋ฆฌ์‚ฌ์ด์ฆˆ๋ฅผ ์ถ”์ ํ•˜๋Š” ํ›…)๋‚˜ ์šฐ๋ฆฌ๊ฐ€ ๋ฐฐ์šด useCountdownTimer ๊ฐ™์€ ๋กœ์ง๋“ค์„ ๊ตณ์ด React ์ผ๋ฐ˜ ์ปดํฌ๋„ŒํŠธ(DOM ์š”์†Œ ๋ฆฌํ„ด)๋กœ ๋งŒ๋“ค์ง€ ์•Š๊ณ  'Headless ํ›…'์œผ๋กœ ๋ถ„๋ฆฌํ•ด์•ผ ํ•˜๋Š” ๊ฐ€์žฅ ๊ฒฐ์ •์ ์ธ ์ด์œ ๋Š” ๋ฌด์—‡์ธ๊ฐ€์š”?

  • A) ํด๋ž˜์Šค ๊ธฐ๋ฐ˜ React ์ƒํƒœ๊ณ„์˜ ์ž”์žฌ๋ฅผ ์ง€์šฐ๊ธฐ ์œ„ํ•ด.
  • B) ์ด ๋กœ์ง๋“ค ์ž์ฒด๋Š” ๋ˆˆ์— ๋ณด์ด๋Š” '๊ฐ€์‹œ์  ํ˜•ํƒœ(๋””์ž์ธ)'๋ฅผ ๋จ ํ•„์š”๊ฐ€ ์—†์œผ๋ฉฐ, ์–ด๋А ๊ตฌ์„์˜ ๋ถ€๋ชจ ์ปจํ…Œ์ด๋„ˆ๋“ , ๊ผฌํˆฌ๋ฆฌ ๋ง๋‹จ ํ…์ŠคํŠธ ํƒœ๊ทธ๋“  ์ž์œ ์ž์žฌ๋กœ ๊ทธ ๊ณ„์‚ฐ๋œ '๋ฐ์ดํ„ฐ ๊ฐ’'๋งŒ ์ฃผ์ž…๋ฐ›์•„์„œ ์ž๊ธฐ ์˜ท์— ๋งž์ถฐ ์ž…์–ด์•ผ ํ•˜๋Š” ์ˆœ์ˆ˜ ์ข…์†์„ฑ ์—†๋Š” ๋น„์ฆˆ๋‹ˆ์Šค/ํ™˜๊ฒฝ ํ•จ์ˆ˜์ด๊ธฐ ๋•Œ๋ฌธ.
  • C) Headless ํ›…์€ ๋นŒ๋“œ ๊ณผ์ •์—์„œ ์„œ๋ฒ„๋ฆฌ์Šค(Serverless) ํด๋ผ์šฐ๋“œ ํ™˜๊ฒฝ์œผ๋กœ ์ปดํŒŒ์ผ๋˜์–ด ์—ฐ์‚ฐ ์†๋„๊ฐ€ ๋ฌดํ•œ์— ์ˆ˜๋ ดํ•˜๊ธฐ ๋•Œ๋ฌธ.

โœ… ์ •๋‹ต: B

๐Ÿ’ก ์ƒ์„ธ ํ•ด์„ค: useWindowSize๋ฅผ ์ปดํฌ๋„ŒํŠธ <WindowSizeReporter>๋กœ ๋งŒ๋“ค๊ณ  ๊ทธ ๊ฒฐ๊ณผ๋ฅผ ๋‹ค์‹œ props๋กœ ๋‚ด๋ ค๋ณด๋‚ด๋ฉด, ํ™”๋ฉด ๊ตฌ์กฐ๊ฐ€ ๋ฐ”๋€” ๋•Œ๋งˆ๋‹ค ๋ฐ์ดํ„ฐ ์ „๋‹ฌ ๊ฒฝ๋กœ๋„ ํ”๋“ค๋ฆฝ๋‹ˆ๋‹ค. ํ›…์œผ๋กœ ๋งŒ๋“ค๋ฉด { width, height } ๋ฐ์ดํ„ฐ๋งŒ ํ•„์š”ํ•œ ์ปดํฌ๋„ŒํŠธ๊ฐ€ ์ง์ ‘ ๊ฐ€์ ธ๋‹ค ์“ธ ์ˆ˜ ์žˆ์–ด UI ๊ตฌ์กฐ์™€ ํ™˜๊ฒฝ ๋กœ์ง์˜ ๊ฒฐํ•ฉ๋„๊ฐ€ ๋‚ฎ์•„์ง‘๋‹ˆ๋‹ค.

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)์˜ ์ž‘๋™ ์ฝ”์–ด ๋‡Œ๋ฅผ ๋ถ„๋ฆฌํ•ด๋‘์—ˆ์„ ๋•Œ, ๋‹ค์Œ ์ค‘ ์ด '๋ถ„๋ฆฌ ์•„ํ‚คํ…์ฒ˜' ๋•๋ถ„์— ํšŒ์‚ฌ ์ „์ฒด์ ์œผ๋กœ ์–ป์„ ์ˆ˜ ์žˆ๋Š” ์ด๋“์„ ๋ชจ๋‘ ์„œ์ˆ ํ•ด ๋ณด์„ธ์š”.

โœ… ์ •๋‹ต ๋ฐ ์ฃผ๊ด€์‹ ํ•ด์„ค:

  1. ์ฝ”๋“œ ์žฌ์‚ฌ์šฉ์˜ ํ™•์žฅ: ํ•œ ๋ฒˆ ์ž˜ ์งœ๋‘” Headless Hook์˜ ๋‹ฌ๋ ฅ ๊ณ„์‚ฐ ํ•จ์ˆ˜์™€ ์›” ์ด๋™ ๋กœ์ง์€ PC ์›น ๋‹ฌ๋ ฅ์—๋„, UI ์ฒด๊ณ„๊ฐ€ ๋‹ค๋ฅธ ๋ชจ๋ฐ”์ผ ํ™”๋ฉด์—๋„ ์žฌ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ํ‘œํ˜„ ๊ณ„์ธต์ด ๋ฐ”๋€Œ์–ด๋„ ๋‚ ์งœ ๊ณ„์‚ฐ ๋กœ์ง์€ ๊ทธ๋Œ€๋กœ ๊ฒ€์ฆ๋œ ์ฝ”์–ด๋กœ ๋‚จ์Šต๋‹ˆ๋‹ค.
  2. ํ…Œ์ŠคํŠธ ๊ณ ๋„ํ™”์˜ ์‰ฌ์›€: UI(๋ฒ„ํŠผ ์ƒ‰์ƒ ๊น”๋ฆฌ๊ณ  ์• ๋‹ˆ๋ฉ”์ด์…˜ ๋Œ๊ณ ..)๋ฅผ ํ…Œ์ŠคํŠธํ•˜๋ ค๋ฉด ๋ณต์žกํ•œ ํ…Œ์ŠคํŒ… ๋„๊ตฌ๊ฐ€ ํ•„์š”ํ•˜์ง€๋งŒ, Headless Hook์€ ๋ณธ์งˆ์ด ์ˆœ์ˆ˜ ๋กœ์ง์ด๋ฏ€๋กœ (๊ฐ’์„ ๋„ฃ์–ด์ฃผ๋ฉด ๋‹ค์Œ ๋‹ฌ ๋ฐ์ดํ„ฐ๋ฅผ ์ž˜ ๋ฑ‰๋Š”์ง€) ๋‹จ์œ„ ํ…Œ์ŠคํŠธ(Unit Test)๋ฅผ ์งœ๊ธฐ๊ฐ€ ๋ฐฑ๋งŒ ๋ฐฐ๋Š” ์‰ฝ๊ณ  ๋น ๋ฆ…๋‹ˆ๋‹ค. UI ๋ณ‘๋ชฉ ์—†๋Š” ์ˆœ์ˆ˜ ์ฝ”์–ด์˜ ํƒ„์ƒ์ž…๋‹ˆ๋‹ค!

๐Ÿฃ ์˜์ฒ ์ด์˜ ํ‡ด๊ทผ ์ผ๊ธฐ

๋””์ž์ธ ๋ฐ”๋€” ๋•Œ๋งˆ๋‹ค ์‡ณ๋ฉ์ด ๋กœ๋ด‡ ์™ธํ”ผ๋งŒ ํ•˜์—ผ์—†์ด ๊ฐˆ์•„์น˜์šฐ๋˜ ๋‚˜๋‚ ๋“ค์ด ๋– ์˜ค๋ฅธ๋‹ค. ๋กœ์ง๊ณผ UI๋ฅผ ๋ถ„๋ฆฌํ•œ๋‹ค๋Š” ๋น„์œ ๊ฐ€ ์˜ค๋Š˜ ๋‚ด์šฉ์˜ ํ•ต์‹ฌ์„ ์ž˜ ์žก์•„์คฌ๋‹ค.

๐Ÿ’ก "UI ๊ป๋ฐ๊ธฐ๋ฅผ ๋‚ ๋ ค๋ผ(Headless). ์˜ค์ง ์ˆœ์ˆ˜ํ•œ ๊ธฐ๋Šฅ๊ณผ ๋ฆฌ๋ชจ์ปจ(Hook)๋งŒ ๋ฑ‰์–ด๋‚ด๊ณ , ๊ทธ ์˜ท ์ž…ํžˆ๋Š” ๊ฑด ์ฐฐํ™ ์žฅ์ธ(์‚ฌ์šฉ์ž ์ปดํฌ๋„ŒํŠธ)์—๊ฒŒ ์™„๋ฒฝํžˆ ์œ„์ž„ํ•˜๋ผ."

Prop Getters ๊ธฐ๋ฒ• ์“ด ๊ฑด ์™„์ „ํžˆ ์ถฉ๊ฒฉ์ ์ด์—ˆ๋‹ค. ๋‚ด๊ฐ€ ์ปดํฌ๋„ŒํŠธ์˜ ๋‡Œ๋ฅผ ์งค ๋•Œ, ๋‚จ๋“ค์ด ํด๋ฆญ ํ•ธ๋“ค๋Ÿฌ๋‚˜ ์ ‘๊ทผ์„ฑ ๊ณ ๋ ค ๋ชปํ•  ๊ฒƒ๊นŒ์ง€ ๋‹ค ์žฅ์ฐฉํ•œ ์˜ต์…˜ ๋ญ‰ํ……์ด๋ฅผ {...getSlotProps()}๋กœ ํ•œ ๋ฐฉ์— ์ด์ค€๋‹ค๊ณ ? ์ด๊ฑด ๊ฑฐ์˜ ์˜ˆ์ˆ ์˜ ๊ฒฝ์ง€๋‹ค. ๋“œ๋””์–ด ๋ฆฌ์•กํŠธ ์„ค๊ณ„์˜ ์ •์ ์„ ๋ดค๋‹ค๋Š” ์ฐํ•œ ์„ฑ์ทจ๊ฐ์ด ๋ชฐ๋ ค์˜จ๋‹ค. ์˜ํ˜ธ ์„ ๋ฐฐ ์ตœ๊ณ !