๐Ÿ’ก 20. ๋‹คํ˜•์„ฑ(Polymorphism)๊ณผ ์ ‘๊ทผ์„ฑ(a11y)

๐Ÿ“‹ ๊ฐœ์š”

๋•Œ๋กœ๋Š” <button>์œผ๋กœ, ๋•Œ๋กœ๋Š” <a> ํƒœ๊ทธ๋‚˜ <Link>๋กœ ์ž์œ ์ž์žฌ๋กœ ๋ณ€์‹ ํ•˜๋Š” ๋‹คํ˜•์„ฑ ๋ Œ๋”๋ง(as prop, Slot) ์„ค๊ณ„์™€, ์‹œ๊ฐ์žฅ์• ์ธ๋„ ์™„๋ฒฝํžˆ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋Š” ๋Œ€๊ธฐ์—…๊ธ‰ ์ปดํฌ๋„ŒํŠธ์˜ ๋น„๋ฐ€์„ ํŒŒํ—ค์นฉ๋‹ˆ๋‹ค.

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

  • ์‚ฌ๋‚ด ๊ณตํ†ต Button ์ปดํฌ๋„ŒํŠธ๊ฐ€ ์™œ ์–ด๋–จ ๋•Œ๋Š” ๋งํฌ(<a>)๋กœ ๋ณ€์‹ ํ•ด์•ผ ํ•˜๋Š”์ง€ ์œ ์—ฐ์„ฑ์˜ ๋ณธ์งˆ์„ ๊นจ๋‹ซ๋Š”๋‹ค.
  • as prop๊ณผ Radix UI์˜ Slot ํŒจํ„ด์„ ์‚ฌ์šฉํ•˜์—ฌ ๋งˆ๋ฒ•์ฒ˜๋Ÿผ ํƒœ๊ทธ ๊ป๋ฐ๊ธฐ๋ฅผ ๊ฐˆ์•„ ๋ผ์šฐ๋Š” '๋‹คํ˜•์„ฑ' ์ปดํฌ๋„ŒํŠธ๋ฅผ ์„ค๊ณ„ํ•  ์ˆ˜ ์žˆ๋‹ค.
  • ํ‚ค๋ณด๋“œ ๋„ค๋น„๊ฒŒ์ด์…˜๊ณผ ์Šคํฌ๋ฆฐ ๋ฆฌ๋”(VoiceOver)๋ฅผ ์ง€์›ํ•˜๋Š” aria- ์†์„ฑ์˜ ๊ธฐ๋ณธ๊ธฐ๋ฅผ ๊ฐ–์ถ˜ ๋ฐฐ๋ฆฌ์–ดํ”„๋ฆฌ(Barrier-Free) ํ”„๋ก ํŠธ์—”๋“œ ๊ฐœ๋ฐœ์ž๋กœ ๊ฑฐ๋“ญ๋‚œ๋‹ค.

๐Ÿ“‹ ๋ชฉ์ฐจ


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

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

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

  • ์˜์ˆ™(๋””์ž์ด๋„ˆ): "์˜์ฒ  ๋‹˜! ์ด๋ฒˆ์— ๋งŒ๋“  [ํšŒ์›๊ฐ€์ž… ์™„๋ฃŒ] ๋ฒ„ํŠผ ๋””์ž์ธ ๋„ˆ๋ฌด ์—๋ป์š”. ์ € ๋””์ž์ธ ๊ทธ๋Œ€๋กœ ๋ณต์‚ฌํ•ด์„œ, ๋ฉ”์ธ ํŽ˜์ด์ง€ ํ•˜๋‹จ์˜ [์ด๋ฒคํŠธ ํŽ˜์ด์ง€๋กœ ์ด๋™] ๋ฒ„ํŠผ์œผ๋กœ๋„ ๋˜‘๊ฐ™์ด ๋ Œ๋”๋งํ•ด์ฃผ์‹ค ์ˆ˜ ์žˆ์ฃ ?"
  • ์˜์ฒ (์‹ ์ž…): "๋„ค! ๊ทธ์•ผ ๋‹น์—ฐํ•˜์ฃ  (์ž์‹ ๋งŒ๋งŒ). ์–ด? ๊ทผ๋ฐ [ํšŒ์›๊ฐ€์ž… ์™„๋ฃŒ]๋Š” ๋ˆŒ๋ €์„ ๋•Œ ํผ์„ ์ œ์ถœํ•˜๋Š” <button>์ธ๋ฐ... [์ด๋™] ๋ฒ„ํŠผ์€ ๋‹ค๋ฅธ ํŽ˜์ด์ง€๋กœ ์™„์ „ํžˆ ๋„˜์–ด๊ฐ€๋Š” <a> ํƒœ๊ทธ(Next.js Link)์—ฌ์•ผ ํ•˜์ž–์•„์š”? ๊ทธ๋Ÿผ ๋””์ž์ธ์€ ๋˜‘๊ฐ™์€๋ฐ ์ปดํฌ๋„ŒํŠธ๋ฅผ <Button>์šฉ, <LinkButton>์šฉ 2๊ฐœ๋กœ ๋”ฐ๋กœ ์ชผ๊ฐœ์•ผ ํ•˜๋‚˜์š”? CSS ์ค‘๋ณต์ด 500์ค„์ด ๋„˜์–ด๊ฐ€๋Š”๋ฐ..."
  • ์˜ํ˜ธ(๋ฆฌ๋“œ): "์˜์ฒ  ๋‹˜, ์–ธ์ œ๊นŒ์ง€ ๊ป๋ฐ๊ธฐ(ํƒœ๊ทธ ์ข…๋ฅ˜)์— ์–ฝ๋งค์—ฌ์„œ ๋˜‘๊ฐ™์€ ๋””์ž์ธ์˜ ์ฝ”๋“œ๋ฅผ ๋ณต๋ถ™ํ•˜์‹ค ๊ฑด๊ฐ€์š”. ๋ฒ„ํŠผ ์ปดํฌ๋„ŒํŠธ์—๊ฒŒ ์นด๋ฉœ๋ ˆ์˜จ์ฒ˜๋Ÿผ ๋ณ€์‹ ํ•  ์ˆ˜ ์žˆ๋Š” ๋ณ€์‹  ๋ฌผ์•ฝ (๋‹คํ˜•์„ฑ)์„ ๋จน์ž…์‹œ๋‹ค! ์•„, ๊ทธ๋ฆฌ๊ณ  ์Šคํฌ๋ฆฐ ๋ฆฌ๋”๊ธฐ๋กœ ์ ‘์†ํ•˜์‹œ๋Š” ์‹œ๊ฐ์žฅ์• ์ธ ์œ ์ €๋“ค์„ ์œ„ํ•ด ์ ‘๊ทผ์„ฑ (a11y)ํƒœ๊ทธ๋„ ์ข€ ๋ฐ•์•„๋†“์์‹œ๋‹ค."

