(๋ฒˆ์—ญ) ํ—ค๋“œ๋ฆฌ์Šค ์ปดํฌ๋„ŒํŠธ: ๋ฆฌ์•กํŠธ UI๋ฅผ ํ•ฉ์„ฑํ•˜๊ธฐ ์œ„ํ•œ ํŒจํ„ด

์›๋ฌธ: Headless Component: a pattern for composing React UIs

๋ฆฌ์•กํŠธ UI ์ปจํŠธ๋กค์ด ๋” ์ •๊ตํ•ด์ง์— ๋”ฐ๋ผ ๋ณต์žกํ•œ ๋กœ์ง์ด ์‹œ๊ฐ์  ํ‘œํ˜„๊ณผ ์–ฝํžˆ๊ฒŒ ๋  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์ด๋กœ ์ธํ•ด ์ปดํฌ๋„ŒํŠธ์˜ ๋™์ž‘์„ ์ถ”๋ก ํ•˜๊ธฐ ์–ด๋ ต๊ณ , ํ…Œ์ŠคํŠธํ•˜๊ธฐ๋„ ์–ด๋ ค์›Œ์ง€๋ฉฐ, ๋‹ค๋ฅธ ๋ชจ์–‘์ด ํ•„์š”ํ•œ ์œ ์‚ฌํ•œ ์ปดํฌ๋„ŒํŠธ๋ฅผ ๊ตฌ์ถ•ํ•ด์•ผ ํ•  ์ˆ˜๋„ ์žˆ์Šต๋‹ˆ๋‹ค. ํ—ค๋“œ๋ฆฌ์Šค ์ปดํฌ๋„ŒํŠธ๋Š” ๋ชจ๋“  ๋น„์‹œ๊ฐ์ ์ธ ๋กœ์ง๊ณผ ์ƒํƒœ ๊ด€๋ฆฌ๋ฅผ ์ถ”์ถœํ•˜์—ฌ ์ปดํฌ๋„ŒํŠธ์˜ ๋‘๋‡Œ๋ฅผ UI์—์„œ ๋ถ„๋ฆฌํ•ฉ๋‹ˆ๋‹ค.

๋ชฉ์ฐจ

๋ฆฌ์•กํŠธ๋Š” UI ์ปดํฌ๋„ŒํŠธ์™€ UI์˜ ์ƒํƒœ ๊ด€๋ฆฌ์— ๋Œ€ํ•œ ์‚ฌ๊ณ  ๋ฐฉ์‹์„ ํ˜์‹ ์ ์œผ๋กœ ๋ณ€ํ™”์‹œ์ผฐ์Šต๋‹ˆ๋‹ค. ๊ทธ๋Ÿฌ๋‚˜ ์ƒˆ๋กœ์šด ๊ธฐ๋Šฅ ์š”์ฒญ์ด๋‚˜ ๊ฐœ์„ ์ด ์žˆ์„ ๋•Œ๋งˆ๋‹ค, ๋ณด๊ธฐ์—” ๋‹จ์ˆœํ•ด ๋ณด์ด๋Š” ์ปดํฌ๋„ŒํŠธ๋„ ์„œ๋กœ ์–ฝํžŒ ์ƒํƒœ์™€ UI ๋กœ์ง์˜ ๋ณต์žก์ฒด๋กœ ๋น ๋ฅด๊ฒŒ ๋ฐœ์ „ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๊ฐ„๋‹จํ•œ ๋“œ๋กญ๋‹ค์šด ๋ชฉ๋ก์„ ๊ตฌ์ถ•ํ•œ๋‹ค๊ณ  ์ƒ์ƒํ•ด ๋ณด์„ธ์š”. ์ฒ˜์Œ์—๋Š” ์—ด๊ธฐ/๋‹ซ๊ธฐ ์ƒํƒœ๋ฅผ ๊ด€๋ฆฌํ•˜๊ณ  ๋ชจ์–‘์„ ๋””์ž์ธํ•˜๋Š” ๋“ฑ ๊ฐ„๋‹จํ•ด ๋ณด์ž…๋‹ˆ๋‹ค. ํ•˜์ง€๋งŒ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์ด ์„ฑ์žฅํ•˜๊ณ  ๋ฐœ์ „ํ•จ์— ๋”ฐ๋ผ ์ด ๋“œ๋กญ๋‹ค์šด์— ๋Œ€ํ•œ ์š”๊ตฌ ์‚ฌํ•ญ๋„ ๋ฐœ์ „ํ•ฉ๋‹ˆ๋‹ค.

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

