
์๋
์ ์น ์ผ์ ํ ์๋น์ค๋ฅผ ๊ตฌํํ๋ฉด์ ๋ฐ์ํ UI์ ์คํฌ๋กค UX์ ๊ด๋ จ๋ ๋ช ๊ฐ์ง ์ด์๋ค์ด ์์๋๋ฐ,
๊ทธ๋ ๋น์๋ฅผ ํ๊ณ ํด๋ณด๋ฉด์ ๊ฐ๊ฐ์ ๋ฌธ์ ์ํฉ๊ณผ ์ด๋ฅผ ํด๊ฒฐํ ๋ฐฉ๋ฒ๋ค์ ์ ๋ฆฌํด๋ณด๋ ค๊ณ ํฉ๋๋ค.
๐ ์น ์ผ์ ํ ๊ธฐ๋ฅ๊ณผ ์ง์ ๋ฒ์
ํธ๋ฆฌํ ์น ์ผ์ ํ
์ PC์ ๋ชจ๋ฐ์ผ(ํ๋ธ๋ฆฟ ํฌํจ) ํ๊ฒฝ์ ํ๊น์ผ๋ก ํ ์น ์๋น์ค์
๋๋ค.
ํธ๋ฆฌํ ์ฑ์์ ์์ฑํ ์ผ์ ์ ํธ๋ฆฌํ ์ ์ ๊ฐ ์๋ ์ฌ๋๋ค๋ ์ ๊ทผํ ์ ์๋ ๋งํฌ๋ฅผ ์์ฑํด ์ค๋๋ค.


ํธ๋ฆฌํ ์ฑ์์ ์ผ์ ํ ๋งํฌ๋ ์ด 3๊ฐ์ง ๋ฐฉ๋ฒ์ผ๋ก ๊ณต์ ๊ฐ ๊ฐ๋ฅํฉ๋๋ค.
- ์ผ์ ๋งํฌ ๋ณต์ฌํ๊ธฐ
- ์นด์นด์คํก์ผ๋ก ๊ณต์ ํ๊ธฐ
- ๊ทธ ์ธ ๋ฉ์ ์ ๋ฑ์ ๋ฐฉ๋ฒ์ผ๋ก ๊ณต์ ํ๊ธฐ
๋งํฌ๋ฅผ ๋ณดํต ์นด์นด์คํก์ผ๋ก ๊ณต์ ํ๊ธฐ ๋๋ฌธ์ ์นด์นด์คํก ์ธ์ฑ ๋ธ๋ผ์ฐ์ ๋ฅผ ๋น๋กฏํ์ฌ ์ ๋ฌ๋ฐ์ ๋งํฌ๋ฅผ ์คํํ ์ ์๋ ์ฃผ์ ๋ธ๋ผ์ฐ์ (safari, chrome, whale, firefox)๊น์ง ํธํ์ด ๋์ด์ผ ํฉ๋๋ค.
๋ํ ๋ชจ๋ฐ์ผ ๋ฐ ํ๋ธ๋ฆฟ์ ๊ฒฝ์ฐ ๋ฐ์คํฌํฑ๊ณผ๋ ๋ค๋ฅด๊ฒ ๊ฐ๋ก, ์ธ๋ก๋ชจ๋์ ํ์ด ๊ฐ๋ฅํ๋ค๋ ๊ฒ์ ๊ณ ๋ คํด์ผ ํฉ๋๋ค.
๐ viewport height ์ฌ๋ฐ๋ฅด๊ฒ ๊ณ์ฐํ๊ธฐ
๐ฟ ๋ฌธ์ ์ํฉ 1 - webkit ๋ธ๋ผ์ฐ์
๋ฉ์ธ ํ์ด์ง์ ๋ ์ด์์์ ๋ค์๊ณผ ๊ฐ์ต๋๋ค.
const PageLayout = styled.div`
height: 100vh;
overflow: hidden;
`

