๐กํดํ ์ปดํฌ๋ํธ๋?
ํดํ(Tooltip) ์ปดํฌ๋ํธ๋ ์ฌ์ฉ์๊ฐ ์ธํฐํ์ด์ค ์์์ ๋ง์ฐ์ค๋ฅผ ์ฌ๋ฆฌ๊ฑฐ๋ ์ด์ ์ ๋ง์ถ ๋, ์ถ๊ฐ์ ์ธ ์ ๋ณด๋ฅผ ์ ๊ณตํ๋ ์์ ํ์ ์ฐฝ์ ๋๋ค. ์ผ๋ฐ์ ์ผ๋ก ์งง๊ณ ์ ์ฉํ ์ค๋ช ์ด๋ ๊ฐ์ด๋๋ฅผ ๋ณด์ฌ์ฃผ๋ ๋ฐ ์ฌ์ฉ๋ฉ๋๋ค. ํดํ์ ์ฌ์ฉ์ฑ ํฅ์๊ณผ ์ฌ์ฉ์ ๊ฒฝํ ๊ฐ์ ์ ์ค์ํ ์ญํ ์ ํฉ๋๋ค.
์ด ์ปดํฌ๋ํธ๋ ์ฃผ๋ก ๋ค์๊ณผ ๊ฐ์ ์ํฉ์์ ์ฌ์ฉ๋ฉ๋๋ค.
- ์์ด์ฝ ์ค๋ช
- ์ถ์ฝ๋ ์ ๋ณด์ ํ์ฅ
- ํผ ํ๋์ ๋์๋ง
- ๋ฒํผ ๋๋ ๋งํฌ์ ์ถ๊ฐ ์ ๋ณด
ํดํ ์ปดํฌ๋ํธ๋ฅผ ์ํด ๊ณ ๋ฏผํ ์
ํดํ์ ์์น
ํดํ ์ปดํฌ๋ํธ๋ฅผ ๊ฐ๋ฐํ ๋, ๊ธฐ๋ณธ์ ์ผ๋ก๋ ํดํ์ ํธ์ถํ๋ ๋ถ๋ชจ์ปดํฌ๋ํธ์์ ์ํ๋ ์์น๋ฅผ ์ง์ ํ ์ ์๋๋ก ํฉ๋๋ค. ํ์ง๋ง, ์ด๋ ๊ฒ ํธ์ถ๋ ํดํ์ด ์คํฌ๋กค์ ๊ฐ๋ฆฌ๊ฑฐ๋ ์ ์ ์ ๋ทฐํฌํธ์ ๋ณด์ด์ง ์๋ ๊ฒฝ์ฐ๋ ๊ณ ๋ คํ ํ์๊ฐ ์๋ค ์๊ฐํฉ๋๋ค. ํดํ์ด ๊ธธ์ด์ง๋ ๊ฒฝ์ฐ๋ ํ๋ฉด์ ๊ตฌ์์ ๋ ๋ ๋ ๊ฐ๋ฅ์ฑ์ด ์๋ค๋ฉด ์๋์ผ๋ก ์์น์กฐ์ ์ ํ์ฌ ํญ์ ํ๋ฉด์ ๋ ธ์ถํ๊ฒ ํ๋ ๊ฒ๋ ์ข์ ๋ฐฉ๋ฒ์ ๋๋ค.
ํธ๋ฆฌ๊ฑฐ์ด๋ฒคํธ
ํดํ์ ํธ๋ฆฌ๊ฑฐ ์ด๋ฒคํธ๋ฅผ ์ด๋ป๊ฒ ๋ฐ์๊ฑด์ง๋ ์ค์ํ ์์์ ๋๋ค. ๋ํ์ ์ผ๋ก๋ ๋ง์ฐ์ค ์ค๋ฒ, ํค๋ณด๋ํฌ์ปค์ค, ํด๋ฆญ ๋ฑ์ ๋ฐฉ๋ฒ์ด ์์ผ๋ฉฐ ๊ธฐํ์ ์ธ ๋ถ๋ถ๊ณผ ์ ๊ณ ๋ฏผํ์ฌ ํธ๋ฆฌ๊ฑฐ๋ฅผ ์ ํ๋ ๊ฒ์ด ์ค์ํฉ๋๋ค.
์ ๋ ๊ฐ๋ฐ ๋น์ ํดํ์ปดํฌ๋ํธ๋ฅผ ๊ธฐ์กด์ ๊ฐ๋ฐํ๋ ์ฝํ ์คํธ๋ฉ๋ด ์ปดํฌ๋ํธ์ ๋์ผํ ํด๋ฆญ ํธ๋ฆฌ๊ฑฐ๋ก ๊ฐ๋ฐํ ์ ์ด ์์๋๋ฐ, ๊ธฐํ์์ ์๊ฐ์ด ๋ฌ๋ผ ์๋ก ํดํ์ปดํฌ๋ํธ๋ฅผ ๊ฐ๋ฐํ๋ ๊ฒฝํ์ด ์์ต๋๋ค. ๋จ์ํด ๋ณด์ด๋ ์์๋ผ๋ ์ฝ๊ฐ์ ์ํต์ ํตํด์ ๋ ๋ฒ ์์ ํ๋ ๊ฒ์ ๋ง์ ์ ์์ผ๋, ์ด ๋ถ๋ถ๋ ํจ๊ป ๊ณ ๋ฏผํ๊ณ ๋์ด๊ฐ๋ ๊ฒ์ด ์ค์ํฉ๋๋ค.
ํ ๋ฒ์ ์ด๋ฆฌ๋ ํดํ์ ๊ฐ์
๋ง์ฝ ํดํ์ด ํด๋ฆญ ์ด๋ฒคํธ์ ๊ฐ์ ํธ๋ฆฌ๊ฑฐ๋ก ์ด๋ฆฌ๊ฒ ๋๋ค๋ฉด ํ ๋ฒ์ ์ฌ๋ฌ๊ฐ์ ํดํ์ด ์ด๋ ค์๋ ์ํ๋ ์กด์ฌํ ์ ์์ต๋๋ค. ์ด ๋ถ๋ถ๋ ๊ธฐํ์์ ๊ณ ๋ฏผํ์ฌ ์ ์ด์ ์ฌ๋ฌ๊ฐ๊ฐ ํ๋ฒ์ ์ด๋ฆด์ง, ํ๋์ ํดํ๋ง ์ด๋ฆด์ง์ ๋ํ ๋ก์ง๋ ๊ณ ๋ฏผํ๋ ๊ฒ ์ข์ต๋๋ค.
๐๊ตฌํํด ๋ณด๊ธฐ
useExternalSync
useSyncExternalStroe๋ ๋ฆฌ์กํธ ์ธ๋ถ store๋ฅผ ๊ตฌ๋ ํ๋ React Hook์ ๋๋ค. ์ ๋ ์ฒ์์ ๊ตฌ๋ ์ด๋ ๋จ์ด๊ฐ ์ด์ํ๊ฒ ๋๊ปด์ก๋๋ฐ, ๊ตฌ๋ ์ ๊ฐ์ ๋ณํ์ ๋ฐ๋ผ ๋ฆฌ๋ ๋๋ฅผ ์ผ์ผํค๋ ๊ฒ์ด๋ผ๊ณ ์ดํดํ๋ ์ด ํ ์ด ๊ฝค ์ง๊ด์ ์ด๊ฒ ๋ค๊ฐ์์ต๋๋ค.
์ธ๋ถ store๋ผ๊ณ ํ๋ฉด, redux, zustand ๋ฑ์ด ์๋๋ฐ ์ฌ์ค ์ด๋ฌํ ๊ฐ๋ค์ ๋ฆฌ์กํธ ์ธ๋ถ์ ์ ์ฅ์์ด๊ณ , ๋์ด์ผ ์๊ฐํด ๋ณด๋ฉด ์ธ๋ถ์ ๊ฐ๋ค์ด ๋ฆฌ์กํธ์ ๋ฆฌ๋ ๋๋ง์ ์ผ์ผํค๋ ๊ฒ ๋ฏ์ค๊ฒ ๋๊ปด์ง๋๋ค. ์ฌ์ค ์ด๋ฅผ ๊ฐ๋ฅํ๊ฒ ํ๋ ๊ฒ์ด useSyncExternal๊ณผ ๊ฐ์ ํ ์ ๋๋ค. zustand๊ตฌํ์ฒด ๋ด๋ถ์๋ useSyncExternalStoreWithSelector ์ด๋ฌํ ํจ์๋ค์ด ์กด์ฌํ๋๋ฐ, ์ธ๋ถ์ ์ ์ฅ๋ ๊ฐ์ด ๋ณ๊ฒฝ๋์ ๋ ๋ฆฌ๋ ๋๋ฅผ ์ผ์ผํค๋ ์ฝ๋์์ ์ง์ํ ์ ์์ต๋๋ค.
useSyncExternalStore๋ ๋ธ๋ผ์ฐ์ API๋ฅผ ๊ตฌ๋ ํ๋๋ฐ๋ ์ ์ฉํ๊ฒ ์ฌ์ฉํ ์ ์์ต๋๋ค. ๋ธ๋ผ์ฐ์ ์ ์คํฌ๋กค์ ๋ณด๋ react์์ ๊พธ์คํ ๊ด๋ฆฌํด์ผ ํ๋ ๊ฐ์ด์ง๋ง, ์คํฌ๋กค์ด ๋ณํ๋ค๊ณ ๋ฆฌ๋ ๋๋ฅผ ์ผ์ผํค์ง๋ ์์ต๋๋ค. ์ด๋, useSyncExternalStore๋ฅผ ์ฌ์ฉํ๋ค๋ฉด ์คํฌ๋กค์ ๊ตฌ๋ ํ๋ฉด์, ๊ฐ์ด ๋ณํ ๋๋ง๋ค ๋ฆฌ๋ ๋๋ฅผ ์ผ์ผํฌ ์ ์๊ฒ ๋ฉ๋๋ค.
// getSnapShot ํจ์
/**
* getSnapShot ํ์
* store์ ๋ฐ์ดํฐ๋ฅผ ๋ฐํํ๋ ํจ์์
๋๋ค. store๊ฐ ๋ณ๊ฒฝ๋ ๋๋ง๋ค ํด๋น ํจ์๊ฐ ์คํ๋๋ฉฐ ๋ฆฌ์กํธ์์๋
* ์ดํจ์์ ๋ณํ๊ฐ์ Object.is๋ฅด ๋น๊ตํ์ฌ ๋ค๋ฅผ๊ฒฝ์ฐ ๋ฆฌ๋ ๋๋ฅผ ๋ฐ์์ํต๋๋ค.
* ์ด ์์ ์์๋ ๊ฐ์ฒด๋ฅผ ๋ฐํํด๋ ๋ฆฌ๋ ๋๋ฅผ ๋ฐฉ์งํ๊ธฐ์ํด ํด๋ก์ ๋ฅผ ํ์ฉํ์ต๋๋ค.
*/
const getViewportRect = () => {
let stored: Rect = DefaultRect
return () => {
const elem = typeof document !== 'undefined' && document.scrollingElement
if (!elem) return stored
const { left, top, width, height } = elem.getBoundingClientRect()
const newRect = { left, top, width, height, scrollHeight: elem.scrollHeight }
if (newRect && !isSameRect(stored, newRect)) stored = newRect
return stored
}
}
/**
* store๋ฅผ ๊ตฌ๋
ํ๋ ํจ์์
๋๋ค. subscribe๋ ์คํ ์ด๋ฅผ ๊ตฌ๋
ํ๋ ํจ์์ ๊ตฌ๋
์ ์ทจ์ํ๋ ํจ์๋ฅผ ๋ฐํํด์ผํฉ๋๋ค.
* ๋ํ ์ธ์๋ก ์ฝ๋ฐฑํจ์๋ฅผ ์ ๊ณตํ๋๋ฐ, ์ฝ๋ฐฑํจ์๋ฅผ ์คํ ์ด์ ์ ๋ฌํ์ฌ ์คํ ์ด์ ๋ณ๊ฒฝ์ฌํญ์ ์ ์ ์๋๋ก ํฉ๋๋ค.
*/
const subscribe = (callback: () => void) => {
const resizeObserver = new ResizeObserver(callback)
window.addEventListener('scroll', callback)
resizeObserver.observe(document.body)
return () => {
window.removeEventListener('scroll', callback)
resizeObserver.disconnect()
}
}
const viewportRect = useSyncExternalStore(subscribe, getViewportRect())
ํด๋น ์ฝ๋๋ ํ์ฌ ์ฐฝ์ ๋ณํ๋ฅผ ๊ฐ์งํ์ฌ ์ด๊ฒ์ด ๋ณํ์ ๋, ๋ฆฌ๋ ๋๋ฅผ ์ผ์ผํค๋ ์ฝ๋์ ๋๋ค. useExternalSync๋ ์ฃผ์ํด์ผ ํ ์ ์ด Primitive ํ ๊ฐ์ ์ด์ฉํด์ผ ํฉ๋๋ค. ํจ์์ return ๊ฐ์Object.is๋ก ๋น๊ตํ๊ณ ๋ฌ๋ผ์ง๋ฉด, ๋ฆฌ๋ ๋๋ฅผ ์ผ์ผํค๊ธฐ ๋๋ฌธ์ ๋๋ค. ํ์ง๋ง, ์ด ์ฝ๋๋ top, left, width, height ์ ๋ณด๋ฅผ ๊ฐ์ฒด๋ก ๋ฆฌํดํ๋๋ฐ ์ด๋ฅผ ์ํด ํด๋ก์ ๋ฅผ ์ด์ฉํ๋ ์ ์ด ์ธ์์ ์ด์์ต๋๋ค.
ํดํ์ ์์น ๊ณ์ฐ ๋ก์ง
const tooltipPosition = {
top: '100%',
bottom: 20,
left: 0,
right: 0,
}
const useStyleInView = (
wrapperRef: RefObject<HTMLElement>,
targetRef: RefObject<HTMLElement>,
position: Position,
) => {
const viewportRect = useViewportRect()
const [style, setStyle] = useState<Style>({})
useLayoutEffect(() => {
if (!wrapperRef.current || !targetRef.current) return
const wrapperRect = wrapperRef.current.getBoundingClientRect()
const targetRect = targetRef.current.getBoundingClientRect()
// "๊ธฐ์ค๊ฐ". top์ top์ ๊ธฐ์ค์ผ๋ก ์๋๋ก ๋ณด์ฌ์ฃผ๊ธฐ. bottom์ ์๋ก ๋ณด์ฌ์ฃผ๊ธฐ.
const verticalKey =
wrapperRect.bottom + targetRect.height < viewportRect.height ? 'top' : 'bottom'
const horizontalKey =
wrapperRect.right + targetRect.width < viewportRect.width ? 'left' : 'right'
setStyle({
[verticalKey]: position[verticalKey] || 0,
[verticalKey === 'top' ? 'bottom' : 'top']: 'auto',
[horizontalKey]: position[horizontalKey] || 0,
[horizontalKey === 'left' ? 'right' : 'left']: 'auto',
})
}, [viewportRect, wrapperRef, targetRef, position])
return style
}
ํดํ์ ์์น๋ฅผ ๊ณ์ฐํด ์ฃผ๊ธฐ ์ํด์๋ ํ๋ฉด ๊ธฐ์ค์ผ๋ก ์, ์๋, ์ผ์ชฝ, ์ค๋ฅธ์ชฝ์ ๊ตฌํด์ผ ํฉ๋๋ค.
์ / ์๋๋ ํ์ฌ ํดํ์ ์์น์ ๋ ๋๋ง ๋ ํดํ์ ๋์ด๊ฐ ์ดํ๋ฉด์ ๋์ด๋ณด๋ค ์๋ค๋ฉด, ํ๊น์์ ์๋์(๊ธฐ๋ณธ๊ฐ) ํฌ๋ค๋ฉด ํ๊ฒ์์ ์์ ๋ ๋ ํ๋๋ก ํฉ๋๋ค.
์ข / ์ฐ๋ ๋ง์ฐฌ๊ฐ์ง๋ก, ํ๊น์์์ ์ฐ์ธก๊ณผ ๋ ๋๋ง ๋ ํดํ์ ๊ธธ์ด๊ฐ ํ๋ฉด์ ๋์ด๋ณด๋ค ๋๋ค๋ฉด ์ผ์ชฝ, ์๋๋ผ๋ฉด ์ค๋ฅธ์ชฝ์ ๋๋ค.
์ด๋ ๊ฒ ์์ฑํ๋ฉด ๊ฐ๋จํ ๋ด์ฉ์ด์ง๋ง ์ฝ๋๋ก ์์ฑํ๋ ค๋ฉด ๊ฝค๋ ์ ๊ฒฝ ์ธ ์ ์ด ๋ง์ ์ฝ๋๊ฐ ๋ณต์กํด์ง ์ ์์ต๋๋ค. ํ์ง๋ง, ๊ฐ์๋ฅผ ๋ณด๋ฉด์ computed property๋ฅผ ์ฌ์ฉํด์ ๊น๋ํ๊ฒ ์ฝ๋ ์์ฑ์ ํ ๊ฒ ์ธ์์ ์ด์์ต๋ค.
๐ ๊ฒฐ๊ณผ๋ฌผ
'๊ฐ๋ฐ' ์นดํ ๊ณ ๋ฆฌ์ ๋ค๋ฅธ ๊ธ
[UI] ์์ฝ๋์ธ ์ปดํฌ๋ํธ (feat: UI ์์ ์ง์ ๋ง๋ค๊ธฐ) (0) | 2025.01.06 |
---|---|
[๊ฐ์์คํฌ๋กค] ๊ฐ์์คํฌ๋กค ์ง์ ๊ฐ๋ฐํ๊ธฐ (2) | 2024.10.19 |
[SARAMARA] ๋น์ฆ๋์ค๋ก์ง ๋ถ๋ฆฌํ๊ธฐ (0) | 2023.12.11 |
์ฝ๋ ๋ฆฌํฉํ ๋ง (์ก์ , ๋ฐ์ดํฐ, ๊ณ์ฐ ๋๋๊ธฐ) (0) | 2023.12.08 |
CORS (0) | 2023.12.06 |