๐Ÿค” ์™œ ์•Œ์•„์•ผ ํ•˜๋Š”๊ฐ€: '๊ป๋ฐ๊ธฐ'์˜ ์ €์ฃผ

๋ชจ์–‘๊ณผ ํฐํŠธ ํฌ๊ธฐ, ํ˜ธ๋ฒ„(Hover) ์‹œ ํŒŒ๋ž€์ƒ‰์œผ๋กœ ๋ณ€ํ•˜๋Š” ์˜๋กฑํ•œ ์• ๋‹ˆ๋ฉ”์ด์…˜๊นŒ์ง€ ๋ฒฝ๋Œ ํ•˜๋‚˜ ํ‹€๋ฆฌ์ง€ ์•Š๊ณ  ๋˜‘๊ฐ™์€ 100% ๋™์ผํ•œ ๋ฒ„ํŠผ ๋””์ž์ธ์ž…๋‹ˆ๋‹ค.
๊ทธ๋Ÿฐ๋ฐ ์–ด๋–ค ๋†ˆ์€ API๋ฅผ ์˜๋Š” type="submit" ๋ฒ„ํŠผ์ด๊ณ , ์–ด๋–ค ๋†ˆ์€ ๊ตฌ๊ธ€ ์ฐฝ์„ ์—ฌ๋Š” href="https://..." ํ˜•ํƒœ์˜ ์•ต์ปค(A) ํƒœ๊ทธ๋ผ๊ณ  ์นฉ์‹œ๋‹ค.

// โŒ ์˜์ฒ ์ด์˜ ๋ฌด์‹ํ•œ ํ•˜๋“œ์ฝ”๋”ฉ (๋””์ž์ธ ์ค‘๋ณต์˜ ์ง€์˜ฅ)
 
// 1๋ฒˆ: ํผ ์ œ์ถœ์šฉ ๋ฒ„ํŠผ
export function DefaultButton({ children, onClick }) {
  return <button className="super-gorgeous-blue-btn" onClick={onClick}>{children}</button>;
}
 
// 2๋ฒˆ: ์™ธ๋ถ€ ๋งํฌ ์ด๋™์šฉ ๋ฒ„ํŠผ
export function LinkButton({ children, href }) {
  // CSS ํด๋ž˜์Šค ์˜คํƒ€๋ผ๋„ ๋‚˜๋ฉด ๋””์ž์ธ์ด ํ‹€์–ด์ง„๋‹ค!
  return <a className="super-gorgeous-blue-btn" href={href}>{children}</a>;
}

๋””์ž์ธ ์‹œ์Šคํ…œ(Design System)์„ ๊ตฌ์ถ•ํ•˜๋Š” ํŒ€์—์„œ ์ด๋Ÿฐ ์ง“์„ ํ•œ๋‹ค๋ฉด ๋‹น์žฅ ์ง์„ ์‹ธ์•ผ ํ•ฉ๋‹ˆ๋‹ค. ๋””์ž์ธ(Style)๊ณผ ๊ธฐ๋Šฅ(HTML Tag)์€ ์™„์ „ํžˆ ์œตํ•ฉ๋  ์ˆ˜ ์—†๋Š” ์กด์žฌ์ž…๋‹ˆ๋‹ค. ์šฐ๋ฆฌ๋Š” ์ปดํฌ๋„ŒํŠธ ํ˜ธ์ŠคํŠธ๊ฐ€ ๋˜์ ธ์ฃผ๋Š” ์ง€์‹œ๋ฅผ ๋“ฃ๊ณ  ์œ ์ฒด์ดํƒˆ ๋ณ€ํ˜•์„ ํ•˜๋Š” ๋‹คํ˜•์„ฑ(Polymorphism) ๋ฒ„ํŠผ์„ ๋งŒ๋“ค์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.


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

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

  1. ๊ณผ๊ฑฐ์˜ ๋ปฃ๋ปฃํ•œ ๊ฐ‘์˜ท (ํ•˜๋“œ์ฝ”๋”ฉ): "์ด ์•„๋ฆ„๋‹ค์šด ํ™ฉ๊ธˆ ํŒŒ๋ž€์ƒ‰ ๊ฐ‘์˜ท์€ ๋ฌด์กฐ๊ฑด '๊ฒ€์‚ฌ(Button)' ์ „์šฉ์ž…๋‹ˆ๋‹ค!" ๋งˆ๋ฒ•์‚ฌ(Link)๊ฐ€ ์ž…์œผ๋ ค๊ณ  ํ•˜๋ฉด ์–ต์ง€๋กœ ๋‘ ๋ฒˆ์งธ ๊ฐ‘์˜ท์„ ์•„์˜ˆ ์ฒ˜์Œ๋ถ€ํ„ฐ ๋‹ค์‹œ ์ฒ ์„ ๋‘๋“ค๊ฒจ ์ƒˆ๋กœ ๋งŒ๋“ค์–ด ์ค˜์•ผ ํ•ฉ๋‹ˆ๋‹ค.
  2. ๋‹คํ˜•์„ฑ์˜ ๋งˆ๋ฒ• ๊ฐ‘์˜ท (Polymorphic): ์ด ํ™ฉ๊ธˆ ํŒŒ๋ž€์ƒ‰ ๊ฐ‘์˜ท(CSS ์Šคํƒ€์ผ)์€ ์•ก์ฒด๊ดด๋ฌผ์ฒ˜๋Ÿผ ์ž…๋Š” ์‚ฌ๋žŒ์˜ ์ง์—…์— ๋งž์ถฐ ๋ณ€ํ˜•๋ฉ๋‹ˆ๋‹ค!
    ๋งˆ๋ฒ•์‚ฌ๊ฐ€ ์ž…์œผ๋ฉด ์ง€ํŒก์ด ๊ตฌ๋ฉ(href)์ด ๋‚˜ ์žˆ๋Š” ์˜ท์œผ๋กœ ์Šค๋ฅด๋ฅต ๋ณ€ํ•˜๊ณ , ๊ฒ€์‚ฌ๊ฐ€ ์ž…์œผ๋ฉด ์นผ๊ฝ‚์ด(type="submit")๊ฐ€ ๋‹ฌ๋ฆฐ ๋ฒ„ํŠผ์œผ๋กœ ๋ณ€์‹ ํ•˜์ฃ . ํ•˜์ง€๋งŒ ๊ฒ‰์—์„œ ๋ณผ ๋• ๋ˆˆ๋ถ€์‹  ํŒŒ๋ž€ ํ™ฉ๊ธˆ์ƒ‰ ์Šคํƒ€์ผ์„ ์™„๋ฒฝํ•˜๊ฒŒ ๋˜‘๊ฐ™์ด ์œ ์ง€ํ•ฉ๋‹ˆ๋‹ค.