์ด๋Ÿฌํ•œ ๊ฐ ๊ณ ๋ ค ์‚ฌํ•ญ์€ ๋“œ๋กญ๋‹ค์šด ์ปดํฌ๋„ŒํŠธ์— ๋ณต์žก์„ฑ์„ ๋”ํ•ฉ๋‹ˆ๋‹ค. ์ƒํƒœ, ๋กœ์ง ๋ฐ UI ํ‘œํ˜„์ด ์„ž์ด๋ฉด ์œ ์ง€ ๊ด€๋ฆฌ๊ฐ€ ์–ด๋ ต๊ณ  ์žฌ์‚ฌ์šฉํ•˜๊ธฐ ์–ด๋ ต์Šต๋‹ˆ๋‹ค. ์„œ๋กœ ์–ฝํ˜€ ์žˆ์„์ˆ˜๋ก ์˜๋„ํ•˜์ง€ ์•Š์€ ๋ถ€์ž‘์šฉ ์—†์ด ๋ณ€๊ฒฝํ•˜๊ธฐ๊ฐ€ ๋” ์–ด๋ ค์›Œ์ง‘๋‹ˆ๋‹ค.

ํ—ค๋“œ๋ฆฌ์Šค ์ปดํฌ๋„ŒํŠธ ํŒจํ„ด ์†Œ๊ฐœ

์ด๋Ÿฌํ•œ ๋ฌธ์ œ๋ฅผ ์ •๋ฉด์œผ๋กœ ๋งˆ์ฃผํ•œ ์ƒํ™ฉ์—์„œ, ํ—ค๋“œ๋ฆฌ์Šค ์ปดํฌ๋„ŒํŠธ ํŒจํ„ด์€ ํ•ด๊ฒฐ์ฑ…์„ ์ œ์‹œํ•ฉ๋‹ˆ๋‹ค. ํ—ค๋“œ๋ฆฌ์Šค ์ปดํฌ๋„ŒํŠธ ํŒจํ„ด์€ ๊ณ„์‚ฐ๊ณผ UI ํ‘œํ˜„์„ ๋ถ„๋ฆฌํ•˜์—ฌ, ๊ฐœ๋ฐœ์ž๊ฐ€ ๋‹ค์žฌ๋‹ค๋Šฅํ•˜๊ณ  ์œ ์ง€ ๊ด€๋ฆฌ๊ฐ€ ๊ฐ€๋Šฅํ•˜๋ฉฐ ์žฌ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•œ ์ปดํฌ๋„ŒํŠธ๋ฅผ ๊ตฌ์ถ•ํ•  ์ˆ˜ ์žˆ๋„๋ก ์ง€์›ํ•ฉ๋‹ˆ๋‹ค.

ํ—ค๋“œ๋ฆฌ์Šค ์ปดํฌ๋„ŒํŠธ๋Š” ๋ฆฌ์•กํŠธ ๋””์ž์ธ ํŒจํ„ด์œผ๋กœ ์ผ๋ฐ˜์ ์œผ๋กœ ๋ฆฌ์•กํŠธ ํ›…์œผ๋กœ ๊ตฌํ˜„๋˜๋ฉฐ, ์ปดํฌ๋„ŒํŠธ๊ฐ€ ํŠน์ • UI(์‚ฌ์šฉ์ž ์ธํ„ฐํŽ˜์ด์Šค)๋ฅผ ๊ทœ์ •ํ•˜์ง€ ์•Š๊ณ , ๋กœ์ง๊ณผ ์ƒํƒœ ๊ด€๋ฆฌ๋งŒ์„ ์ „์ ์œผ๋กœ ์ฑ…์ž„์ง€๋Š” ์ปดํฌ๋„ŒํŠธ์ž…๋‹ˆ๋‹ค. ์ด๋Š” ์ž‘์—…์˜ โ€˜๋‘๋‡Œโ€™๋ฅผ ์ œ๊ณตํ•˜์ง€๋งŒ โ€˜๊ฒ‰๋ชจ์Šตโ€™์€ ๊ตฌํ˜„ํ•˜๋Š” ๊ฐœ๋ฐœ์ž์—๊ฒŒ ๋งก๊น๋‹ˆ๋‹ค. ๋ณธ์งˆ์ ์œผ๋กœ ํŠน์ • ์‹œ๊ฐ์  ํ‘œํ˜„์„ ๊ฐ•์š”ํ•˜์ง€ ์•Š๊ณ  ๊ธฐ๋Šฅ์„ฑ์„ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค.