๋๋ฐ์ด์ค ๋์ด๊ฐ์ ๋ฐ๋ผ์ ์๋ก ๋ค๋ฅธ height
๊ฐ์ ๊ฐ์ง๊ธฐ ์ํด vh
๋ฅผ ์ฌ์ฉํ๋๋ฐ, webkit๊ธฐ๋ฐ ๋ชจ๋ฐ์ผ ๋ธ๋ผ์ฐ์ (iOS chrome & safari)์์ ํ๋จ ์ฃผ์ํ์์ค ๋์ด๋งํผ ์คํฌ๋กค์ด ๋ฐ์ํ๋ ์ด์๊ฐ ๋ฐ์ํ์ต๋๋ค.
์ฃผ์ํ์์ค์ ๋์ด๊น์ง vh์ ํฌํจ๋์ด์ ๊ณ์ฐ๋๋ ํ์์ ๋๋ค.
๐ฟ ๋ฌธ์ ์ํฉ 2 - ์นด์นด์คํก ์ธ์ฑ ๋ธ๋ผ์ฐ์
์นด์นด์คํก์ ์ฑํ ๋ฐฉ์ผ๋ก ์ธ๋ถ URL ๊ณต์ ์ ์ธ๋ถ ๋ธ๋ผ์ฐ์ ๊ฐ ์๋๋ผ ์์ฌ ์ธ์ฑ ๋ธ๋ผ์ฐ์ ๋ก ํ์ด์ง๋ฅผ ๋ก๋ฉํฉ๋๋ค.
์นด์นด์คํก ์ธ์ฑ ๋ธ๋ผ์ฐ์ ์์ ๊ฐ์ฅ ํฐ ๋ฌธ์ ๋ ๋ด์ฅ ๋ธ๋ผ์ฐ์ ์ ํ๋จ ์ํ๋ฐ๊ฐ ์คํฌ๋กค ๋ฐฉํฅ์ ๋ฐ๋ผ ๋ ธ์ถ ์ฌ๋ถ๊ฐ ๊ฒฐ์ ๋๋ฉฐ ์ด ๊ณผ์ ์์ viewport๋ฅผ resize ํ๋ค๋ ๊ฒ์ ๋๋ค. ์ด๋ก ์ธํด viewport unit ๊ณ์ฐ์ด ๊น๋ค๋ก์์ง๋๋ค.
์ ์๋ ค์ง ์ด์๋ผ์ ๊ทธ๋ฐ์ง ์นด์นด์คํก ๊ฐ๋ฐ์ ์ผํฐ์ ์ด๋ฏธ ๋น์ทํ ์ด์๊ฐ ์ฌ๋ฟ ์กด์ฌํ๋๋ฐ,
์นด์นด์ค ๋ด๋ถ ์๋น์ค์์๋ resize
์ด๋ฒคํธ๋ฅผ ๊ฐ์งํ๋ ๋ฐฉ์์ผ๋ก ๋์ํ๋ค๊ณ ํฉ๋๋ค.
devtalk.kakao - ์ธ์ฑ๋ธ๋ผ์ฐ์ ํ๋จ๋ฐ ์ฌ๋ผ์ง
์ธ์ฑ๋ธ๋ผ์ฐ์ ํ๋จ๋ฐ ์ฌ๋ผ์ง
์๋ ํ์ธ์ ์นด์นด์คํก์ผ๋ก url์ ์ด๋ฉด ์นด์นด์คํก ์์ฒด ๋ด ์ธ์ฑ๋ธ๋ผ์ฐ์ ๊ฐ ์ด๋ฆฌ๋๋ฐ ์ด๋ฆฌ๊ณ ๋์ ์คํฌ๋กคํ๋ฉด ํ๋จ๋ฐ๊ฐ ์์ด์ก๋ค๊ฐ ๋ค์ ๋ํ๋ฉ๋๋ค. ์ ํฌ ์ฌ์ดํธ์ ํ๋จ fixed ๋ฒํผ์ ์ถ๊ฐํด ๋์๋
devtalk.kakao.com
๐ก ๋ฌธ์ ํด๊ฒฐ - vh unit ์ง์ ๊ณ์ฐํ๊ธฐ
์ ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๋ ํ ๊ฐ์ง ๋ฐฉ๋ฒ์ vh
์ ๋์ ์ง์ ๊ณ์ฐํ๋ ๊ฒ์
๋๋ค.
์๋ resize
์ด๋ฒคํธ๊ฐ ๋ฐ์ํ ๋๋ง๋ค ๋ธ๋ผ์ฐ์ ๋ด๋ถ ๋์ด๊ฐ์ ๊ณ์ฐํ๊ณ ์ด๋ฅผ ์ด์ฉํด์ vh
๋ฅผ ๊ณ์ฐํฉ๋๋ค.
๋ชจ๋ resize
์ด๋ฒคํธ๋ง๋ค ์ฝ๋ฐฑ์ด ์คํ๋๋ ๊ฒ์ด ์ฑ๋ฅ ์ด์๊ฐ ์ฐ๋ ค๋์ด ๋๋ฐ์ด์ค๋ฅผ ํจ๊ป ์ ์ฉํ์ต๋๋ค.
import { useEffect, useLayoutEffect } from 'react'
import { debounce } from './utils'
export const VH = (unit: number) => `calc(var(--vh, 1vh) * ${unit})`
const useIsomorphicLayoutEffect =
typeof window !== 'undefined' ? useLayoutEffect : useEffect
export function useAdjustVhUnit() {
useIsomorphicLayoutEffect(() => {
const setVhUnit = debounce(() => {
const vh = window.innerHeight * 0.01
document.documentElement.style.setProperty('--vh', `${vh}px`)
}, 100)
setVhUnit()
window.addEventListener('resize', setVhUnit)
return () => window.removeEventListener('resize', setVhUnit)
}, [])
}
์ด์ ์ฌ์ฉํ๋ ์ชฝ์์ vh
๋จ์๋ฅผ ๊ณ์ฐ๋ ๊ฒฐ๊ณผ๋ฅผ ์ฌ์ฉํ ์ ์๋๋ก ๋ณ๊ฒฝํฉ๋๋ค.
const PageLayout = styled.div`
height: ${VH(100)};
overflow: hidden;
`

