๊ฐ๋ฐ
[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
์ ํ์ฉํด ํ์ค๋ฒ๋ฅผ ์ธ๋ถ์ ๋ ๋๋ง ํ๊ธฐ ๋๋ฌธ์ ๋ถ๋ชจ์์์ ์คํ์ผ์๋ ์์ ๋ก์ธ ์ ์๋ ์ฅ์