ํ—ค๋“œ๋ฆฌ์Šค ์ปดํฌ๋„ŒํŠธ๋ฅผ ์‹œ๊ฐํ™”ํ•ด๋ณด์ž๋ฉด, ํ•œ์ชฝ์—์„œ๋Š” JSX ๋ทฐ์™€ ์ƒํ˜ธ์ž‘์šฉํ•˜๊ณ  ๋‹ค๋ฅธ ํ•œ์ชฝ์—์„œ๋Š” ํ•„์š”์— ๋”ฐ๋ผ ๊ธฐ๋ณธ ๋ฐ์ดํ„ฐ ๋ชจ๋ธ๊ณผ ํ†ต์‹ ํ•˜๋Š” ๊ฐ€๋Š๋‹ค๋ž€ ๋ ˆ์ด์–ด๋กœ ๋‚˜ํƒ€๋‚ผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์ด ํŒจํ„ด์€ ์‹œ๊ฐ์  ํ‘œํ˜„์—์„œ ๋กœ์ง์„ ๋ถ„๋ฆฌํ•˜๊ธฐ ๋•Œ๋ฌธ์—, UI์˜ ๋™์ž‘ ๋˜๋Š” ์ƒํƒœ ๊ด€๋ฆฌ ์ธก๋ฉด๋งŒ์„ ์›ํ•˜๋Š” ๊ฐœ๋ฐœ์ž์—๊ฒŒ ํŠนํžˆ ์œ ์šฉํ•ฉ๋‹ˆ๋‹ค.

์‚ฌ์ง„ 1: ํ—ค๋“œ๋ฆฌ์Šค ์ปดํฌ๋„ŒํŠธ ํŒจํ„ด

์‚ฌ์ง„ 1: ํ—ค๋“œ๋ฆฌ์Šค ์ปดํฌ๋„ŒํŠธ ํŒจํ„ด

์˜ˆ๋ฅผ ๋“ค์–ด ํ—ค๋“œ๋ฆฌ์Šค ๋“œ๋กญ๋‹ค์šด ์ปดํฌ๋„ŒํŠธ๋ฅผ ์ƒ๊ฐํ•ด ๋ด…์‹œ๋‹ค. ์ด ์ปดํฌ๋„ŒํŠธ๋Š” ์—ด๊ธฐ/๋‹ซ๊ธฐ ์ƒํƒœ, ํ•ญ๋ชฉ ์„ ํƒ, ํ‚ค๋ณด๋“œ ํƒ์ƒ‰ ๋“ฑ์— ๋Œ€ํ•œ ์ƒํƒœ ๊ด€๋ฆฌ๋ฅผ ์ฒ˜๋ฆฌํ•ฉ๋‹ˆ๋‹ค. ๋ Œ๋”๋งํ•  ๋•Œ๊ฐ€ ๋˜๋ฉด ์ž์ฒด์ ์œผ๋กœ ํ•˜๋“œ์ฝ”๋”ฉ๋œ ๋“œ๋กญ๋‹ค์šด UI๋ฅผ ๋ Œ๋”๋งํ•˜๋Š” ๋Œ€์‹ , ์ด ์ƒํƒœ์™€ ๋กœ์ง์„ ์ž์‹ ํ•จ์ˆ˜๋‚˜ ์ปดํฌ๋„ŒํŠธ์— ์ œ๊ณตํ•˜์—ฌ ๊ฐœ๋ฐœ์ž๊ฐ€ ์‹œ๊ฐ์ ์œผ๋กœ ์–ด๋–ป๊ฒŒ ํ‘œ์‹œํ• ์ง€ ๊ฒฐ์ •ํ•  ์ˆ˜ ์žˆ๋„๋ก ํ•ฉ๋‹ˆ๋‹ค.

์ด ๊ธ€์—์„œ๋Š” ๋ณต์žกํ•œ ์ปดํฌ๋„ŒํŠธ์ธ ๋“œ๋กญ๋‹ค์šด ๋ชฉ๋ก์„ ์ฒ˜์Œ๋ถ€ํ„ฐ ๋‹ค์‹œ ๊ตฌ์„ฑํ•˜์—ฌ ์‹ค์ œ ์˜ˆ์ œ๋ฅผ ์‚ดํŽด๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค. ์ปดํฌ๋„ŒํŠธ์— ๋” ๋งŽ์€ ๊ธฐ๋Šฅ์„ ์ถ”๊ฐ€ํ•˜๋ฉด์„œ ๋ฐœ์ƒํ•˜๋Š” ๋ฌธ์ œ๋ฅผ ๊ด€์ฐฐํ•  ๊ฒƒ์ž…๋‹ˆ๋‹ค. ์ด๋ฅผ ํ†ตํ•ด ํ—ค๋“œ๋ฆฌ์Šค ์ปดํฌ๋„ŒํŠธ ํŒจํ„ด์ด ์–ด๋–ป๊ฒŒ ์ด๋Ÿฌํ•œ ๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐํ•˜๊ณ , ์„œ๋กœ ๋‹ค๋ฅธ ๊ด€์‹ฌ์‚ฌ๋ฅผ ๊ตฌ๋ถ„ํ•˜๋ฉฐ, ๋ณด๋‹ค ๋‹ค์žฌ๋‹ค๋Šฅํ•œ ์ปดํฌ๋„ŒํŠธ๋ฅผ ์ œ์ž‘ํ•˜๋Š” ๋ฐ ๋„์›€์ด ๋˜๋Š”์ง€ ๋ณด์—ฌ๋“œ๋ฆฌ๊ฒ ์Šต๋‹ˆ๋‹ค.