๐Ÿงฉ ๊ทน์˜ ์ฒด๋“: ์œ ์ฒด์ดํƒˆ ๋‹คํ˜•์„ฑ ์„ค๊ณ„ํ•˜๊ธฐ

๋ณดํ†ต ์‹ค๋ฌด์—์„œ๋Š” ์ด "๋ณ€์‹ " ์ง€์‹œ๋ฅผ ๋‚ด๋ฆฌ๊ธฐ ์œ„ํ•ด ๋‘ ๊ฐ€์ง€ ํŒจํ„ด์„ ์ฃผ๋กœ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค.

โœ… ๋ฐฉ๋ฒ• 1: as Prop ํŒจํ„ด (์ „ํ†ต์˜ ๊ฐ•์ž)

๊ฐ€์žฅ ๋ฌด๋‚œํ•˜๊ณ  ๋„๋ฆฌ ์•Œ๋ ค์ง„ ํŒจํ„ด์ž…๋‹ˆ๋‹ค. "๋„ˆ ์˜ค๋Š˜๋ถ€ํ„ฐ ์ € ํƒœ๊ทธ(as)๋กœ ์‚ด์•„๋ผ!" ๋ผ๊ณ  ๋ช…์‹œํ•ด์ค๋‹ˆ๋‹ค.

// ๐ŸŽฏ as๋ฅผ ์‚ฌ์šฉํ•œ ๋‹คํ˜•์„ฑ ๋ฒ„ํŠผ ์„ค๊ณ„
export function MagicButton({ as: Component = "button", children, ...props }) {
  
  // Component์— "a"๊ฐ€ ๋“ค์–ด์˜ค๋ฉด <a> ํƒœ๊ทธ๋กœ, "button"์ด๋ฉด <button>์œผ๋กœ ๋ Œ๋”๋ง!
  // ์‹ฌ์ง€์–ด Next.js์˜ <Link> ์ปดํฌ๋„ŒํŠธ๋ฅผ ํ†ต์งธ๋กœ ๋„˜๊ฒจ๋„ ๋ฐ›์•„๋จน๋Š”๋‹ค!
  return (
    <Component className="super-gorgeous-blue-btn" {...props}>
      {children}
    </Component>
  );
}
 
// ------ ์‚ฌ์šฉํ•˜๋Š” ์ชฝ (์†Œ๋น„์ž) ------
function App() {
  return (
    <div>
      {/* 1. ๊ธฐ๋ณธ์€ ์ œ์ถœ์šฉ <button>์œผ๋กœ ๋ Œ๋”๋ง ๋จ */}
      <MagicButton type="submit">๋กœ๊ทธ์ธ</MagicButton>
 
      {/* 2. ๊ตฌ๊ธ€๋กœ ๋‚ ์•„๊ฐ€๋Š” <a> ํƒœ๊ทธ๋กœ ๋ณ€์‹ ! ๋””์ž์ธ์€ ๋˜‘๊ฐ™๋‹ค! */}
      <MagicButton as="a" href="https://google.com">
        ๊ตฌ๊ธ€์—์„œ ๋กœ๊ทธ์ธ
      </MagicButton>
 
      {/* 3. ๋ผ์šฐํŒ… ์ตœ์ ํ™”๋ฅผ ์œ„ํ•ด Next.js Link ํƒœ๊ทธ์˜ ์˜ํ˜ผ์„ ๋ฎ์–ด์”Œ์›€! */}
      <MagicButton as={Link} href="/home">
        ์šฐ๋ฆฌ์ง‘์œผ๋กœ ๊ฐ€์ž
      </MagicButton>
    </div>
  )
}

