Radix-ui ์š”์  ์ •๋ฆฌ ๐ŸŒŸ

ํ˜œํ˜œยท2024๋…„ 9์›” 25์ผ
7

UI

๋ชฉ๋ก ๋ณด๊ธฐ
1/1
post-thumbnail

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

radix-ui ๊ณต์‹ ํ™ˆํŽ˜์ด์ง€
radix-ui ๊ณต์‹ github

๐Ÿ“Œ Introduction

๐ŸŒŸ ๊ณ ํ’ˆ์งˆ, ์ ‘๊ทผ ๊ฐ€๋Šฅํ•œ ๋””์ž์ธ ์‹œ์Šคํ…œ๊ณผ ์›น์•ฑ์„ ๊ตฌ์ถ•ํ•˜๋Š” ๋ฐ ์‚ฌ์šฉํ•˜๋Š” ์˜คํ”ˆ์†Œ์Šค UI ์ปดํฌ๋„ŒํŠธ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ

  • Radix Primitives๋Š” ์ ‘๊ทผ ๊ฐ€๋Šฅ์„ฑ, ์ปค์Šคํ…€, DX์— ์ง‘์ค‘ํ•œ low-level UI ์ปดํฌ๋„ŒํŠธ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ

  • ๊ทธ๋งŒํผ ๊ฐœ๋ฐœ์ž๊ฐ€ ์ปค์Šคํ…€ ํ•˜๊ธฐ ์‰ฌ์šด headless UI ๊ฐ™์€ ๋А๋‚Œ์ž…๋‹ˆ๋‹ค

  • ๋…๋ณด์ ์ธ UI/UX๋ฅผ ๊ฐ€์ง„ ํŒ€๋“ค์ด ์‚ฌ์šฉํ•˜๊ธฐ์— ์ข‹์„ ๊ฒƒ ๊ฐ™์€!

  • ์‚ฌ์šฉ์ž(๊ฐœ๋ฐœ์ž) ๊ฐœ์ธ์˜ ๋””์ž์ธ ์‹œ์Šคํ…œ์˜ base layer๋กœ์„œ ์‚ฌ์šฉํ•˜๊ธฐ์—๋„ ์ข‹๊ณ ,
    ์ ์ง„์ ์œผ๋กœ ์ ์šฉํ•ด๋‚˜๊ฐ€๊ธฐ๋„ ์ข‹๋‹ค!

โœ… Vision

  • accordion, checkbox, combobox, dialog, dropdown, select ๋“ฑ๋“ฑ ๊ธฐ๋ณธ UI ํŒจํ„ด๋“ค์„ ๋”ฑ ๋ดค์„ ๋•Œ, ์šฐ๋ฆฌ์—๊ฒŒ ๊ณต์œ ๋˜๋Š” ๊ธฐ๋ณธ์ ์ธ ๊ฐœ๋…์ด ์žˆ์Œ
  • ํ•˜์ง€๋งŒ ์›น ํ”Œ๋žซํผ์—์„œ ์ œ๊ณตํ•˜๋Š” ์ด๋Ÿฐ ์ปดํฌ๋„ŒํŠธ๋“ค์— ๋Œ€ํ•œ ๊ตฌํ˜„์€ ๋ถˆ์ถฉ๋ถ„ํ•˜๋‹ค!
  • ๋”ฐ๋ผ์„œ ๊ฐœ๋ฐœ์ž๋“ค์€ ๊ฐ•์ œ์ ์œผ๋กœ ์ปค์Šคํ…€ ์ปดํฌ๋„ŒํŠธ๋ฅผ ๋งŒ๋“ค ์ˆ˜๋ฐ–์— ์—†์—ˆ๊ณ , ์ด๋Š” ์–ด๋ ค์šด ์ž‘์—…์ด๋‹ค...

โ†’ radix-ui์˜ ๋ชฉํ‘œ๋Š” ๊ฐœ๋ฐœ์ž ์ปค๋ฎค๋‹ˆํ‹ฐ๊ฐ€ ์‰ฝ๊ฒŒ ๋””์ž์ธ ์‹œ์Šคํ…œ์„ ๊ตฌ์ถ•ํ•  ์ˆ˜ ์žˆ๋„๋ก ๋•๋Š” ์˜คํ”ˆ์†Œ์Šค ์ปดํฌ๋„ŒํŠธ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ๋งŒ๋“œ๋Š” ๊ฒƒ!