๋“œ๋กญ๋‹ค์šด ๋ชฉ๋ก ๊ตฌํ˜„ํ•˜๊ธฐ

๋“œ๋กญ๋‹ค์šด ๋ชฉ๋ก์€ ๋งŽ์€ ๊ณณ์—์„œ ์‚ฌ์šฉ๋˜๋Š” ์ผ๋ฐ˜์ ์ธ ์ปดํฌ๋„ŒํŠธ์ž…๋‹ˆ๋‹ค. ๊ธฐ๋ณธ์ ์ธ ์‚ฌ์šฉ ์‚ฌ๋ก€๋ฅผ ์œ„ํ•œ ๋„ค์ดํ‹ฐ๋ธŒ select ์ปดํฌ๋„ŒํŠธ๋„ ์žˆ์ง€๋งŒ, ๊ฐ ์˜ต์…˜์„ ๋” ์ž˜ ์ œ์–ดํ•  ์ˆ˜ ์žˆ๋Š” ๊ณ ๊ธ‰ ๋ฒ„์ „์€ ๋” ๋‚˜์€ ์‚ฌ์šฉ์ž ๊ฒฝํ—˜์„ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค.

์‚ฌ์ง„ 2: ๋“œ๋กญ๋‹ค์šด ๋ชฉ๋ก ์ปดํฌ๋„ŒํŠธ

์‚ฌ์ง„ 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: ๋ชฉ๋ก ๋„ค์ดํ‹ฐ๋ธŒ ๊ตฌํ˜„

์‚ฌ์ง„ 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๊ฐ€ ํฌํ•จ๋œ ์„ ์–ธ์  ํ—ค๋“œ๋ฆฌ์Šค ์ปดํฌ๋„ŒํŠธ

์ด ์˜ˆ์‹œ์—์„œ๋Š” ๋ฆฌ์•กํŠธ ์ปจํ…์ŠคํŠธ 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

์‚ฌ์ง„ 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๋กœ ์–ผ๋งˆ๋‚˜ ์‰ฝ๊ฒŒ ์ „ํ™˜ํ•  ์ˆ˜ ์žˆ๋Š”์ง€ ๋ณด์—ฌ๋“œ๋ฆฌ๊ฒ ์Šต๋‹ˆ๋‹ค.

์ƒˆ๋กœ์šด 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: ๋ฐ๋ธŒํˆด

์‚ฌ์ง„ 5: ๋ฐ๋ธŒํˆด

๋ชจ๋“  ๋“œ๋กญ๋‹ค์šด ๋ชฉ๋ก์€ ์™ธํ˜•์— ๊ด€๊ณ„์—†์ด ๋‚ด๋ถ€์ ์œผ๋กœ ์ผ๊ด€๋œ ๋™์ž‘์„ ๊ณต์œ ํ•˜๋ฉฐ, ์ด ๋ชจ๋“  ๋™์ž‘์€ useDropdown ํ›…(ํ—ค๋“œ๋ฆฌ์Šค ์ปดํฌ๋„ŒํŠธ)์— ์บก์Šํ™”๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค. ํ•˜์ง€๋งŒ ์›๊ฒฉ ํ™˜๊ฒฝ์—์„œ ๋ฐ์ดํ„ฐ๋ฅผ ๊ฐ€์ ธ์™€์•ผ ํ•˜๊ฑฐ๋‚˜ ๋น„๋™๊ธฐ ์ƒํƒœ์™€ ๊ฐ™์ด ๋” ๋งŽ์€ ์ƒํƒœ๋ฅผ ๊ด€๋ฆฌํ•ด์•ผ ํ•˜๋Š” ๊ฒฝ์šฐ์—๋Š” ์–ด๋–ป๊ฒŒ ํ•ด์•ผ ํ• ๊นŒ์š”?

์ถ”๊ฐ€ ์ƒํƒœ์— ๋Œ€ํ•ด ๋” ๊นŠ๊ฒŒ ํŒŒ๋ณด๊ธฐ