์ด์ vh
๊ฐ ์ฌ๋ฐ๋ฅด๊ฒ ๊ณ์ฐ๋๋ ๊ฒ์ ํ์ธํ ์ ์์ต๋๋ค.
์คํฌ๋กค์ ํ์ด์ง ์ต์๋จ๊น์ง ์ฌ๋ ธ์ ๋ ํ์ด์ง ์๋ก๊ณ ์นจ์ด ๊ฐ๋ฅํด์ก์ต๋๋ค.
๋ฐ์ํ UI ๊ฐ ์ ์ฉ๋ airbnb๋ ์ด๋ป๊ฒ ์ฒ๋ฆฌํ๊ณ ์์๊น ๊ถ๊ธํด์ ๊ฐ๋ฐ์ ๋๊ตฌ๋ก ๋ฏ์ด๋ณด๋ ์ด๋ฏธ ์ด ๋ฐฉ๋ฒ์ ์ฌ์ฉํ๊ณ ์๋๋ผ๊ณ ์.

๐ก ์ฐธ๊ณ - ์ ๊ท css ์คํ ์ฌ์ฉํ๊ธฐ
webkit ๋ธ๋ผ์ฐ์ ๋ด์์ vh ์ด์๋ ์ด๋ฏธ ๊ฝค๋ ๋๋ฆฌ ์๋ ค์ง ์ด์์ธ๋ฐ,
์ต๊ทผ์ ํฌ๋ก์ค ๋ธ๋ผ์ฐ์ง์ ์ง์ํ๊ฒ ๋ dynamic viewport
์ต์
์ ์ฌ์ฉํ๋ฉด ์ฝ๊ฒ ํด๊ฒฐํ ์ ์์ ๊ฒ ๊ฐ์ต๋๋ค.
๊ตฌ๋ฒ์ safari ๊ฐ ์ผ๋ฅธ fade-out ๋๊ธธ ๋น์ด๋ด ๋๋ค. ๐
The large, small, and dynamic viewport units
ํฌ๊ณ ์์ ๋์ ํ์ ์์ญ ๋จ์ | Blog | web.dev
๋์ ํด๋ฐ์ ํจ๊ป ๋ชจ๋ฐ์ผ ํ์ ์์ญ์ ๊ณ ๋ คํ๋ ์๋ก์ด CSS ๋จ์
web.dev
๐ intersection observer ์ฌ์ฉ ์ sticky ์์ญ ํจ๊ป ๊ณ ๋ คํ๊ธฐ
๐ฅ ๊ธฐ๋ ๋์
์น ์ผ์ ํ์ ๋ชจ๋ฐ์ผ ๋๋ฐ์ด์ค (ํน์ ๋ชจ๋ฐ์ผ ์ฌ์ด์ฆ ๋ธ๋ผ์ฐ์ )์์๋ ์ง๋๋ทฐ๋ฅผ sticky ์์ญ์ผ๋ก ํจ๊ป ๋ ธ์ถํฉ๋๋ค.
๋ํ ์ฌ์ฉ์๊ฐ ํ์ฌ ๋ณด๊ณ ์๋ ์ผ์ ์ ์๊ฐ์ ์ผ๋ก ํ๋จํ ์ ์๋๋ก ํ๊ธฐ ์ํด์ ๋ฆฌ์คํธ ์๋จ ์์ญ๊ณผ ๊ต์ฐจ๋๋ ๋ฆฌ์คํธ ์์ดํ ์๋ ๋ฐฐ๊ฒฝ์ ๊ฐ์กฐ ์ฒ๋ฆฌ๋ฅผ ํ๊ณ ์์ต๋๋ค.