โœ… Key Features

1. Accessible

  • WAI-ARIA Design Patterns ํ™œ์šฉ
  • arai, role ์†์„ฑ๊ณผ focus, ํ‚ค๋ณด๋“œ navigation ๋“ฑ ์ ‘๊ทผ์„ฑ๊ณผ ๊ด€๋ จ๋œ ๋งŽ์€ ๋””ํ…Œ์ผ๋“ค์„ ์ฒ˜๋ฆฌํ•จ

2. Unstyled

  • ์ปดํฌ๋„ŒํŠธ๋Š” ์Šคํƒ€์ผ ์—†์ด ์ œ๊ณต๋จ โ†’ ์‚ฌ์šฉ์ž(๊ฐœ๋ฐœ์ž)๊ฐ€ ๋ง์ž…ํž ์ˆ˜ ์žˆ๊ฒŒ!
  • vanilla CSS, CSS preprocessors, CSS-in-JS ๋“ฑ ์–ด๋–ค ๊ฒƒ์œผ๋กœ๋“  ๋ง์ž…ํ˜€์งˆ ์ˆ˜ ์žˆ๋‹ค

3. Opened

  • ์‚ฌ์šฉ์ž์˜ ๋‹ˆ์ฆˆ์— ๋งž๊ฒŒ ์ปค์Šคํ…€ ๊ฐ€๋Šฅํ•˜๋„๋ก ์„ค๊ณ„๋จ
  • ๊ฐ ์ปดํฌ๋„ŒํŠธ ๋ถ€๋ถ„๋งˆ๋‹ค ์„ธ๋ถ„ํ™”๋œ ์ ‘๊ทผ์„ ์ œ๊ณต โ†’ ๋ถ€๋ถ„๋ถ€๋ถ„๋งˆ๋‹ค ์ด๋ฒคํŠธ ๋ฆฌ์Šค๋„ˆ, props, refs ๋“ฑ๋“ฑ ๋‹ค์–‘ํ•˜๊ฒŒ ์ถ”๊ฐ€ ๊ฐ€๋Šฅ

4. Uncontrolled

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

5. Developer experience

  • Full-typed API ์ œ๊ณต
  • ๋ชจ๋“  ์ปดํฌ๋„ŒํŠธ๋Š” ์ผ๊ด€๋˜๊ณ  ์˜ˆ์ธก ๊ฐ€๋Šฅํ•œ ๊ฐœ๋ฐœ์ž ๊ฒฝํ—˜์„ ๋งŒ๋“œ๋Š” ๋น„์Šทํ•œ API๋ฅผ ์„œ๋กœ ๊ณต์œ ํ•จ

6. Incremental Adoption

๊ฐ Primitives๋Š” ๊ฐœ๋ณ„์ ์œผ๋กœ ์„ค์น˜ ๊ฐ€๋Šฅ

npm install @radix-ui/react-dialog 
npm install @radix-ui/react-dropdown-menu 
npm install @radix-ui/react-tooltip 

๋ฒ„์ €๋‹๋„ ๋…๋ฆฝ์ ์œผ๋กœ ๋œ๋‹ค๊ณ  ํ•จ (๋ฏธ์ณค๋‹ค)

๐Ÿ“Œ Getting Started

npm install @radix-ui/{ํŠน์ • radix primitive}@latest -E 

์ฐธ๊ณ ๋กœ -E๋Š” --save-exact๋กœ ๋ฒ„์ „๋ช…๊ณผ ์ •ํ™•ํžˆ ์ผ์น˜ํ•˜๋Š” ํŒจํ‚ค์ง€๋ฅผ ๋ฐ›์Œ

// index.jsx 
import * as React from 'react'; 
import * as Popover from '@radix-ui/react-popover'; 

const PopoverDemo = () => ( 
  <Popover.Root> 
    <Popover.Trigger>More info</Popover.Trigger> 
    <Popover.Portal> 
      <Popover.Content> 
        Some more infoโ€ฆ 
        <Popover.Arrow /> 
      </Popover.Content> 
    </Popover.Portal> 
  </Popover.Root> 
); 

export default PopoverDemo; 

๊ฐ€์ ธ์˜จ primitive๊ฐ€ Popover๋ผ๊ณ  ํ–ˆ์„ ๋•Œ, ์ด๋Ÿฐ ์‹์œผ๋กœ ์‚ฌ์šฉ ๊ฐ€๋Šฅ