๋“œ๋กญ๋‹ค์šด ์ปดํฌ๋„ŒํŠธ๋ฅผ ์ง„ํ–‰ํ•˜๋ฉด์„œ ์›๊ฒฉ ๋ฐ์ดํ„ฐ๋ฅผ ์ฒ˜๋ฆฌํ•  ๋•Œ ๋ฐœ์ƒํ•˜๋Š” ์ข€ ๋” ๋ณต์žกํ•œ ์ƒํƒœ๋ฅผ ์‚ดํŽด๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค. ์›๊ฒฉ ์†Œ์Šค์—์„œ ๋ฐ์ดํ„ฐ๋ฅผ ๊ฐ€์ ธ์˜ค๋Š” ์‹œ๋‚˜๋ฆฌ์˜ค์—์„œ๋Š” ๋กœ๋”ฉ, ์˜ค๋ฅ˜ ๋ฐ ๋ฐ์ดํ„ฐ ์ƒํƒœ๋ฅผ ์ฒ˜๋ฆฌํ•ด์•ผ ํ•˜๋Š” ๋“ฑ ๋ช‡ ๊ฐ€์ง€ ์ƒํƒœ๋ฅผ ๋” ๊ด€๋ฆฌํ•ด์•ผ ํ•  ํ•„์š”์„ฑ์ด ์ƒ๊น๋‹ˆ๋‹ค.

์‚ฌ์ง„ 6: ๋‹ค๋ฅธ ์ƒํƒœ

์‚ฌ์ง„ 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์—์„œ ๋ฃจํŠธ ํŒจํ„ด ๋‹ค์‹œ ์‚ดํŽด๋ณด๊ธฐ

์—…๊ณ„์— ์˜ค๋ž˜ ์ข…์‚ฌํ–ˆ๊ฑฐ๋‚˜ ๋ฐ์Šคํฌํ†ฑ ์„ค์ •์—์„œ 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๊ฐ€ ์ง๋ฉดํ•œ ๋˜ ๋‹ค๋ฅธ ์ฃผ์š” ๊ณผ์ œ๋Š” ๋ฐ์ดํ„ฐ ๋™๊ธฐํ™”๋กœ, ํ”„๋ ˆ์  ํ„ฐ ๋˜๋Š” ํ”„๋ ˆ์  ํ…Œ์ด์…˜ ๋ชจ๋ธ์ด ๊ธฐ์ดˆ ๋ฐ์ดํ„ฐ์˜ ๋ณ€๊ฒฝ ์‚ฌํ•ญ์„ ์กฐ์œจํ•˜๊ณ  ๋‹ค๋ฅธ ๋ Œ๋”๋ง ๋ถ€๋ถ„์— ์•Œ๋ ค์•ผ ํ–ˆ์Šต๋‹ˆ๋‹ค. ๋Œ€ํ‘œ์ ์ธ ์˜ˆ๊ฐ€ ์•„๋ž˜ ๊ทธ๋ฆผ๊ณผ ๊ฐ™์Šต๋‹ˆ๋‹ค.

Figure 7: One model has multiple presentations

์‚ฌ์ง„ 7: ํ•˜๋‚˜์˜ ๋ชจ๋ธ์— ์—ฌ๋Ÿฌ ํ”„๋ ˆ์  ํ…Œ์ด์…˜์ด ์žˆ์Šต๋‹ˆ๋‹ค.

์œ„ ๊ทธ๋ฆผ์—์„œ ์„ธ ๊ฐ€์ง€ UI ์ปดํฌ๋„ŒํŠธ(ํ‘œ, ๊บพ์€์„ ํ˜• ์ฐจํŠธ, ํžˆํŠธ๋งต)๋Š” ์™„์ „ํžˆ ๋…๋ฆฝ์ ์ด์ง€๋งŒ ๋ชจ๋‘ ๋™์ผํ•œ ๋ชจ๋ธ ๋ฐ์ดํ„ฐ๋ฅผ ๋ Œ๋”๋งํ•˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค. ํ‘œ์—์„œ ๋ฐ์ดํ„ฐ๋ฅผ ์ˆ˜์ •ํ•˜๋ฉด ๋‹ค๋ฅธ ๋‘ ๊ทธ๋ž˜ํ”„๊ฐ€ ์ƒˆ๋กœ ๊ณ ์ณ์ง‘๋‹ˆ๋‹ค. ๋ณ€๊ฒฝ ์‚ฌํ•ญ์„ ๊ฐ์ง€ํ•˜๊ณ  ํ•ด๋‹น ์ปดํฌ๋„ŒํŠธ๋ฅผ ์ƒˆ๋กœ ๊ณ ์น˜๋„๋ก ๋ณ€๊ฒฝ ์‚ฌํ•ญ์„ ์ ์šฉํ•˜๋ ค๋ฉด ์ด๋ฒคํŠธ ๋ฆฌ์Šค๋„ˆ๋ฅผ ์ˆ˜๋™์œผ๋กœ ์„ค์ •ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