๐ฟ ๋ฌธ์ ์ํฉ
๊ธฐ๊ธฐ์ ๋๋น ๋ฐ ๋์ด๊ฐ ์ค์ด๋๋ ์ํฉ (landscape & portrait ๋ชจ๋ ๋ณ๊ฒฝ ํฌํจ) ํน์ ๋ชจ๋ฐ์ผ ํ๊ฒฝ์์ ์ง๋๋ฅผ ์ด๊ณ ๋ซ๋ ์ฌ๋ถ์ ๋ฐ๋ผ์ intersection observer์ root margin ๋ณ๊ฒฝ์ด ํ์ํ์ต๋๋ค.
๐ก ๋ฌธ์ ํด๊ฒฐ - scroll observer & detector ๊ตฌํํ๊ธฐ
root
์์๋ฅผ ์ง์ ํ๊ณ sticky
์์ญ์ ๊ณ ๋ คํ root margin ์ค์ ์ ๋์์ฃผ๋ ์ปดํฌ๋ํธ๋ฅผ ๊ตฌํํ๊ฒ ์ต๋๋ค.
์คํฌ๋กค ๋ฃจํธ๋ฅผ ์ง์ ํ๋ observer์ ์คํฌ๋กค๋ง์ ๊ฐ์งํ detector ์ปดํฌ๋ํธ๋ก ๊ตฌ๋ถํฉ๋๋ค.
์ดํ๋ถํฐ๋ ๊ฐ๋ ์ฑ์ ์ํด ์์ฌ์ฝ๋ ํ์์ผ๋ก ์ ๋ฆฌํ๊ฒ ์ต๋๋ค.
๋จผ์ observer
๋ scroll spy ์๋ฆฌ๋จผํธ๋ฅผ ๊ฐ์ง๊ณ ์๋ ์ปดํฌ๋ํธ์
๋๋ค.
export function ScrollObserver() {
return (
<ScrollObserverProvider>
<ScrollDetectorProvider>
<SpyContainer>{children}</SpyContainer>
</ScrollDetectorProvider>
</ScrollObserverProvider>
)
}
function SpyContainer({ children }) {
const { observer, setObserver } = useScrollObserverContext()
return (
<Container
id={SCROLL_ROOT_ID}
ref={($el) => {
if (!observer && $el) {
setObserver($el)
}
}}
css={{ overflow: 'scroll', height: '100%' }}
>
{children}
</Container>
)
}
scroll root๋ก ์ง์ ํ ์๋ฆฌ๋จผํธ์ SCROLL_ROOT_ID
์ ํด๋นํ๋ ID ๊ฐ์ ํ ๋นํด ์ฃผ๋ ๊ฒ์
๋๋ค.
์ด ID ๊ฐ์ ์ด์ฉํด ๋ค์ ํํธ์์ ์งํ๋ useScrollIntoView
ํ
์ ๊ตฌํํฉ๋๋ค.
์ฐ๋ฆฌ๋ ์ผ์ ๋ฆฌ์คํธ ์์ญ์ ์คํฌ๋กค ๋์์ผ๋ก ๊ฐ์งํ ๊ฒ์ด๊ธฐ ๋๋ฌธ์ ํด๋น ์ปดํฌ๋ํธ๋ฅผ ScrollObserver
๋ก ๊ฐ์ธ๊ฒ ๋ฉ๋๋ค.
function View() {
return (
<ListLayout>
<ScrollObserver>
<PlanListView />
</ScrollObserver>
</ListLayout>
)
}
์ด์ detector
๋ฅผ ๊ตฌํํด ๋ณด๊ฒ ์ต๋๋ค.
detector๋ ์ง๋์ ๋ ธ์ถ์ฌ๋ถ์ scroll root์ ๋์ด ๋ณ๊ฒฝ์ ๊ฐ์งํด์ root margin ์ฌ์ฐ์ ์ด ๊ฐ๋ฅํด์ผ ํฉ๋๋ค.
scroll root๋ ๋ธ๋ผ์ฐ์ ํฌ๊ธฐ ๋ณ๊ฒฝ์ ๋ฐ๋ผ ๋์ด๊ฐ ๋ณ๊ฒฝ๋ ์ ์์ต๋๋ค.
์ด๋ ๊ณง ์ฌ์ฉ์์ ์ํด ์์๋ก ๋ธ๋ผ์ฐ์ ํฌ๊ธฐ๊ฐ ์กฐ์ ๋๊ฑฐ๋ landscape, portrait ๋ชจ๋ ๋ณ๊ฒฝ์ ์ํด ๋ฐ์ํฉ๋๋ค.
์ด๋ฅผ ์ํด ResizeObserver
ํน์ resize event handler
์ค ํ๋๋ฅผ ์ ํํด์ผ ํ๊ณ , ์ข ๋ ์ ์ฉ์ด ํธ๋ฆฌํ๋ resize ์ด๋ฒคํธ ํธ๋ค๋ฌ๋ฅผ ๋๋ฐ์ด์ค๋ฅผ ํจ๊ป ์ฌ์ฉํ์ต๋๋ค.
function ObserveDetector({
children,
threshold = 0.4,
observer,
onIntersect,
}: PropsWithChildren<ObserveDetectorProps>) {
const { showMiniMap, disabled } = useScrollDetectorContext()
const [rootMargin, setRootMargin] = useState<string>()
useEffect(() => {
const setObserverRootMargin = debounce(() => {
if (observer) {
const OBSERVER_ROOT_HEIGHT = observer.getBoundingClientRect().height
const STICKY_HEIGHT = getStickyElementHeight()
const topMargin = STICKY_HEIGHT
const bottomMargin =
OBSERVER_ROOT_HEIGHT - STICKY_HEIGHT - OBSERVER_HEIGHT
setRootMargin(`${-topMargin}px 0px ${-bottomMargin}px`)
}
}, 300)
setObserverRootMargin()
window.addEventListener('resize', setObserverRootMargin)
return () => window.removeEventListener('resize', setObserverRootMargin)
}, [observer, showMiniMap])
const handleChange: ChangeHandler = useCallback(
({ isIntersecting }) => {
if (isIntersecting) {
onIntersect?.()
}
},
[onIntersect],
)
return (
<IntersectionObserver
root={observer}
rootMargin={rootMargin}
threshold={threshold}
disabled={disabled}
onChange={handleChange}
>
<div>{children}</div>
</IntersectionObserver>
)
}
detector
์ ์ฌ์ฉ ์์์
๋๋ค.
์ผ์ ์์ดํ
์ค ํญ๊ณต๊ถ
์ detector
๋ฅผ ์ ์ฉํ ์์์
๋๋ค.
ObservedFlightItem
์ ์์ ์ปดํฌ๋ํธ์์๋ ์ผ์ ๋ฐ์ดํฐ๋ฅผ ์ํํ๋ฉฐ ์์ดํ
์ ๋ฆฌ์คํธ UI๋ก ๊ตฌ์ฑํฉ๋๋ค.
function ObservedFlightItem({
onIntersect,
...rest
}) {
const { observer } = useScrollObserverContext()
return (
<ObserveDetector observer={observer} onIntersect={onIntersect}>
<FlightItem {...rest} />
</ObserveDetector>
)
}
๐ ํน์ ์๋ฆฌ๋จผํธ ์์น๋ก ์คํฌ๋กค๋งํ๊ธฐ
๐ฅ ๊ธฐ๋ ๋์
๋ ์ง๊ฐ ๋ณ๊ฒฝ๋๊ฑฐ๋ ์ง๋๋ทฐ์์ ํน์ POI๋ฅผ ์ ํํ ๊ฒฝ์ฐ ํด๋น POI๋ก ๋ฆฌ์คํธ ๋ทฐ์ ์คํฌ๋กค ์์น๋ฅผ ๋ณ๊ฒฝํด์ผ ํฉ๋๋ค.