// index.jsx 
import * as React from 'react'; 
import * as Popover from '@radix-ui/react-popover'; 
import './styles.css'; 

const PopoverDemo = () => ( 
  <Popover.Root> 
    <Popover.Trigger className="PopoverTrigger">Show info</Popover.Trigger> 
    <Popover.Portal> 
      <Popover.Content className="PopoverContent"> 
        Some content 
        <Popover.Arrow className="PopoverArrow" /> 
      </Popover.Content> 
    </Popover.Portal> 
  </Popover.Root> 
); 

export default PopoverDemo; 
/* styles.css */ 
.PopoverTrigger { 
  background-color: white; 
  border-radius: 4px; 
} 

.PopoverContent { 
  border-radius: 4px; 
  padding: 20px; 
  width: 260px; 
  background-color: white; 
} 

.PopoverArrow { 
  fill: white; 
} 

์Šคํƒ€์ผ์„ ์ถ”๊ฐ€ํ•˜๋ ค๋ฉด ์ด๋Ÿฐ ์‹์œผ๋กœ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋‹ค

๐Ÿ“Œ Styling

.AccordionItem { 
  border-bottom: 1px solid gainsboro; 
} 

.AccordionItem[data-state='open'] { 
  border-bottom-width: 2px; 
} 

์ƒํƒœ์— ๋”ฐ๋ผ ์Šคํƒ€์ผ์ด ๋‹ฌ๋ผ์งˆ ์ˆ˜ ์žˆ๋Š” ๋ถ€๋ถ„๋“ค์€ ์ด๋ฏธ radix ๋‹จ์—์„œ data-state๊ฐ€ ์ง€์ •๋˜์–ด ์žˆ๋‹ค
๋”ฐ๋ผ์„œ data-state์— ๋งž๊ฒŒ ์Šคํƒ€์ผ์„ ์ ์šฉํ•ด ์ค„ ์ˆ˜ ์žˆ๋‹ค!

๐Ÿ“Œ Animation

@keyframes fadeIn { 
  from { 
    opacity: 0; 
  } 

  to { 
    opacity: 1; 
  } 
} 

@keyframes fadeOut { 
  from { 
    opacity: 1; 
  } 

  to { 
    opacity: 0; 
  } 
} 

  

.DialogOverlay[data-state='open'], 
.DialogContent[data-state='open'] { 
  animation: fadeIn 300ms ease-out; 
}

.DialogOverlay[data-state='closed'], 
.DialogContent[data-state='closed'] { 
  animation: fadeOut 300ms ease-in; 
} 

์• ๋‹ˆ๋ฉ”์ด์…˜๋„ ์œ„์™€ ๊ฐ™์ด ์˜ˆ์ธก ๊ฐ€๋Šฅํ•œ ํ˜•ํƒœ๋กœ ์ค„ ์ˆ˜ ์žˆ๋‹ค

๐Ÿ“Œ Composition

Radix์˜ ๊ธฐ๋Šฅ๊ณผ ์‚ฌ์šฉ์ž์˜ ์ปดํฌ๋„ŒํŠธ๋ฅผ compose ํ•˜๋ ค๋ฉด, asChild prop์„ ์‚ฌ์šฉํ•˜์ž!!

Element type ๋ณ€๊ฒฝ

  • ๋Œ€๋ถ€๋ถ„์˜ ๊ฒฝ์šฐ Radix์˜ element type์„ ๋ฐ”๊พธ๊ฒŒ ๋  ์ผ์€ ์—†๊ธด ํ•˜์ง€๋งŒ ๋ฐฉ๋ฒ•์€ ์žˆ๋‹ค
  • Tooltip.Trigger๊ฐ€ ์ข‹์€ ์˜ˆ์‹œ
  • ๋””ํดํŠธ๋กœ๋Š” button์— ๋ถ™์–ด์„œ hover ํ–ˆ์„ ๋•Œ Tooltip์ด ๋‚˜ํƒ€๋‚˜๊ฒŒ ๋˜์–ด ์žˆ๋Š”๋ฐ, a ๋งํฌ์— ๋‚˜ํƒ€๋‚˜๊ฒŒ ์ˆ˜์ •ํ•  ์ˆ˜ ์žˆ๋‹ค
import * as React from 'react'; 
import * as Tooltip from '@radix-ui/react-tooltip'; 