๊ทธ๋Ÿฌ๋‚˜ ๋‹จ๋ฐฉํ–ฅ ๋ฐ์ดํ„ฐ ํ๋ฆ„์ด ๋“ฑ์žฅํ•˜๋ฉด์„œ ๋ฆฌ์•กํŠธ๋Š” ๋‹ค๋ฅธ ๋งŽ์€ ์ตœ์‹  ํ”„๋ ˆ์ž„์›Œํฌ๋ฅผ ๋”ฐ๋ผ์„œ ๋‹ค๋ฅธ ๊ธธ์„ ๊ฑท๊ฒŒ ๋˜์—ˆ์Šต๋‹ˆ๋‹ค. ๊ฐœ๋ฐœ์ž๋กœ์„œ ์šฐ๋ฆฌ๋Š” ๋” ์ด์ƒ ๋ชจ๋ธ ๋ณ€๊ฒฝ ์‚ฌํ•ญ์„ ๋ชจ๋‹ˆํ„ฐ๋งํ•  ํ•„์š”๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค. ๊ธฐ๋ณธ ์•„์ด๋””์–ด๋Š” ๋ชจ๋“  ๋ณ€๊ฒฝ ์‚ฌํ•ญ์„ ์™„์ „ํžˆ ์ƒˆ๋กœ์šด ์ธ์Šคํ„ด์Šค๋กœ ์ทจ๊ธ‰ํ•˜๊ณ  ๋ชจ๋“  ๊ฒƒ์„ ์ฒ˜์Œ๋ถ€ํ„ฐ ๋‹ค์‹œ ๋ Œ๋”๋งํ•˜๋Š” ๊ฒƒ์ž…๋‹ˆ๋‹ค. ์—ฌ๊ธฐ์„œ ์ „์ฒด ํ”„๋กœ์„ธ์Šค๋ฅผ ํ˜„์ €ํ•˜๊ฒŒ ๊ฐ„์†Œํ™”ํ•˜๊ณ  ๊ฐ€์ƒ DOM๊ณผ ์ฐจ๋ณ„ํ™” ๋ฐ ์กฐ์ • ํ”„๋กœ์„ธ์Šค๋ฅผ ๊ฐ„๊ณผํ•˜๊ณ  ์žˆ๋‹ค๋Š” ์ ์— ์ฃผ๋ชฉํ•˜๋Š” ๊ฒƒ์ด ์ค‘์š”ํ•ฉ๋‹ˆ๋‹ค. ์ฆ‰, ์ฝ”๋“œ๋ฒ ์ด์Šค ๋‚ด์—์„œ ๋ชจ๋ธ ๋ณ€๊ฒฝ ํ›„ ๋‹ค๋ฅธ ์„ธ๊ทธ๋จผํŠธ๋ฅผ ์ •ํ™•ํ•˜๊ฒŒ ์—…๋ฐ์ดํŠธํ•˜๊ธฐ ์œ„ํ•ด ์ด๋ฒคํŠธ ๋ฆฌ์Šค๋„ˆ๋ฅผ ๋“ฑ๋กํ•ด์•ผ ํ•  ํ•„์š”์„ฑ์ด ์‚ฌ๋ผ์กŒ์Œ์„ ์˜๋ฏธํ•ฉ๋‹ˆ๋‹ค.

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

์ปค๋ฎค๋‹ˆํ‹ฐ ์ดํ•ดํ•˜๊ธฐ