์ด์ œ ์šฐ๋ฆฌ์˜ ์˜๋กฑํ•œ ํŒŒ๋ž€์ƒ‰ CSS๋Š” ์ง€๊ตฌ์ƒ์— ๋‹จ ํ•œ ๋ฒˆ(className="super-gorgeous...")๋งŒ ์„ ์–ธ๋˜๊ณ , ์ˆ˜์ฒœ๋งŒ ๊ฐ€์ง€์˜ ํƒœ๊ทธ ๊ตฌ์‹ค์„ ๋‹ค ํ•ด๋ƒ…๋‹ˆ๋‹ค!

โœ… ๋ฐฉ๋ฒ• 2: Slot (asChild) ํŒจํ„ด (ํ˜„๋Œ€ Radix UI์˜ ๋Œ€์„ธ)

as ํŒจํ„ด์€ ๋„ˆ๋ฌด ์ง๊ด€์ ์ด๊ณ  ์ข‹์ง€๋งŒ ํ•œ๊ณ„๊ฐ€ ์กด์žฌํ•ฉ๋‹ˆ๋‹ค. ํƒ€์ž…์Šคํฌ๋ฆฝํŠธ์˜ ์ถ”๋ก (TypeScript Generic type infer)์„ ์ž‘์„ฑํ•˜๊ธฐ ์œ„ํ•ด ์ฝ”๋“œ๊ฐ€ ์šฐ์ฃผ ๋๊นŒ์ง€ ๋”๋Ÿฌ์›Œ์ง‘๋‹ˆ๋‹ค. "๋งŒ์•ฝ as๊ฐ€ a ์†์„ฑ์ด๋ฉด ๋ฐ˜๋“œ์‹œ href ์†์„ฑ๋„ ๋ฑ‰์–ด๋‚ด๋ผ!" ๊ฐ™์€ ์กฐ๊ฑด์„ ๋งค๊ธฐ๊ธฐ๊ฐ€ ๊นŒ๋‹ค๋กญ์ฃ .

๊ทธ๋ž˜์„œ ์ตœ๊ทผ ๋Œ€๊ธฐ์—…๋“ค(Radix, Shadcn UI ๋“ฑ)์ด ์ด ํ•œ๊ณ„๋ฅผ ๋šซ๊ณ ์ž ํญ๋ฐœ์ ์œผ๋กœ ๋ฐ€๊ณ  ์žˆ๋Š” ํŒจํ„ด์ด ๋ฐ”๋กœ ์ž์‹์—๊ฒŒ ๋””์ž์ธ์˜ ๊ถŒ๋ ฅ์„ ์œ„์ž„ํ•˜๋Š” Slot (asChild) ํŒจํ„ด์ž…๋‹ˆ๋‹ค.

// ๐ŸŽฏ Slot (asChild) ํŒจํ„ด์˜ ์ฒ ํ•™
import { Slot } from '@radix-ui/react-slot'; // (์›๋ฆฌ๋ฅผ ๊ตฌํ˜„ํ•œ ์œ ๋ช… ํˆด)
 
// ๋””์ž์ธ ๊ป๋ฐ๊ธฐ๋งŒ ์ฅ๊ณ  ์žˆ๋Š” ๋ฐ”๋ณด ์ปดํฌ๋„ŒํŠธ
export function MasterButton({ asChild, children, ...props }) {
  
  // asChild๊ฐ€ ์ผœ์ ธ์žˆ์œผ๋ฉด? "์•ˆ๋…•, ๋‚œ ์žํญํ• ๊ฒŒ. ๋‚ด ํŒŒ๋ž€์ƒ‰ ์™ธํˆฌ๋ž‘ ์ด๋ฒคํŠธ(props)๋“ค์€ ์ฃ„๋‹ค 
  // ๋‚ด ๋ฐ‘์— ๋“ค์–ด์˜จ ๋‚ด ์ž์‹(children) ๋†ˆํ•œํ…Œ ๋ชฝ๋•… ํ† ์Šคํ•ด์„œ ๋ฎ์–ด์”Œ์›Œ์ค˜!"
  const Comp = asChild ? Slot : "button";
 
  return (
    <Comp className="super-gorgeous-blue-btn" {...props}>
      {children}
    </Comp>
  )
}
 
// ------ ์‚ฌ์šฉํ•˜๋Š” ์ชฝ (์†Œ๋น„์ž) ------
function App() {
  return (
    <MasterButton asChild>
      <a href="https://google.com">ํŒŒ๋ž€ ๊ตฌ๊ธ€๋ฒ„ํŠผ</a> 
      {/* ๊ฒฐ๊ณผ: Slot์ด <a href="..." className="super-gorgeous..." /> ๋กœ ๋ณ‘ํ•ฉํ•ด ๋ Œ๋”๋ง! */}
    </MasterButton>
  )
}

์ด ๋ฐฉ๋ฒ•์˜ ์•„๋ฆ„๋‹ค์›€: <a> ํƒœ๊ทธ๋Š” ์ž์‹ ์ด ํƒœ์ƒ๋ถ€ํ„ฐ ๊ฐ–๊ณ  ์žˆ๋Š” ๊ณ ์œ ์˜ ์†์„ฑ(href)๋งŒ ์ฑ…์ž„์ง€๊ณ , MasterButton์€ ๋””์ž์ธ CSS ๋ฐ”๋ฅด๋Š” ๊ฒƒ๋งŒ ์ฑ…์ž„์ง‘๋‹ˆ๋‹ค. ํ”„๋กญ์Šค ๊ตฌ์กฐ๊ฐ€ ์–ฝํžˆ์ง€ ์•Š๊ณ  ๊น”๋”ํ•˜๊ฒŒ ์ƒ์†๋˜๋Š” ๊ธฐ์ ์˜ ๋ Œ๋”๋ง์ด ์ผ์–ด๋‚ฉ๋‹ˆ๋‹ค.


๐Ÿ‘‚ ๋น›๊ณผ ์†Œ๋ฆฌ: ์ ‘๊ทผ์„ฑ (Accessibility, a11y) ์ฑ™๊ธฐ๊ธฐ

