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

๐Ÿ“‹ ๊ฐœ์š”

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

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

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

๐Ÿ“‹ ๋ชฉ์ฐจ


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

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

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

  • ์˜์ˆ™(๋””์ž์ด๋„ˆ):"์˜์ฒ ์”จ! ์ €๋ฒˆ์— ๋งŒ๋“  ๊ทธ ๊ฒฐ์ œ ํƒ€์ด๋จธ ํƒ€์ด๋จธ์š”. ๋””์ž์ธ ๊ฐœํŽธํ•˜๋ฉด์„œ ๋ชจ๋ฐ”์ผ์—์„œ๋Š” ๋™๊ทธ๋ผ๋ฏธ๋กœ ๋น™๊ธ€๋น™๊ธ€ ๋Œ๋ฉด์„œ ๋‚จ์€ ์ดˆ๊ฐ€ ๋œจ๊ฒŒ ํ•  ๊ฑฐ๊ณ ์š”, ๋ฉ”์ธ์—์„œ๋Š” ํฌ๊ฒŒ ๊ธ€์ž๋กœ, ํ—ค๋”์—์„œ๋Š” ์ž‘๊ฒŒ ํ…์ŠคํŠธ๋กœ๋งŒ ๋„์›Œ์ฃผ์„ธ์š”. ์•„, ๊ธ€์”จ ์ƒ‰๊น”๋„ ๋‚จ์€ ์‹œ๊ฐ„์— ๋”ฐ๋ผ ์•Œ๋ก๋‹ฌ๋กํ•˜๊ฒŒ ๋ฐ”๊ฟ”์ฃผ์‹œ๊ณ ์š”!"
  • ์˜์ฒ (์‹ ์ž…):"...๋„ค? ํƒ€์ด๋จธ ๋กœ์ง์ด๋ž‘ CSS ๋””์ž์ธ์ด๋ž‘ ํ•œ ๋ฉ์–ด๋ฆฌ๋กœ ๊ตณํ˜€๋†จ๋Š”๋ฐ... ์ด๊ฑธ ๋‹ค ๋œฏ์–ด๊ณ ์ณ์•ผ ํ•˜๋‚˜์š”? isMobileCircle, isHeaderSmall ํ”„๋กญ์Šค ๋˜ 500๊ฐœ ๋šซ์–ด์•ผ๊ฒ ๋„ค ์—‰์—‰..."
  • ์˜ํ˜ธ(๋ฆฌ๋“œ): "์˜์ฒ  ๋‹˜, ๊ทธ๋ž˜์„œ CSS๋ž‘ ๋กœ์ง์„ ๋ถ„๋ฆฌํ•˜๋Š” ๊ฒŒ ํ”„๋ก ํŠธ์˜ ์ฒ ํ†ต ์›์น™์ž…๋‹ˆ๋‹ค! ํ•ต์‹ฌ ๋กœ์ง(Raw Logic) ๋งŒ ๋‚จ๊ธฐ๊ณ  ์Šคํƒ€์ผ(UI) ์€ ์‹น ๊ฑท์–ด๋‚ด์„ธ์š”. ์ง„์ •ํ•œ ํ—ค๋“œ๋ฆฌ์Šค(Headless) ๋กœ ๊ฐˆ์•„์—Ž์์‹œ๋‹ค."

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

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

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


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

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

  1. ์˜์ฒ ์ด๋Š” ๋กœ๋ด‡ ๊ณต์ž‘์†Œ ์‚ฌ์žฅ๋‹˜์ด์•ผ. "๊ฑธ์–ด ๋‹ค๋‹ˆ๋Š” ๋กœ๋ด‡"์„ ์ฃผ๋ฌธ๋ฐ›์œผ๋ฉด ์‡ณ๋ฉ์–ด๋ฆฌ๋กœ ๋ชธ์ฒด๋„ ๋‹ค ๊นŽ๊ณ  ๋ชจํ„ฐ๋„ ๋‹ฌ์•„์„œ ์™„์„ฑ๋œ ์€์ƒ‰ ๋กœ๋ด‡์„ ๊ณ ๊ฐ์—๊ฒŒ ๋˜์ ธ์คฌ์–ด(์ผ๋ฐ˜ ์ปดํฌ๋„ŒํŠธ).
  2. ๊ณ ๊ฐ(์˜์ˆ™) ์™ˆ: "์‚ฌ์žฅ๋‹˜, ๋‹ค์Œ ์ฃผ๋ฌธ์š”! ์ด๋ฒˆ์—” ๊ฑท๋Š” ์ธํ˜•์ธ๋ฐ... ์™ธํ”ผ๋Š” ํฌ๊ทผํ•œ ๊ณฐ ์ธํ˜•์œผ๋กœ, ๊ทธ ๋‹ค์Œ ๊ฑด ์œ ๋ฆฌ๋กœ ๋œ ํˆฌ๋ช… ๊ฑด๋‹ด์œผ๋กœ, ๊ทธ ๋‹ค์Œ๊ฑด ์ง•๊ทธ๋Ÿฌ์šด ์ด‰์ˆ˜ ์™ธ๊ณ„์ธ์œผ๋กœ ๋งŒ๋“ค์–ด์ฃผ์„ธ์š”!"
  3. ๋ฉ˜ํƒˆ์ด ๋‚˜๊ฐ„ ์˜์ฒ ์ด. ์‡ณ๋ฉ์ด ์€์ƒ‰ ๋กœ๋ด‡ ๋ชธํ†ต ๊ป์งˆ์„ ์ผ์ผ์ด ๋ถ€์ˆ˜๊ณ  ๋‹ค์‹œ ์™ธํ˜•์„ ์กฐ๊ฐํ•˜๋‹ค ์“ฐ๋Ÿฌ์ง.
  4. ์ด๋•Œ ์˜ํ˜ธ ๋“ฑ์žฅ: "์‚ฌ์žฅ๋‹˜, ๋ฐ”๋ณด์„ธ์š”? ๊ทธ ๊ธฐ๊ณ„ ๋‡Œ(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)์˜ ์ž‘๋™ ์ฝ”์–ด ๋‡Œ๋ฅผ ๋ถ„๋ฆฌํ•ด๋‘์—ˆ์„ ๋•Œ, ๋‹ค์Œ ์ค‘ ์ด '๋ถ„๋ฆฌ ์•„ํ‚คํ…์ฒ˜' ๋•๋ถ„์— ํšŒ์‚ฌ ์ „์ฒด์ ์œผ๋กœ ์–ป์„ ์ˆ˜ ์žˆ๋Š” ์ด๋“์„ ๋ชจ๋‘ ์„œ์ˆ ํ•ด ๋ณด์„ธ์š”.

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

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