ํ—ค๋“œ๋ฆฌ์Šค ์ปดํฌ๋„ŒํŠธ์˜ ๊ฐœ๋…์€ ์ƒˆ๋กœ์šด ๊ฒƒ์ด ์•„๋‹ˆ๋ฉฐ ํ•œ๋™์•ˆ ์กด์žฌํ•ด ์™”์ง€๋งŒ ๋„๋ฆฌ ์ธ์ •๋ฐ›๊ฑฐ๋‚˜ ํ”„๋กœ์ ํŠธ์— ํ†ตํ•ฉ๋˜์ง€๋Š” ๋ชปํ–ˆ์Šต๋‹ˆ๋‹ค. ํ•˜์ง€๋งŒ ์—ฌ๋Ÿฌ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์—์„œ ํ—ค๋“œ๋ฆฌ์Šค ์ปดํฌ๋„ŒํŠธ ํŒจํ„ด์„ ์ฑ„ํƒํ•˜์—ฌ ์ ‘๊ทผ ๊ฐ€๋Šฅํ•˜๊ณ  ์ ์‘ ๊ฐ€๋Šฅํ•˜๋ฉฐ ์žฌ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•œ ์ปดํฌ๋„ŒํŠธ์˜ ๊ฐœ๋ฐœ์„ ์ด‰์ง„ํ•˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค. ์ด๋Ÿฌํ•œ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ์ค‘ ์ผ๋ถ€๋Š” ์ด๋ฏธ ์ปค๋ฎค๋‹ˆํ‹ฐ ๋‚ด์—์„œ ์ƒ๋‹นํ•œ ์ฃผ๋ชฉ์„ ๋ฐ›๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค.

  • React ARIA: ํฌ๊ด„์ ์ธ ๋ฆฌ์•กํŠธ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์„ ๊ตฌ์ถ•ํ•˜๊ธฐ ์œ„ํ•œ ์ ‘๊ทผ์„ฑ ๊ธฐ๋ณธ ์š”์†Œ์™€ ํ›…์„ ์ œ๊ณตํ•˜๋Š” Adobe์˜ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์ž…๋‹ˆ๋‹ค. ํ‚ค๋ณด๋“œ ์ƒํ˜ธ์ž‘์šฉ, ํฌ์ปค์Šค ๊ด€๋ฆฌ, ARIA ์ฃผ์„์„ ๊ด€๋ฆฌํ•  ์ˆ˜ ์žˆ๋Š” ํ›… ๋ชจ์Œ์„ ์ œ๊ณตํ•˜์—ฌ ์ ‘๊ทผ์„ฑ ์žˆ๋Š” UI ์ปดํฌ๋„ŒํŠธ๋ฅผ ๋” ์‰ฝ๊ฒŒ ๋งŒ๋“ค ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
  • Headless UI: ์™„์ „ํžˆ ์Šคํƒ€์ผ์ด ์ง€์ •๋˜์ง€ ์•Š๊ณ  ์ ‘๊ทผ์„ฑ์ด ๋†’์€ UI ์ปดํฌ๋„ŒํŠธ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋กœ, Tailwind CSS์™€ ์ž˜ ํ†ตํ•ฉ๋˜๋„๋ก ์„ค๊ณ„๋˜์—ˆ์Šต๋‹ˆ๋‹ค. ๊ฐœ๋ฐœ์ž๋งŒ์˜ ์Šคํƒ€์ผ์ด ์ ์šฉ๋œ ์ปดํฌ๋„ŒํŠธ๋ฅผ ๊ตฌ์ถ•ํ•  ์ˆ˜ ์žˆ๋Š” ๋™์ž‘ ๋ฐ ์ ‘๊ทผ์„ฑ์„ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค.
  • React Table: ๋ฆฌ์•กํŠธ์šฉ ๋น ๋ฅด๊ณ  ํ™•์žฅ ๊ฐ€๋Šฅํ•œ ํ…Œ์ด๋ธ”๊ณผ ๋ฐ์ดํ„ฐ ๊ทธ๋ฆฌ๋“œ๋ฅผ ๊ตฌ์ถ•ํ•˜๊ธฐ ์œ„ํ•œ ํ—ค๋“œ๋ฆฌ์Šค ์œ ํ‹ธ๋ฆฌํ‹ฐ์ž…๋‹ˆ๋‹ค. ๋ณต์žกํ•œ ํ…Œ์ด๋ธ”์„ ์‰ฝ๊ฒŒ ์ƒ์„ฑํ•  ์ˆ˜ ์žˆ๋Š” ์œ ์—ฐํ•œ ํ›…์„ ์ œ๊ณตํ•˜์—ฌ UI ํ‘œํ˜„์€ ์‚ฌ์šฉ์ž์—๊ฒŒ ๋งก๊น๋‹ˆ๋‹ค.
  • Downshift: ์ ‘๊ทผ ๊ฐ€๋Šฅํ•˜๊ณ  ์ปค์Šคํ„ฐ๋งˆ์ด์ง• ๊ฐ€๋Šฅํ•œ ๋“œ๋กญ๋‹ค์šด, ์ฝค๋ณด๋ฐ•์Šค ๋“ฑ์„ ๋งŒ๋“œ๋Š” ๋ฐ ๋„์›€์ด ๋˜๋Š” ๋ฏธ๋‹ˆ๋ฉ€๋ฆฌ์ŠคํŠธ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์ž…๋‹ˆ๋‹ค. ๋ชจ๋“  ๋กœ์ง์„ ์ฒ˜๋ฆฌํ•˜๋Š” ๋™์‹œ์— ๋ Œ๋”๋ง ์ธก๋ฉด์„ ์ •์˜ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

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