export default () => ( 
  <Tooltip.Root> 
    <Tooltip.Trigger asChild> 
      <a href="https://www.radix-ui.com/">Radix UI</a> 
    </Tooltip.Trigger> 
    <Tooltip.Portal>โ€ฆ</Tooltip.Portal> 
  </Tooltip.Root> 
); 
  • asChild๋Š” ๋ฌธ์ž ๊ทธ๋Œ€๋กœ child๋กœ์„œ ์“ฐ์ธ๋‹ค๋Š” ์˜๋ฏธ์ธ๋“ฏ
  • ๋ณดํ†ต ์ด๋ ‡๊ฒŒ ์ˆ˜์ •ํ•ด์„œ ์‚ฌ์šฉํ•˜๋Š” ๊ฒฝ์šฐ๋Š” ๋“œ๋ฌผ๊ณ , ์‚ฌ์šฉ์ž๊ฐ€ ๋งŒ๋“  ๋ฆฌ์•กํŠธ ์ปดํฌ๋„ŒํŠธ๋ฅผ ์‚ฌ์šฉํ•˜๊ฒŒ ๋  ๊ฒƒ
  • ๊ทธ๋ž˜๋„ ์•Œ์•„๋‘๋ฉด ์ข‹๋‹ค!

๋‚ด๊ฐ€ ๋งŒ๋“  React ์ปดํฌ๋„ŒํŠธ์™€ Composing

  • asChild์™€ ์›๋ฆฌ๋Š” ๊ฑฐ์˜ ์œ ์‚ฌํ•จ
  • ํ•˜์ง€๋งŒ ๋ช‡ ๊ฐ€์ง€ ์ฃผ์˜ํ•ด์•ผ ํ•  ๋ถ€๋ถ„์ด ์žˆ๋‹ค

1. props

  • Radix๋Š” ์‚ฌ์šฉ์ž์˜ component๋ฅผ ๋ณต์ œํ•  ๋•Œ ๊ทธ๊ฒƒ์˜ props์™€ ์ด๋ฒคํŠธ ํ•ธ๋“ค๋Ÿฌ๋ฅผ passํ•จ
  • ๋งŒ์•ฝ ์‚ฌ์šฉ์ž์˜ component๊ฐ€ ์ด๋Ÿฌํ•œ props๋ฅผ ์ง€์›ํ•˜์ง€ ์•Š๋Š”๋‹ค๋ฉด, ์˜ค๋ฅ˜ ๋ฐœ์ƒ!
  • ์ด๋Š” ๋ชจ๋“  props๋ฅผ DOM ๋…ธ๋“œ์— spread ํ•˜์—ฌ ์ˆ˜ํ–‰ํ•จ
// before 
const MyButton = () => <button />; 

// after 
const MyButton = (props) => <button {...props} />; 

2. forwardRef

  • Radix๋Š” ์ข…์ข… ์‚ฌ์šฉ์ž์˜ ์ปดํฌ๋„ŒํŠธ์— ref๋ฅผ ๋ถ™์—ฌ์•ผ ํ•˜๋Š” ๊ฒฝ์šฐ๊ฐ€ ์žˆ์Œ
  • ์ด๋•Œ ์ปดํฌ๋„ŒํŠธ๊ฐ€ ref๋ฅผ ๋ฐ›์ง€ ์•Š๋Š”๋‹ค๋ฉด, ์˜ค๋ฅ˜ ๋ฐœ์ƒ!
  • React.forwardRef๋ฅผ ํ†ตํ•ด ์ˆ˜ํ–‰๋จ
// before 
const MyButton = (props) => <button {...props} />; 

// after 
const MyButton = React.forwardRef((props, forwardedRef) => ( 
  <button {...props} ref={forwardedRef} /> 
)); 

๋ชจ๋“  ๋ถ€๋ถ„์— ํ•„์š”ํ•œ ๊ฑด ์•„๋‹ˆ์ง€๋งŒ, ๊ทธ๋ƒฅ ์Šต๊ด€์ฒ˜๋Ÿผ ํ•ญ์ƒ ํ•˜๋Š” ๊ฑธ ๊ถŒ์žฅํ•œ๋‹ค๊ณ  ํ•จ