์˜ˆ์œ ๋‹คํ˜•์„ฑ ๋ฒ„ํŠผ์„ ๋นš์—ˆ๋‹ค๊ณ  ์‹œ๋‹ˆ์–ด๊ธ‰์ด ๋˜๋Š” ๊ฒŒ ์•„๋‹™๋‹ˆ๋‹ค. ์ด ๋ฒ„ํŠผ์ด ๋ˆŒ๋ฆด ์ˆ˜ ์žˆ๋Š” ์กฐ๊ฑด์€ ๋‹น์‹ ์˜ ์•ฑ์— ์ ‘์†ํ•˜๋Š” ๋งˆ์šฐ์Šค๊ฐ€ ๊ณ ์žฅ ๋‚œ ์œ ์ €, ํ˜น์€ ์‹œ๋ ฅ์ด ๋„์ €ํžˆ ์•ˆ ๋ณด์—ฌ ์Šคํฌ๋ฆฐ ๋ฆฌ๋”๊ธฐ(TTS)๋กœ ๋ฒ„ํŠผ์„ ๋“ฃ๋Š” ์‹œ๊ฐ์žฅ์• ์ธ๋“ค์—๊ฒŒ๋„ ์œ ํšจํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

์ด๊ฒƒ์„ ์šฐ๋ฆฌ๋Š” **a11y (์ ‘๊ทผ์„ฑ)**์ด๋ผ๊ณ  ๋ถ€๋ฅด๋ฉฐ, ํ˜„์—… ๋””์ž์ธ ์‹œ์Šคํ…œ์—์„œ ๊ฐ€์žฅ ์•…๋ช… ๋†’์€ ๋‚œ์ด๋„๋ฅผ ์ž๋ž‘ํ•ฉ๋‹ˆ๋‹ค.

  1. aria- ์†์„ฑ์˜ ์ง€์˜ฅ: ์ € ๋ฒ„ํŠผ์ด ๋‹จ์ˆœํžˆ '์ „์†ก' ๊ธ€์ž๋งŒ ์žˆ๋Š” ๊ฒŒ ์•„๋‹ˆ๋ผ๋ฉด?
// ์‹œ๊ฐ์žฅ์• ์ธ ๋ฆฌ๋”๊ธฐ๋Š” "์•ˆ๋‚ด๋ฌธ ์—ด๊ธฐ" ๋ผ๊ณ  ์นœ์ ˆํ•˜๊ฒŒ ์ฝ์–ด์ค€๋‹ค!
<MagicButton aria-label="์•ˆ๋‚ด๋ฌธ ์—ด๊ธฐ" aria-expanded={isOpen} onClick={toggle}>
   ๐Ÿš€ {/* ๋ˆˆ์— ๋„๋Š” ํ…์ŠคํŠธ๊ฐ€ ์—†๊ณ  ์ด๋ชจ์ง€๋‚˜ ์•„์ด์ฝ˜ ์ชผ๊ฐ€๋ฆฌ๋งŒ ๋ฐ•ํžŒ ๋ฒ„ํŠผ์ผ ๋•Œ ํ•„์ˆ˜ */}
</MagicButton>
  1. Tab ํฌ์ปค์Šค ๋ฐฉ์–ด๋ง‰: ๋งˆ์šฐ์Šค๋กœ ๋ชป ๋ˆ„๋ฅด๋Š” ์‚ฌ๋žŒ์€ ํ‚ค๋ณด๋“œ์˜ Tab๊ณผ Enter ํ‚ค๋งŒ์œผ๋กœ ํ™”๋ฉด ์ด๊ณณ์ €๊ณณ์„ ์˜ค๊ฐ‘๋‹ˆ๋‹ค. ์—ฌ๊ธฐ์„œ ๋งŒ์•ฝ ์ € ๋‹คํ˜•์„ฑ ๋ฒ„ํŠผ์ด div๋‚˜ span ํƒœ๊ทธ๋กœ ๋ Œ๋”๋ง ๋˜์—ˆ๋‹ค๋ฉด? (๊ณผ๊ฑฐ์—” ํ”ํžˆ ์ด๋žฌ์Œ) ์•„์˜ˆ ํ‚ค๋ณด๋“œ ์ดˆ์ ์ด ์žกํžˆ์ง€ ์•Š๊ณ  ๋งˆ์šฐ์Šค๋กœ๋งŒ ํด๋ฆญ์ด ๊ฐ€๋Šฅํ•œ ์“ฐ๋ ˆ๊ธฐ ๋ฒ„ํŠผ์ด ๋ฉ๋‹ˆ๋‹ค.

๊ทธ๋ ‡๊ธฐ์— ๊ธฐ๋ณธ์ ์œผ๋กœ ๋ธŒ๋ผ์šฐ์ €๊ฐ€ Tab ํ‚ค ์กฐ์ž‘์„ 100% ์ง€์›ํ•ด ์ฃผ๋Š” ๋„ค์ดํ‹ฐ๋ธŒ <button> ์š”์†Œ๋‚˜ <a> ํƒœ๊ทธ๋กœ ๋ณ€ํ˜•์‹œ์ผœ์•ผ๋งŒ ํ•˜๋Š” ์œ„ "๋‹คํ˜•์„ฑ ํŒจํ„ด"์ด, ๋‹จ์ˆœํžˆ ๋ณ€ํƒœ ๊ฐ™์€ ๊ฐœ๋ฐœ ๋งŒ์กฑ๋„๋ฅผ ๋„˜์–ด ์ˆ˜๋ฐฑ๋งŒ ์œ ์ €์˜ ์‹ค์ œ ์ ‘์† ์ƒ์กด์„ ์ฑ…์ž„์ง€๋Š” ์ฝ”์–ด๊ฐ€ ๋œ ๊ฒƒ์ž…๋‹ˆ๋‹ค.


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