์ด ํŒจํ„ด์€ ๋ณต์žกํ•œ ๋กœ์ง๊ณผ ์ƒํƒœ ๊ด€๋ฆฌ์— ๋Œ€ํ•œ ๊ต์œก์„ ์ œ๊ณตํ•  ๋ฟ๋งŒ ์•„๋‹ˆ๋ผ ํ—ค๋“œ๋ฆฌ์Šค ์ปดํฌ๋„ŒํŠธ ์ ‘๊ทผ ๋ฐฉ์‹์„ ๊ฐœ์„ ํ•˜์—ฌ ์‹ค์ œ ํ™˜๊ฒฝ์—์„œ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋Š” ๊ฐ•๋ ฅํ•˜๊ณ  ์ ‘๊ทผ ๊ฐ€๋Šฅํ•˜๋ฉฐ ์‚ฌ์šฉ์ž ์ •์˜ ๊ฐ€๋Šฅํ•œ ์ปดํฌ๋„ŒํŠธ๋ฅผ ์ œ๊ณตํ•˜๋Š” ํ”„๋กœ๋•์…˜ ์ง€์› ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ํƒ์ƒ‰ํ•˜๋„๋ก ์œ ๋„ํ•ฉ๋‹ˆ๋‹ค.

๊ฒฐ๋ก 

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

์‹ค์ œ ์‚ฌ๋ก€๋ฅผ ํ†ตํ•ด ์ด๋Ÿฌํ•œ ๋ถ„๋ฆฌ๊ฐ€ ์–ด๋–ป๊ฒŒ ์žฌ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•˜๊ณ  ์ ‘๊ทผ ๊ฐ€๋Šฅํ•œ ๋งž์ถคํ˜• ์ปดํฌ๋„ŒํŠธ๋ฅผ ๊ตฌ์ถ•ํ•  ์ˆ˜ ์žˆ๋Š” ๊ธธ์„ ์—ด์–ด์ฃผ๋Š”์ง€ ์กฐ๋ช…ํ•ฉ๋‹ˆ๋‹ค. ๋˜ํ•œ ํ—ค๋“œ๋ฆฌ์Šค ์ปดํฌ๋„ŒํŠธ ํŒจํ„ด์„ ์˜นํ˜ธํ•˜๋Š” React Table, Downshift, React UseGesture, React ARIA, ํ—ค๋“œ๋ฆฌ์Šค UI์™€ ๊ฐ™์€ ์œ ๋ช…ํ•œ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ์ง‘์ค‘ ์กฐ๋ช…ํ•ฉ๋‹ˆ๋‹ค. ์ด๋Ÿฌํ•œ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋Š” ์ธํ„ฐ๋ž™ํ‹ฐ๋ธŒํ•˜๊ณ  ์‚ฌ์šฉ์ž ์นœํ™”์ ์ธ UI ์ปดํฌ๋„ŒํŠธ๋ฅผ ๊ฐœ๋ฐœํ•˜๊ธฐ ์œ„ํ•ด ๋ฏธ๋ฆฌ ๊ตฌ์„ฑ๋œ ์†”๋ฃจ์…˜์„ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค.

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


๊ฐ์ฃผ

1: Fake๋Š” ์™ธ๋ถ€ ์‹œ์Šคํ…œ์ด๋‚˜ ๋ฆฌ์†Œ์Šค์— ๋Œ€ํ•œ ์•ก์„ธ์Šค๋ฅผ ์บก์Šํ™”ํ•˜๋Š” ๊ฐ์ฒด์ž…๋‹ˆ๋‹ค. ๋ชจ๋“  ์ฑ„ํƒ ๋กœ์ง์„ ์ฝ”๋“œ๋ฒ ์ด์Šค์— ๋ถ„์‚ฐ์‹œํ‚ค๊ณ  ์‹ถ์ง€ ์•Š์„ ๋•Œ ์œ ์šฉํ•˜๋ฉฐ, ์™ธ๋ถ€ ์‹œ์Šคํ…œ์ด ๋ณ€๊ฒฝ๋  ๋•Œ ํ•œ ๊ณณ์—์„œ ๋ณ€๊ฒฝํ•˜๊ธฐ๊ฐ€ ๋” ์‰ฌ์›Œ์งˆ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.


๐Ÿš€ ํ•œ๊ตญ์–ด๋กœ ๋œ ํ”„๋ŸฐํŠธ์—”๋“œ ์•„ํ‹ฐํด์„ ๋น ๋ฅด๊ฒŒ ๋ฐ›์•„๋ณด๊ณ  ์‹ถ๋‹ค๋ฉด Korean FE Article(https://kofearticle.substack.com/)์„ ๊ตฌ๋…ํ•ด์ฃผ์„ธ์š”!


Written by@[Ykss]
๊ณ ์ด๊ฒŒ ๋‘์ง€ ์•Š๊ณ  ํ˜๋ ค๋ณด๋‚ด๋Š” ๊ฐœ๋ฐœ์ž๊ฐ€ ๋˜์ž.

GitHubInstagramLinkedIn