Radix primitives๋ผ๋ฆฌ Composing

  • asChild๋Š” depth๋ฅผ ์ค‘์ฒฉํ•ด์„œ ๊นŠ๊ฒŒ ์‚ฌ์šฉํ•  ์ˆ˜๋„ ์žˆ์Œ
  • Tooltip.Trigger์™€ Dialog.Trigger๋ฅผ ๊ฐ™์ด ์‚ฌ์šฉํ•˜๋Š” ์˜ˆ์‹œ
import * as React from 'react'; 
import * as Dialog from '@radix-ui/react-dialog'; 
import * as Tooltip from '@radix-ui/react-tooltip'; 

const MyButton = React.forwardRef((props, forwardedRef) => ( 
  <button {...props} ref={forwardedRef} /> 
)); 

export default () => { 
  return ( 
    <Dialog.Root> 
      <Tooltip.Root> 
        <Tooltip.Trigger asChild> 
          <Dialog.Trigger asChild> 
            <MyButton>Open dialog</MyButton> 
          </Dialog.Trigger> 
        </Tooltip.Trigger> 
        <Tooltip.Portal>โ€ฆ</Tooltip.Portal> 
      </Tooltip.Root> 
      <Dialog.Portal>...</Dialog.Portal> 
    </Dialog.Root> 
  ); 
}; 
  • ์ด๋ ‡๊ฒŒ ํ•˜๋ฉด hover ํ–ˆ์„ ๋•Œ๋Š” Trigger๊ฐ€ ๋‚˜์˜ค๊ณ , ๋ˆŒ๋ €์„ ๋•Œ๋Š” Dialog๊ฐ€ ๋‚˜์˜ฌ ์ˆ˜ ์žˆ๊ฒŒ, Button ํ•˜๋‚˜์— Trigger, Dialog๊ฐ€ ๊ฑธ๋ฆฌ๊ฒŒ ํ•  ์ˆ˜ ์žˆ๋Š” ๋“ฏ!

๐Ÿ“Œ Server-side rendering

Radix primitive๋Š” SSR์ด๋“  Static rendering์—์„œ๋“  ์‚ฌ์šฉ ๊ฐ€๋Šฅ!

๐Ÿ“Œ Primitives ์ข…๋ฅ˜

ํ˜„์žฌ ํ•˜๊ณ  ์žˆ๋Š” ํ”„๋กœ์ ํŠธ์—์„œ ์“ธ ์ˆ˜ ์žˆ์„ ๋งŒํ•œ primitive๋ฅผ ๋ช‡ ๊ฐ€์ง€ ์ •๋ฆฌํ•ด ๋ณด์ž! tailwind css๋ฅผ ์‚ฌ์šฉํ•˜๊ฒŒ ๋  ๊ฒƒ ๊ฐ™๊ธด ํ•˜์ง€๋งŒ, ๋ธ”๋กœ๊ทธ ํฌ์ŠคํŒ…์šฉ ์ •๋ฆฌ์ด๊ธฐ ๋•Œ๋ฌธ์— ๊ฐ€๋…์„ฑ์ƒ CSS๋ฅผ ์ด์šฉํ•œ ์ฝ”๋“œ๋กœ ๊ฐ€์ ธ์™€ ๋ณด์•˜๋‹ค.

โœ… Accordion

Accordion

import React from 'react'; 
import * as Accordion from '@radix-ui/react-accordion'; 
import classNames from 'classnames'; 
import { ChevronDownIcon } from '@radix-ui/react-icons'; 
import './styles.css'; 

const AccordionDemo = () => ( 
  <Accordion.Root className="AccordionRoot" type="single" defaultValue="item-1" collapsible> 
    <Accordion.Item className="AccordionItem" value="item-1"> 
      <AccordionTrigger>Is it accessible?</AccordionTrigger> 
      <AccordionContent>Yes. It adheres to the WAI-ARIA design pattern.</AccordionContent> 
    </Accordion.Item> 

    <Accordion.Item className="AccordionItem" value="item-2"> 
      <AccordionTrigger>Is it unstyled?</AccordionTrigger> 
      <AccordionContent> 
        Yes. It's unstyled by default, giving you freedom over the look and feel. 
      </AccordionContent> 
    </Accordion.Item> 

    <Accordion.Item className="AccordionItem" value="item-3"> 
      <AccordionTrigger>Can it be animated?</AccordionTrigger> 
      <Accordion.Content className="AccordionContent"> 
        <div className="AccordionContentText"> 
          Yes! You can animate the Accordion with CSS or JavaScript. 
        </div> 
      </Accordion.Content> 
    </Accordion.Item> 
  </Accordion.Root> 
); 

