๋ณธ๋ฌธ ๋ฐ”๋กœ๊ฐ€๊ธฐ
๐Ÿ‘จ‍๐Ÿ’ป web.dev/fe

TRIPLE ์›น ์ผ์ •ํŒ ํŠธ๋Ÿฌ๋ธ” ์ŠˆํŒ… ํšŒ๊ณ ๋ก

by HandHand 2024. 3. 11.

 

 

 

์ž‘๋…„์— ์›น ์ผ์ •ํŒ ์„œ๋น„์Šค๋ฅผ ๊ตฌํ˜„ํ•˜๋ฉด์„œ ๋ฐ˜์‘ํ˜• 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๋Š” ์–ด๋–ป๊ฒŒ ์ฒ˜๋ฆฌํ•˜๊ณ  ์žˆ์„๊นŒ ๊ถ๊ธˆํ•ด์„œ ๊ฐœ๋ฐœ์ž ๋„๊ตฌ๋กœ ๋œฏ์–ด๋ณด๋‹ˆ ์ด๋ฏธ ์ด ๋ฐฉ๋ฒ•์„ ์‚ฌ์šฉํ•˜๊ณ  ์žˆ๋”๋ผ๊ณ ์š”.

 

airbnb ๋„ custom vh unit ์„ ์‚ฌ์šฉ์ค‘์ด๋‹ค.

 

 

๐Ÿ’ก ์ฐธ๊ณ  - ์‹ ๊ทœ 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

 

๋ฐ˜์‘ํ˜•

๐Ÿ’ฌ ๋Œ“๊ธ€