๊ด€์ ๋‹จ์ผ ํƒœ๊ทธ ๋ฒ„ํŠผ <button>๋‹คํ˜•์„ฑ(Polymorphism) ๋ฒ„ํŠผ <Button as="a">
์žฅ์ ๋งŒ๋“ค๊ธฐ๊ฐ€ ๊ฐ€์žฅ ๋น ๋ฅด๋‹ค.์™„๋ฒฝํ•œ ๋””์ž์ธ CSS์˜ ๋‹จ์ผ ์†Œ์Šค ์›์น™. ํ•œ ๋ฒˆ ๋ฐ”๊ฟ”๋„ 1๋งŒ ๊ณณ์˜ ๋งํฌ ๋ฒ„ํŠผ๊ณผ ์ œ์ถœ ๋ฒ„ํŠผ์ด ๋˜‘๊ฐ™์ด ์ด๋ป์ง„๋‹ค.
๋‹จ์  / ์ œ์•ฝhref๋ฅผ ์“ธ ์ˆ˜ ์—†๊ณ  ์–ต์ง€๋กœ window.location.href= ํด๋ฆญ ์ด๋ฒคํŠธ๋ฅผ ๊ฐ•์ œํ•ด SEO ๊ตฌ๊ธ€ ๋ด‡(๊ฒ€์ƒ‰ ๋…ธ์ถœ)์ด ๋ฐ•์‚ด ๋‚œ๋‹ค.์ž‘์„ฑํ•  ๋•Œ TypeScript ํƒ€์ž… ํƒ€์ดํ•‘ ๋‚œ์ด๋„๊ฐ€ ์šฐ์ฃผ๋กœ ๊ฐ„๋‹ค. (์ตœ๊ทผ์—” Slot ํŒจํ„ด์ด ์ด ๋‹จ์ ์„ ์ƒ์‡„ ์ค‘)
์ ‘๊ทผ์„ฑ(a11y)์˜ ์ค‘์š”์„ฑ์Šคํฌ๋ฆฐ ๋ฆฌ๋” ๋ด‡(Bot)๋„ ์ด๋†ˆ์ด ๋ญ” ์ง“์„ ํ•˜๋Š” ๋ฒ„ํŠผ์ธ์ง€(๋ฒ„ํŠผ์˜ ์†์„ฑ) ๋ช…์พŒํ•˜๊ฒŒ ์•Œ ๊ถŒ๋ฆฌ๊ฐ€ ์žˆ๋‹ค! ์•„์ด์ฝ˜ ํˆฌ์„ฑ์ด๋ฉด ๊ผญ aria-label์„ ๋ฐ•์ž!

๐Ÿ’ก ํ•œ ์ค„๋กœ ๊ธฐ์–ตํ•˜๊ธฐ
"๋ชจ์–‘(๋””์ž์ธ)์€ ์นด๋ฉœ๋ ˆ์˜จ์ฒ˜๋Ÿผ ๊ฐ™๊ณ  ๋ผˆ๋Œ€(ํƒœ๊ทธ ๊ธฐ๋Šฅ)๋Š” ํŠธ๋žœ์Šคํฌ๋จธ์ฒ˜๋Ÿผ ์กฐ๋ฆฝํ•ด์•ผ ํ•˜๋Š” ์ˆœ๊ฐ„, ๋‹น์‹ ์ด ์ฅ˜ ์ˆ˜ ์žˆ๋Š” ๊ถ๊ทน์˜ ๊ฒ€์€ ๋ฐ”๋กœ 'Slot'๊ณผ 'as'์ด๋‹ค."


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

๋””์ž์ธ์ด ๋˜‘๊ฐ™์€๋ฐ ์†์„ฑ์ด ๋‹ค๋ฅด๋‹ค๊ณ  ๋ฒ„ํŠผ ์ปดํฌ๋„ŒํŠธ๋ฅผ ๋‘์„ธ ๊ฐœ์”ฉ ๋ณต์‚ฌ๋ณธ์œผ๋กœ ๋งŒ๋“ค๋˜ ๋‚ด ํ‘์—ญ์‚ฌ๊ฐ€ ์Šค์ณ ์ง€๋‚˜๊ฐ”๋‹ค. as๋ž‘ Slot ํŒจํ„ด์œผ๋กœ ์˜ท๋งŒ ์˜ˆ์˜๊ฒŒ ์ž…ํžˆ๊ณ  ์•Œ๋งน์ด๋Š” ์ž์œ ์ž์žฌ๋กœ ๋ฐ”๊พธ๋Š” ๋‹คํ˜•์„ฑ ๊ธฐ๋ฒ•์€ ์ง„์งœ '์œ ์ฒด์ดํƒˆ' ๊ทธ ์ž์ฒด๋‹ค.

๐Ÿ’ก "์‹œ๊ฐ ์žฅ์• ์ธ๋„, ๊ตฌ๊ธ€ ๊ฒ€์ƒ‰ ๋ด‡๋„ ์†์ด์ง€ ์•Š๋Š” ์ง„์งœ ์ปดํฌ๋„ŒํŠธ๋ฅผ ๋งŒ๋“ค์ž. ๊ป๋ฐ๊ธฐ์— ์ง‘์ฐฉํ•˜์ง€ ๋ง๊ณ  ๊ธฐ๋Šฅ์— ๋งž๋Š” ํƒœ๊ทธ๋กœ ๋งˆ์Œ๊ป ๋ณ€์‹ (asChild)์‹œ์ผœ๋ผ!"