const AccordionTrigger = React.forwardRef(({ children, className, ...props }, forwardedRef) => ( 
  <Accordion.Header className="AccordionHeader"> 
    <Accordion.Trigger 
      className={classNames('AccordionTrigger', className)} 
      {...props} 
      ref={forwardedRef} 
    > 
      {children} 
      <ChevronDownIcon className="AccordionChevron" aria-hidden /> 
    </Accordion.Trigger> 
  </Accordion.Header> 
)); 

  

const AccordionContent = React.forwardRef(({ children, className, ...props }, forwardedRef) => ( 
  <Accordion.Content 
    className={classNames('AccordionContent', className)} 
    {...props} 
    ref={forwardedRef} 
  > 
    <div className="AccordionContentText">{children}</div> 
  </Accordion.Content> 

)); 

export default AccordionDemo; 
  • forwardRef๋กœ AccordionTrigger, AccordionContent๋ฅผ ์ง€์ •ํ•ด ์คŒ์œผ๋กœ์จ ๋ถ€๋ชจ ์š”์†Œ์—์„œ ํ•ด๋‹น ์š”์†Œ๋“ค์— ์ ‘๊ทผํ•  ์ˆ˜ ์žˆ๊ฒŒ ํ•จ
  • ์ฐธ๊ณ  ๋งํฌ
  • ์ผ๋ฐ˜์ ์œผ๋กœ ์ปดํฌ๋„ŒํŠธ์˜ DOM ๋…ธ๋“œ๋Š” ๋น„๊ณต๊ฐœ
  • ํ•˜์ง€๋งŒ forwardRef๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ์ž์‹์„ ๋ถ€๋ชจ์—๊ฒŒ ๋…ธ์ถœ์‹œํ‚ฌ ์ˆ˜ ์žˆ๋‹ค!

โœ… Checkbox

Checkbox

import React from 'react'; 
import * as Checkbox from '@radix-ui/react-checkbox'; 
import { CheckIcon } from '@radix-ui/react-icons'; 
import './styles.css'; 

const CheckboxDemo = () => ( 
  <form> 
    <div style={{ display: 'flex', alignItems: 'center' }}> 
      <Checkbox.Root className="CheckboxRoot" defaultChecked id="c1"> 
        <Checkbox.Indicator className="CheckboxIndicator"> 
          <CheckIcon /> 
        </Checkbox.Indicator> 
      </Checkbox.Root> 
      <label className="Label" htmlFor="c1"> 
        Accept terms and conditions. 
      </label> 
    </div> 
  </form> 
); 

export default CheckboxDemo; 

โœ… Dialog

Dialog

import React from 'react'; 
import * as Dialog from '@radix-ui/react-dialog'; 
import { Cross2Icon } from '@radix-ui/react-icons'; 
import './styles.css'; 

const DialogDemo = () => ( 
  <Dialog.Root> 
    <Dialog.Trigger asChild> 
      <button className="Button violet">Edit profile</button>
    </Dialog.Trigger> 
    <Dialog.Portal> 
      <Dialog.Overlay className="DialogOverlay" /> 
      <Dialog.Content className="DialogContent"> 
        <Dialog.Title className="DialogTitle">Edit profile</Dialog.Title> 
        <Dialog.Description className="DialogDescription"> 
          Make changes to your profile here. Click save when you're done. 
        </Dialog.Description> 
        <fieldset className="Fieldset"> 
          <label className="Label" htmlFor="name"> 
            Name 
          </label> 
          <input className="Input" id="name" defaultValue="Pedro Duarte" /> 
        </fieldset> 
        <fieldset className="Fieldset"> 
          <label className="Label" htmlFor="username"> 
            Username 
          </label> 
          <input className="Input" id="username" defaultValue="@peduarte" /> 
        </fieldset> 
        <div style={{ display: 'flex', marginTop: 25, justifyContent: 'flex-end' }}> 
          <Dialog.Close asChild> 
            <button className="Button green">Save changes</button> 
          </Dialog.Close> 
        </div> 
        <Dialog.Close asChild> 
          <button className="IconButton" aria-label="Close"> 
            <Cross2Icon /> 
          </button> 
        </Dialog.Close> 
      </Dialog.Content> 
    </Dialog.Portal> 
  </Dialog.Root> 
); 

export default DialogDemo; 

