๊ฐœ๋ฐœ

[UI] Popover[UI์š”์†Œ ๋งŒ๋“ค๊ธฐ]

๊ฑฐ๋… 2025. 4. 20. 22:26

๐Ÿ“Œ ํŒ์˜ค๋ฒ„(Popover) ์ปดํฌ๋„ŒํŠธ๋ž€?

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


๐ŸŽฏ ์ฃผ์š” ๊ธฐ๋Šฅ

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

โ—๏ธ๊ตฌํ˜„ ๋‚ด์šฉ

์ผ๋ฐ˜์ ์ธ ๊ตฌํ˜„

const MenuPopover = ({
  id,
  close,
  wrapperRef,
}: {
  id: string
  close: () => void
  wrapperRef: RefObject<HTMLElement>
}) => {
  const targetRef = useRef<HTMLUListElement>(null)
  const style = useStyleInView(wrapperRef, targetRef, menuPosition)
  return (
    <div className={cx('MenuPopover')} onClick={close}>
      <ul className={cx('menus')} onClick={e => e.stopPropagation()} ref={targetRef} style={style}>
        <li>#{id}</li>
        <li>์Šค๋ ˆ๋“œ์˜ ๋Œ“๊ธ€</li>
        <li>๋ฉ”์‹œ์ง€ ์ „๋‹ฌ</li>
        <li>๋‚˜์ค‘์„ ์œ„ํ•ด ์ €์žฅ</li>
        <li>์ฝ์ง€ ์•Š์Œ์œผ๋กœ ํ‘œ์‹œ</li>
        <li>์‚ญ์ œ</li>
      </ul>
    </div>
  )
}

const ListItem = ({ id, title, index }: { id: string; title: string; index: number }) => {
  const buttonRef = useRef<HTMLButtonElement>(null)
  const [menuOpened, toggleMenu] = useState(false)
  const handleClickButton = () => toggleMenu(true)

  return (
    <li id={id} className={cx('list-item')}>
      #{index + 1}. {title}
      <button className={cx('popover-button')} onClick={handleClickButton} ref={buttonRef} />
      {menuOpened && (
        <MenuPopover id={index + 1 + ''} close={() => toggleMenu(false)} wrapperRef={buttonRef} />
      )}
    </li>
  )
}
  • ๋‚ด๋ถ€์— popover ์ปดํฌ๋„ŒํŠธ๋ฅผ ๋„ฃ๊ณ , ํด๋ฆญ ์ด๋ฒคํŠธ๋ฅผ ๊ด€๋ฆฌํ•˜๋Š” ๋ฐฉ๋ฒ•
  • ์‰ฝ๊ฒŒ ๊ตฌํ˜„ํ•  ์ˆ˜ ์žˆ๋‹ค๋Š” ๊ฒƒ์ด ์žฅ์ ์ด์ง€๋งŒ, ๋งŒ์•ฝ ListItem์— overflow: hidden ์Šคํƒ€์ผ์ด ๊ฑธ๋ฆฌ๊ฒŒ ๋œ๋‹ค๋ฉด ํŒ์˜ค๋ฒ„์š”์†Œ๊ฐ€ ๊ฐ€๋ ค์งˆ ์ˆ˜ ์žˆ์Œ

createPortal์„ ํ™œ์šฉํ•œ ๊ตฌํ˜„

const MenuPopover = ({
  id,
  close,
  wrapperRef,
}: {
  id: string
  close: () => void
  wrapperRef: RefObject<HTMLElement>
}) => {
  const targetRef = useRef<HTMLUListElement>(null)
  const style = useStyleInView(wrapperRef, targetRef, menuPosition, 'absolute')

  return createPortal(
    <div className={cx('MenuPopover')} onClick={close}>
      <ul className={cx('menus')} onClick={e => e.stopPropagation()} ref={targetRef} style={style}>
        <li>#{id}</li>
        <li>์Šค๋ ˆ๋“œ์˜ ๋Œ“๊ธ€</li>
        <li>๋ฉ”์‹œ์ง€ ์ „๋‹ฌ</li>
        <li>๋‚˜์ค‘์„ ์œ„ํ•ด ์ €์žฅ</li>
        <li>์ฝ์ง€ ์•Š์Œ์œผ๋กœ ํ‘œ์‹œ</li>
        <li>์‚ญ์ œ</li>
      </ul>
    </div>,
    document.querySelector('#popoverRoot')!,
  )
}
  • useStyleInView ํ›…์„ ์ˆ˜์ •ํ•˜์—ฌ, absolute ์ธ ๊ฒฝ์šฐ์—๋„ ํ™”๋ฉด์ƒ์— ๋ณด์ผ ์ˆ˜ ์žˆ๋„๋ก ์ˆ˜์ •
{
...
      const absoluteTop = -viewportRect.top + wrapperRect.top

      setStyle({
        [verticalKey]:
          verticalKey === 'top'
            ? absoluteTop + wrapperRect.height + +(position.top || 0)
            : viewportRect.height - absoluteTop + +(position.bottom || 0),
        [verticalKey === 'top' ? 'bottom' : 'top']: 'auto',
        [horizontalKey]:
          horizontalKey === 'left'
            ? wrapperRect.left - +(position.left || 0)
            : viewportRect.width - wrapperRect.right + +(position.right || 0),
        [horizontalKey === 'left' ? 'right' : 'left']: 'auto',
      })

    }
}
  • absoluteTop : wrapperRect์˜ ์œ„์น˜
    • verticalKey === top ์ธ ๊ฒฝ์šฐ absoluteTop ์—์„œ wrapperHeight ๋งŒํผ ์•„๋ž˜์—์„œ ์•„๋ž˜๋กœ ๋ณด์—ฌ์คŒ
  • createPortal์„ ํ™œ์šฉํ•ด ํŒ์˜ค๋ฒ„๋ฅผ ์™ธ๋ถ€์— ๋ Œ๋”๋ง ํ•˜๊ธฐ ๋•Œ๋ฌธ์— ๋ถ€๋ชจ์š”์†Œ์˜ ์Šคํƒ€์ผ์—๋„ ์ž์œ ๋กœ์šธ ์ˆ˜ ์žˆ๋Š” ์žฅ์ 