๋‹จ์ˆœํžˆ ์˜ˆ์œ ๊ฒŒ ์žฅ๋•ก์ด ์•„๋‹ˆ๋ผ, ์ ‘๊ทผ์„ฑ(a11y)๊นŒ์ง€ ์ฑ™๊ฒจ์•ผ ๋Œ€๊ธฐ์—… ์ˆ˜์ค€์˜ ํ€„๋ฆฌํ‹ฐ๊ฐ€ ๋‚˜์˜จ๋‹ค๋Š” ์˜ํ˜ธ ์„ ๋ฐฐ์˜ ํŒฉํŠธ ํญํ–‰์ด ์ œ์ผ ์•„ํ”„๊ณ  ์‹œ์›ํ–ˆ๋‹ค. ์ด์ œ div์— onClick ๋ฐ”๋ฅด๋Š” ๋”์ฐํ•œ ์ง“์€ ๋‚ด ๊นƒํ—ˆ๋ธŒ์—์„œ ์˜์›ํžˆ ํ‡ด์ถœ์ด๋‹ค.


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

Q1. ๋‹น์‹ ์ด 5๋…„ ๋งŒ์— ํ† ์Šค, ๋ฐฐ๋ฏผ๊ณผ ๊ฐ™์€ ์ดˆ๋Œ€ํ˜• ๊ธฐ์—…์˜ ํ”„๋ก ํŠธ์—”๋“œ ๊ณตํ†ต ์ปดํฌ๋„ŒํŠธ(Design System) ๊ฐœ๋ฐœํŒ€์— ํ•ฉ๋ฅ˜ํ–ˆ์Šต๋‹ˆ๋‹ค. ์ฒซ ์ž„๋ฌด๋กœ Button ์ปดํฌ๋„ŒํŠธ๋ฅผ ์„ค๊ณ„ํ•˜๋Š”๋ฐ, ๊ธฐ์กด ํŒ€์› ์˜์ฒ ์ด๊ฐ€ ๋งŒ๋“ค์–ด๋‘” ์ฝ”๋“œ๋Š” ์•„๋ž˜์™€ ๊ฐ™์Šต๋‹ˆ๋‹ค. ์‹œ๋‹ˆ์–ด์˜ ๊ด€์ ์—์„œ, ์˜์ฒ ์ด์˜ ์ฝ”๋“œ๋ฅผ ๋ฐ˜๋ ค์‹œํ‚ค๋ฉฐ ์ง€์ ํ•ด์•ผ ํ•  ๋‹คํ˜•์„ฑ๊ณผ SEO, ์ ‘๊ทผ์„ฑ(a11y) ์ธก๋ฉด์˜ ๊ฐ€์žฅ ๋”์ฐํ•œ ๋‹จ์ ์„ ๊ณจ๋ผ๋ณด์„ธ์š”.

function SimpleButton({ title, url }) {
  // ์‚ฌ์šฉ์ž๊ฐ€ url์„ ๋„˜๊ธฐ๋ฉด ์ž๋ฐ”์Šคํฌ๋ฆฝํŠธ๋กœ ๊ฐ•์ œ๋กœ ์ฐฝ์„ ์ด๋™์‹œํ‚จ๋‹ค!
  const pushLink = () => window.location.href = url;
  
  return <div className="btn" onClick={url ? pushLink : undefined}>{title}</div>
}
  • A) div ์ชผ๊ฐ€๋ฆฌ๋ฅผ ๋ฆฌ์•กํŠธ๊ฐ€ ๋ฑ‰์–ด๋‚ด๋Š” ์ˆœ๊ฐ„ ์ปดํŒŒ์ผ ํƒ€์ž„์— ๋ธŒ๋ผ์šฐ์ €๊ฐ€ VDOM ์ฃผ์ž…์„ ๊ฑฐ๋ถ€ํ•˜๋ฉฐ ๋นจ๊ฐ„ ์—๋Ÿฌ ์ฐฝ์„ ๋„์šด๋‹ค.
  • B) ์ด ๋ฒ„ํŠผ์€ ๊ตฌ๊ธ€ ๊ฒ€์ƒ‰ ์—”์ง„ ๋กœ๋ด‡(Bot)์ด ํŒŒ์‹ฑํ•  ๋•Œ ๊ทธ์ € "์•„๋ฌด๋Ÿฐ ์ด๋™ ๊ธฐ๋Šฅ์ด ์—†๋Š” ๊ธ€์ž ํ•œ ์ค„(div๋ธ”๋ก)"์œผ๋กœ ์ทจ๊ธ‰ํ•˜๋ฏ€๋กœ ์‚ฌ์ดํŠธ ๊ฐ„ ์—ฐ๊ฒฐ ๊ณ ๋ฆฌ(SEO)๊ฐ€ ํŒŒ๊ดด๋œ๋‹ค. ๋˜ํ•œ, ํ‚ค๋ณด๋“œ Tab ํ‚ค๋ฅผ ๋ˆ„๋ฅด๋Š” ์‹œ๊ฐ/์ง€์ฒด ์žฅ์• ์ธ ์œ ์ €๋“ค์ด ์ด ๋ฒ„ํŠผ์— ๋„๋‹ฌ์กฐ์ฐจ ํ•  ์ˆ˜ ์—†๊ณ , ์—”ํ„ฐ ํ‚ค๋ฅผ ์ณ๋„ ์•„๋ฌด ์ผ๋„ ๋ฒŒ์–ด์ง€์ง€ ์•Š๋Š”๋‹ค.
  • C) ์ด ๋ฒ„ํŠผ์€ div ํƒœ๊ทธ๋กœ ์–ฝ๋งค์—ฌ ์žˆ๊ธฐ์— ์Šคํƒ€์ผ(className="btn")์ด ๋ฐ–์œผ๋กœ ์ƒˆ์–ด ๋‚˜๊ฐ€ ๊ธ€๋กœ๋ฒŒ ์˜ค์—ผ์„ ์ผ์œผํ‚ค๋Š” ๋ฒ„๊ทธ๋ฅผ ์œ ๋ฐœํ•œ๋‹ค.

โœ… ์ •๋‹ต: B