๐ฟ ๋ฌธ์ ์ํฉ
ํ์ค ์น ์คํ์๋ scrollIntoView
๋ผ๋ ์ธํฐํ์ด์ค๊ฐ ์์ต๋๋ค.
๋ถ๋ชจ ์ปจํ
์ด๋ ๋ด์์ ํน์ ์์์์๊ฐ ์ฌ์ฉ์์๊ฒ ๋
ธ์ถ๋ ์ ์๋ ์์น๊น์ง ์คํฌ๋กคํด์ฃผ๋๋ฐ,
๋ฌธ์ ๋ sticky
์์ญ์ ์คํฌ๋กค์ ์ํ offset ๊ณ์ฐ์์ ์ ์ธ๋๋ค๋ ๊ฒ์
๋๋ค.
์ด ๋๋ฌธ์ ์คํฌ๋กค์ด ๋ ๋ ์์น๋ก ์ด๋ํ๊ฒ ๋๋ ์ํฉ์ด์์ต๋๋ค.
๐ก ๋ฌธ์ ํด๊ฒฐ - use-scroll-into-view hook ๊ตฌํํ๊ธฐ
ํน์ ํ ๋ถ๋ชจ๋ฅผ ๊ธฐ์ค์ผ๋ก ์์ ์์๊ฐ ํ๋ฉด์ ๋ ธ์ถ๋๋๋ก ์คํฌ๋กคํด์ฃผ๋ ํจ์๋ฅผ ๊ตฌํํด ๋ด ์๋ค.
์ด๋ฅผ ์ํด ์คํฌ๋กค ๋ถ๋ชจ ์์๋ฅผ scroll root
, ์์ ์์๋ฅผ child
๋ก ๋ถ๋ฅด๊ฒ ์ต๋๋ค.
import { SCROLL_ROOT_ID, STICKY_ID } from './constants'
export function scrollIntoView({ childId, offset = 0 }) {
const $scrollRoot = document.getElementById(SCROLL_ROOT_ID)
const $targetChild = document.getElementById(childId)
if (!$scrollRoot || !$targetChild) {
return
}
const currentScrollOffset = $scrollRoot.scrollTop
const targetScrollOffset = $targetChild.getBoundingClientRect().top
const stickyHeight = getStickyElementHeight()
const scrollOffset =
currentScrollOffset + targetScrollOffset + offset - stickyHeight
$scrollRoot.scrollTo({
top: scrollOffset,
left: 0,
})
}
export function getStickyElementHeight() {
return document.getElementById(STICKY_ID)?.getBoundingClientRect().height || 0
}
import { useCallback } from 'react'
import { LAYOUT } from '@/plan/constants'
import { useScrollIntoView, useIsMobileMedia } from '.'
export function useScrollIntoPlan() {
const scrollIntoView = useScrollIntoView()
const { isMobileMedia } = useIsMobileMedia()
const scrollIntoPlan = useCallback(
(planId: string) => {
const offset = isMobileMedia
? LAYOUT.CONTROL_HEADER.HEIGHT + LAYOUT.MOBILE_MAP_EXPANDER
: LAYOUT.CONTROL_HEADER.HEIGHT
scrollIntoView({
childId: planId,
offset: -offset,
})
},
[scrollIntoView, isMobileMedia],
)
return scrollIntoPlan
}
์ด์ ํน์ ์์น ์ ํ ์ ํธ๋ค๋ฌ ๋ด๋ถ์์ scrollIntoPlan
์ ํธ์ถํด ์ฃผ๋ฉด ๋ฉ๋๋ค.
์ด์ธ์๋ ๋ ์ง ํญ ์ ํ ์ ํน์ ๋ ์ง๋ก ๋ฆฌ์คํธ๋ฅผ ์ด๋ํ๋ use-scoll-into-day
ํ
๋ ๊ตฌํํด์ ์ฌ์ฉํ์ต๋๋ค.
์น ์ขํ ์์คํ ์ ์ดํดํ๋ฉฐ ํน์ Element๋ก ์คํฌ๋กคํ๋ ๋ฐฉ๋ฒ
์น ์ขํ ์์คํ ์ ์ดํดํ๋ฉฐ ํน์ Element๋ก ์คํฌ๋กค ํ๋ ๋ฐฉ๋ฒ
Javascript๋ฅผ ํ์ฉํ์ฌ ํน์ Element๋ก ์คํฌ๋กค ํ๋ ๋ฐฉ๋ฒ๊ณผ ์น ์ขํ ์์คํ ์ ๋ํด ์์๋ด ์๋ค
blog.eunsukim.me
'๐จโ๐ป web.dev > fe' ์นดํ ๊ณ ๋ฆฌ์ ๋ค๋ฅธ ๊ธ
Server Sent Events ์ ์ฉํด๋ณด๊ธฐ (0) | 2024.03.10 |
---|---|
์ ๊ทํํ์๊ณผ ํจ๊ป URL ๋ฏ์ด๋ณด๊ธฐ (0) | 2024.03.01 |
Chrome ๊ฐ๋ฐ์๋๊ตฌ๋ฅผ ์ด์ฉํด์ ์์น์ ๋ณด ์ค์ ํ๊ธฐ (0) | 2024.03.01 |
storybook middleware proxy ๋ก CORS ์ฐํํ๊ธฐ (0) | 2024.03.01 |
์ ๊ท์์ผ๋ก camel-case ํ์ฑํ๊ธฐ (1) | 2024.02.24 |
๐ฌ ๋๊ธ