โœ… Form

Form

Built-in validation๊ณผ custom validation ๋ชจ๋‘ ์ œ๊ณตํ•˜๋Š” ์•ผ๋ฌด์ง„ ๋…€์„์ด๋‹ค...

import React from 'react'; 
import * as Form from '@radix-ui/react-form'; 
import './styles.css'; 

const FormDemo = () => ( 
  <Form.Root className="FormRoot"> 
    <Form.Field className="FormField" name="email"> 
      <div style={{ display: 'flex', alignItems: 'baseline', justifyContent: 'space-between' }}> 
        <Form.Label className="FormLabel">Email</Form.Label> 
        <Form.Message className="FormMessage" match="valueMissing"> 
          Please enter your email 
        </Form.Message> 
        <Form.Message className="FormMessage" match="typeMismatch"> 
          Please provide a valid email 
        </Form.Message> 
      </div> 
      <Form.Control asChild> 
        <input className="Input" type="email" required /> 
      </Form.Control> 
    </Form.Field> 
    <Form.Field className="FormField" name="question"> 
      <div style={{ display: 'flex', alignItems: 'baseline', justifyContent: 'space-between' }}> 
        <Form.Label className="FormLabel">Question</Form.Label> 
        <Form.Message className="FormMessage" match="valueMissing"> 
          Please enter a question 
        </Form.Message> 
      </div> 
      <Form.Control asChild> 
        <textarea className="Textarea" required /> 
      </Form.Control> 
    </Form.Field> 
    <Form.Submit asChild> 
      <button className="Button" style={{ marginTop: 10 }}> 
        Post question 
      </button> 
    </Form.Submit> 
  </Form.Root> 
); 

export default FormDemo; 

โœ… Popover

Popover

import React from 'react'; 
import * as Popover from '@radix-ui/react-popover'; 
import { MixerHorizontalIcon, Cross2Icon } from '@radix-ui/react-icons'; 
import './styles.css'; 

const PopoverDemo = () => ( 
  <Popover.Root> 
    <Popover.Trigger asChild> 
      <button className="IconButton" aria-label="Update dimensions"> 
        <MixerHorizontalIcon /> 
      </button> 
    </Popover.Trigger> 
    <Popover.Portal> 
      <Popover.Content className="PopoverContent" sideOffset={5}> 
        <div style={{ display: 'flex', flexDirection: 'column', gap: 10 }}> 
          <p className="Text" style={{ marginBottom: 10 }}> 
            Dimensions 
          </p> 
          <fieldset className="Fieldset"> 
            <label className="Label" htmlFor="width"> 
              Width 
            </label> 
            <input className="Input" id="width" defaultValue="100%" /> 
          </fieldset> 
          <fieldset className="Fieldset"> 
            <label className="Label" htmlFor="maxWidth"> 
              Max. width 
            </label> 
            <input className="Input" id="maxWidth" defaultValue="300px" /> 
          </fieldset> 
          <fieldset className="Fieldset"> 
            <label className="Label" htmlFor="height"> 
              Height 
            </label> 
            <input className="Input" id="height" defaultValue="25px" /> 
          </fieldset> 
          <fieldset className="Fieldset"> 
            <label className="Label" htmlFor="maxHeight"> 
              Max. height 
            </label> 
            <input className="Input" id="maxHeight" defaultValue="none" /> 
          </fieldset> 
        </div> 
        <Popover.Close className="PopoverClose" aria-label="Close"> 
          <Cross2Icon /> 
        </Popover.Close> 
        <Popover.Arrow className="PopoverArrow" /> 
      </Popover.Content> 
    </Popover.Portal> 
  </Popover.Root> 
); 

export default PopoverDemo; 

โœ… RadioGroup

RadioGroup

import React from 'react'; 
import * as RadioGroup from '@radix-ui/react-radio-group'; 
import './styles.css'; 