๐Ÿ’ก ์ƒ์„ธ ํ•ด์„ค: ๋งŒ์ ์— ๊ฐ€๊นŒ์šด ํ•ด์„ค์ž…๋‹ˆ๋‹ค. div์— ์–ด๊ฑฐ์ง€๋กœ onClick์„ ๋ฐ”๋ฅด๊ณ  ์•ต์ปค ์ด๋™ ํ‰๋‚ด(์ž๋ฐ”์Šคํฌ๋ฆฝํŠธ ๊ฐ•์ œ ๋ผ์šฐํŒ…)๋ฅผ ๋‚ด๋Š” ํ–‰์œ„๋Š” ํ”„๋ก ํŠธ์—”๋“œ ์ดˆ๋ณด์ž๋“ค์ด ๊ฐ€์žฅ ๋งŽ์ด ์ €์ง€๋ฅด๋Š” ์ตœ์•…์˜ ์•ˆํ‹ฐ ํŒจํ„ด(์ ‘๊ทผ์„ฑ ํŒŒ๊ดด)์ž…๋‹ˆ๋‹ค. ์ด๋Ÿฐ ๋”์ฐํ•œ ์‚ฌํƒœ๋ฅผ ๋ง‰์œผ๋ ค๋ฉด ๊ฒ€์ƒ‰ ๋ด‡๊ณผ ์‹œ๊ฐ์žฅ์• ์ธ ๋ชจ๋‘๊ฐ€ ํ•ฉ๋ฒ•์ ์œผ๋กœ ์ธ์‹ํ•  ์ˆ˜ ์žˆ๋Š” ์‹ค์ œ <a> ํƒœ๊ทธ(๋‹คํ˜•์„ฑ)๋กœ ์™„์ „ํžˆ ํŠธ๋žœ์Šคํผํ•˜์—ฌ ๋ผˆ๋Œ€ ์ž์ฒด๋ฅผ ๊ฐˆ์•„์ฃผ์–ด์•ผ๋งŒ ํ•ฉ๋‹ˆ๋‹ค.

Q2. ์ตœ๊ทผ์˜ ํž™์Šคํ„ฐ๊ธ‰ ๋ชจ๋˜ UI ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ(Radix UI ๋“ฑ)๊ฐ€ ์ œ๊ณตํ•˜๋Š” asChild (ํ˜น์€ Slot ํŒจํ„ด)๊ฐ€, ๊ณผ๊ฑฐ 1์„ธ๋Œ€ as="a" ๋ฐฉ์‹์„ ์ œ์น˜๊ณ  ๋Œ€๊ธฐ์—… ๋ฉ”์ธ์ŠคํŠธ๋ฆผ(์ฃผ๋ฅ˜)์œผ๋กœ ๋“ฑ๊ทนํ•˜๊ฒŒ ๋œ ํ•ต์‹ฌ์ ์ธ ๊ฐœ๋ฐœ ๊ฒฝํ—˜(DX)์  ์ด์œ ๋ฅผ ์ฃผ๊ด€์‹ ํ•˜๋‚˜๋กœ ๋น„์œ ํ•˜๊ฑฐ๋‚˜ ๋ฌ˜์‚ฌํ•ด๋ณด์„ธ์š”.
โœ… ์ •๋‹ต ๋ฐ ์ฃผ๊ด€์‹ ํ•ด์„ค:
"๊ณผ๊ฑฐ as="a" ๋ฐฉ์‹์€ ๋ถ€๋ชจ ์ปดํฌ๋„ŒํŠธ๊ฐ€ href๋‚˜ target="_blank" ๊ฐ™์€ ์ž์‹์ด ํ•„์š”ํ•œ ์†์„ฑ๋“ค์„ ์–ด๋–ป๊ฒŒ๋“  ๋‹ค ์ „๋‹ฌ๋ฐ›์•„์„œ ๋Œ€์‹  ์งฌ์ฒ˜๋ฆฌ ํ•ด๊ฐ€๋ฉฐ ๋ณต์žกํ•œ ์ œ๋„ค๋ฆญ ํƒ€์ž… ์—๋Ÿฌ์™€ ์‹ธ์šฐ๋Š” '์˜ค์ง€๋ž–์˜ ํ™”์‹ ' ๊ตฌ์กฐ์˜€์Šต๋‹ˆ๋‹ค. ๋ฐ˜๋ฉด asChild(Slot) ํŒจํ„ด์€, ๋ถ€๋ชจ๋Š” ๊ทธ์ € ๋ฉ‹์ง„ ํ™ฉ๊ธˆ์ƒ‰ ๊ฐ‘์˜ท ๊ป๋ฐ๊ธฐ(๋””์ž์ธ CSS์™€ ๊ณตํ†ต ์ด๋ฒคํŠธ)๋งŒ ๋”ธ๋ž‘ ๋˜์ ธ๋‘๊ณ  ํญ์‚ฌํ•ด๋ฒ„๋ฆฌ๋ฉด, ๊ทธ ๊ฐ‘์˜ท์ด ๋ฐ‘์˜ ์‹ค์ œ ํƒœ๊ทธ ์ž์‹์—๊ฒŒ ๊ธฐ์ƒ์ˆ˜์ฒ˜๋Ÿผ ์•Œ์•„์„œ ์ฐฐ์‹น ์Šค๋ฉฐ๋“ค์–ด ๋ณ‘ํ•ฉ ๋ Œ๋”๋ง๋˜๋Š” ๊ธฐ์ ์„ ๋ฐœํœ˜ํ•˜๋ฏ€๋กœ Props ์ถฉ๋Œ ๊ณ ๋ฏผ์ด ์›์ฒœ ์†Œ๋ฉธํ•˜๋Š” ๊ถ๊ทน์˜ ํ™•์žฅ์„ฑ์„ ์ง€๋‹ˆ๊ธฐ ๋•Œ๋ฌธ์ž…๋‹ˆ๋‹ค."