const RadioGroupDemo = () => ( 
  <form> 
    <RadioGroup.Root className="RadioGroupRoot" defaultValue="default" aria-label="View density"> 
      <div style={{ display: 'flex', alignItems: 'center' }}> 
        <RadioGroup.Item className="RadioGroupItem" value="default" id="r1"> 
          <RadioGroup.Indicator className="RadioGroupIndicator" /> 
        </RadioGroup.Item> 
        <label className="Label" htmlFor="r1"> 
          Default 
        </label> 
      </div> 
      <div style={{ display: 'flex', alignItems: 'center' }}> 
        <RadioGroup.Item className="RadioGroupItem" value="comfortable" id="r2"> 
          <RadioGroup.Indicator className="RadioGroupIndicator" /> 
        </RadioGroup.Item> 
        <label className="Label" htmlFor="r2"> 
          Comfortable 
        </label> 
      </div> 
      <div style={{ display: 'flex', alignItems: 'center' }}> 
        <RadioGroup.Item className="RadioGroupItem" value="compact" id="r3"> 
          <RadioGroup.Indicator className="RadioGroupIndicator" /> 
        </RadioGroup.Item> 
        <label className="Label" htmlFor="r3"> 
          Compact 
        </label> 
      </div> 
    </RadioGroup.Root> 
  </form> 
); 
  
export default RadioGroupDemo; 

โœ… Tabs

Tabs

import React from 'react'; 
import * as Tabs from '@radix-ui/react-tabs'; 
import './styles.css'; 

const TabsDemo = () => ( 
  <Tabs.Root className="TabsRoot" defaultValue="tab1"> 
    <Tabs.List className="TabsList" aria-label="Manage your account"> 
      <Tabs.Trigger className="TabsTrigger" value="tab1"> 
        Account 
      </Tabs.Trigger> 
      <Tabs.Trigger className="TabsTrigger" value="tab2"> 
        Password 
      </Tabs.Trigger> 
    </Tabs.List> 
    <Tabs.Content className="TabsContent" value="tab1"> 
      <p className="Text">Make changes to your account here. Click save when you're done.</p> 
      <fieldset className="Fieldset"> 
        <label className="Label" htmlFor="name"> 
          Name 
        </label> 
        <input className="Input" id="name" defaultValue="Pedro Duarte" /> 
      </fieldset> 
      <fieldset className="Fieldset"> 
        <label className="Label" htmlFor="username"> 
          Username 
        </label> 
        <input className="Input" id="username" defaultValue="@peduarte" /> 
      </fieldset> 
      <div style={{ display: 'flex', marginTop: 20, justifyContent: 'flex-end' }}> 
        <button className="Button green">Save changes</button> 
      </div> 
    </Tabs.Content> 
    <Tabs.Content className="TabsContent" value="tab2"> 
      <p className="Text">Change your password here. After saving, you'll be logged out.</p> 
      <fieldset className="Fieldset"> 
        <label className="Label" htmlFor="currentPassword"> 
          Current password 
        </label> 
        <input className="Input" id="currentPassword" type="password" /> 
      </fieldset> 
      <fieldset className="Fieldset"> 
        <label className="Label" htmlFor="newPassword"> 
          New password 
        </label> 
        <input className="Input" id="newPassword" type="password" /> 
      </fieldset> 
      <fieldset className="Fieldset"> 
        <label className="Label" htmlFor="confirmPassword"> 
          Confirm password 
        </label> 
        <input className="Input" id="confirmPassword" type="password" /> 
      </fieldset> 
      <div style={{ display: 'flex', marginTop: 20, justifyContent: 'flex-end' }}> 
        <button className="Button green">Change password</button> 
      </div> 
    </Tabs.Content> 
  </Tabs.Root> 
); 

export default TabsDemo; 

๐Ÿ“Œ ์ข…ํ•ฉ

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

profile
์‰ฝ๊ฒŒ๋งŒ์‚ด์•„๊ฐ€๋ฉด์žฌ๋ฏธ์—†์–ด๋น™๊ณ 

7๊ฐœ์˜ ๋Œ“๊ธ€

comment-user-thumbnail
2025๋…„ 1์›” 20์ผ

l

1๊ฐœ์˜ ๋‹ต๊ธ€
comment-user-thumbnail
2025๋…„ 2์›” 6์ผ

๋งˆ์นจ radix ๊ด€๋ จ ์ข‹์€ ๊ธ€์„ ์ฐพ์€ ๊ฒƒ ๊ฐ™์•„์„œ ์ •๋…ํ–ˆ๋Š”๋ฐ ํ˜œ์›๋‹˜ ๊ธ€์ด์—ˆ๋„ค์š” ๐Ÿ‘๐Ÿป๐Ÿ‘๐Ÿป ๊ฐ์‚ฌํ•ฉ๋‹ˆ๋‹ค!

1๊ฐœ์˜ ๋‹ต๊ธ€