Skip to content

Latest commit

ย 

History

History
132 lines (97 loc) ยท 8.46 KB

infinite-scroll-with-intersection-observer.md

File metadata and controls

132 lines (97 loc) ยท 8.46 KB

๋ฌดํ•œ ์Šคํฌ๋กค

๋ฌดํ•œ ์Šคํฌ๋กค ์€ ๋‹ค์–‘ํ•œ ์›น์‚ฌ์ดํŠธ, ์–ดํ”Œ๋ฆฌ์ผ€์ด์…˜์—์„œ ๋ณผ ์ˆ˜ ์žˆ๋‹ค. ์ผ๋ฐ˜์ ์œผ๋กœ ๋ณด์—ฌ์ฃผ๊ณ  ์‹ถ์€ ๋ฐ์ดํ„ฐ๊ฐ€ ๋งŽ์€ ๊ฒฝ์šฐ ์ด๋ฅผ ํ•œ๋ฒˆ์— ๋ชจ๋‘ ๋ Œ๋”๋ง ํ•˜๋Š” ๊ฒƒ์€ ๋น„ํšจ์œจ์ ์ด๋‹ค. ์™œ๋ƒํ•˜๋ฉด ์‚ฌ์šฉ์ž๊ฐ€ ๊ทธ ๋งŽ์€ ๋ฐ์ดํ„ฐ๋ฅผ ๋ชจ๋‘ ๋ณผ ํ™•๋ฅ ๋„ ๋‚ฎ๊ณ  ์‚ฌ์šฉ์ž๋Š” ๊ทธ ๋งŽ์€ ๋ฐ์ดํ„ฐ๋ฅผ ๋‹ค์šด๋ฐ›๋Š” ๋„คํŠธ์›Œํฌ ๋ฆฌ์†Œ์Šค ๋‚ญ๋น„๊ฐ€ ๋ฐœ์ƒํ•˜๋ฉด์„œ ๋™๊ธฐ์— ์ดˆ๊ธฐ ๋กœ๋”ฉ ์†๋„๊ฐ€ ๋Š๋ ค์ง€๊ธฐ ๋•Œ๋ฌธ์ด๋‹ค. ๋งŒ์•ฝ ์šฐ๋ฆฌ ์‚ฌ์ดํŠธ๋ฅผ ๋ฐฉ๋ฌธํ•˜๋Š” ์‚ฌ์šฉ์ž๋“ค์ด ๋ฐฐํ„ฐ๋ฆฌ๋„ ๋นจ๋ฆฌ ๋‹ณ๊ณ  ๋ฐ์ดํ„ฐ ์†Œ๋ชจ๋Ÿ‰๋„ ๋Š˜์–ด๋‚˜๋Š”๊ฑธ ๋ฐœ๊ฒฌํ•œ๋‹ค๋ฉด ๋‹ค์‹œ ๋ฐฉ๋ฌธํ•˜์ง€ ์•Š์„ ๊ฐ€๋Šฅ์„ฑ์ด ๋†’์•„ ์งˆ ์ˆ˜๋„ ์žˆ๊ฒ ๋‹ค.

๋ฐ์ดํ„ฐ๊ฐ€ ๋งŽ๋‹ค -> ๋‚˜๋ˆ ์„œ ๋ณด์—ฌ์ฃผ์ž -> ํŽ˜์ด์ง€๋„ค์ด์…˜ or ๋ฌดํ•œ ์Šคํฌ๋กค ๋“ฑ์œผ๋กœ ์ƒ๊ฐ์„ ์˜ฎ๊ฒจ ๋ณผ ์ˆ˜ ์žˆ๋Š”๋ฐ ํŽ˜์ด์ง€๋„ค์ด์…˜๊ณผ ๋ฌดํ•œ์Šคํฌ๋กค์€ ๋ฐ์ดํ„ฐ๋ฅผ ์ œ๊ณตํ•ด์ฃผ๋Š” ์„ฑ๊ฒฉ์ด ์กฐ๊ธˆ ๋‹ค๋ฅธ ๊ฒƒ ๊ฐ™๋‹ค. ํŽ˜์ด์ง€๋„ค์ด์…˜ ๊ฐ™์€ ๊ฒฝ์šฐ ์—ฌ๋Ÿฌ๋ฐ์ดํ„ฐ๋ฅผ ํŽ˜์ด์ง€๋กœ ์ชผ๊ฐœ์„œ ๋‚˜๋ˆ„๋Š”๋ฐ ์ฒซ๋ฒˆ์งธ ์˜๋ฏธ๊ฐ€ ์žˆ๊ณ  ๋‘๋ฒˆ์งธ ์˜๋ฏธ๋Š” ์‚ฌ์šฉ์ž์—๊ฒŒ ๋ฐ์ดํ„ฐ์˜ ์œ„์น˜๋ฅผ ํŠน์ •ํ•  ์ˆ˜ ์žˆ๋„๋ก ๋„์™€์ฃผ๋Š” ๊ฒƒ์ด๋‹ค. ๊ทธ๋ž˜์„œ 1ํŽ˜์ด์ง€์— 3๋ฒˆ์งธ๊ธ€, 3ํŽ˜์ด์ง€์— 9๋ฒˆ์งธ ๊ธ€ ์ฒ˜๋Ÿผ ์œ„์น˜๋ฅผ ๊ธฐ์–ตํ•ด์„œ ๋‹ค์‹œ ํ•ด๋‹น ๊ฒŒ์‹œ๋ฌผ์— ์ ‘๊ทผ ํ•  ์ˆ˜ ์žˆ๋‹ค.

๋ฐ˜๋ฉด์— ๋ฌดํ•œ ์Šคํฌ๋กค์€ ์œ„์น˜๋ฅผ ํŠน์ •ํ•  ์ˆ˜ ์—†์ง€๋งŒ ํ•œ ํŽ˜์ด์ง€ ๋‚ด์—์„œ ๋งŽ์€ ๋ฐ์ดํ„ฐ๋ฅผ ๋ Œ๋”๋ง ํ•  ์ˆ˜ ์žˆ๋‹ค. ๊ทธ๋ž˜์„œ ์ข€ ๋” ๋‚˜์€ UX ๋ฅผ ๋ณด์—ฌ์ค„ ์ˆ˜ ์žˆ๋‹ค.

์ธ์Šคํƒ€๊ทธ๋žจ์ด๋‚˜ ํŽ˜์ด์Šค๋ถ ๊ฐ™์€ ๊ฒฝ์šฐ๊ฐ€ ๊ทธ๋Ÿฐํ•œ๋ฐ ๋ฐฉ๊ธˆ ์ „์— ๋ดค์—ˆ๋˜ ๋ฐ์ดํ„ฐ๋ฅผ ๋‹ค์‹œ ๋ณด๊ณ  ์‹ถ์„ ๋•Œ ํŽ˜์ด์ง€๋ฅผ ์•„๋ฌด๋ฆฌ ๋‚ด๋ ค๋„ ์ฐพ์„ ์ˆ˜ ์—†๋Š” ๊ฒฝ์šฐ๊ฐ€ ๋ฐœ์ƒํ•  ์ˆ˜๋„ ์žˆ๋‹ค. ๊ธฐ์—…๋“ค์€ ์—ฌ๊ธฐ์— ๋ฌด์Šจ ์ธ๊ณต์ง€๋Šฅ์„ ์ด์šฉํ•ด์„œ ๊ฐœ์ธํ™” ๋œ ์ถ”์ฒœ ์‹œ์Šคํ…œ์„ ๋„์ž…ํ•˜๊ณ  ์‚ฌ์šฉ์ž๊ฐ€ ์ข‹์•„ํ•  ๋งŒํ•œ ๋ฐ์ดํ„ฐ๋ฅผ ๋งˆ๊ตฌ์žก์ด๋กœ ๋ผ์›Œ ๋„ฃ์–ด์ฃผ๋Š” ๊ฒƒ ๊ฐ™๋‹ค.

์ผ๋‹จ ์นดํ‚ค์˜คํ†ก์—์„œ๋Š” ๊ทธ๋ƒฅ ์ฑ„ํŒ…๋ฐ์ดํ„ฐ๊ฐ€ ๋„ˆ๋ฌด ๋งŽ์œผ๋ฉด ํ•œ๋ฒˆ์— ๋‹ค ๋ Œ๋”๋ง ํ•  ์ˆ˜ ์—†๊ธฐ ๋•Œ๋ฌธ์— ์ด๋ฅผ ๋‚˜๋ˆ ์„œ ๋ณด์—ฌ์ฃผ๊ธฐ ์œ„ํ•ด์„œ ๋ฌดํ•œ ์Šคํฌ๋กค์„ ์‚ฌ์šฉํ•˜๊ธฐ๋กœ ํ–ˆ๋‹ค. ๋Œ€๋ถ€๋ถ„์˜ ์ฑ„ํŒ… ์–ดํ”Œ๋ฆฌ์ผ€์ด์…˜์„ ๋‹ค ์ด๋Ÿฐ๋ฐฉ์‹์ผ ๊ฑฐ๋‹ค.

์•Œ์•„๋ณธ ๊ฒฐ๊ณผ ๋ฌดํ•œ ์Šคํฌ๋กค์„ scroll ์ด๋ฒคํŠธ, intersection observer API ๋‘๊ฐ€์ง€ ๋ฐฉ์‹์œผ๋กœ ๊ตฌํ˜„ ๊ฐ€๋Šฅํ•˜๋‹ค.

scroll ์ด๋ฒคํŠธ

window.addEventListener('scroll', () => {
  const y = window.scrollY
  const clientHeight = document.element.clientHeight
  const scrollHeight = document.element.scrollHeight

  if(y + clientHeight > scrollHeight) {
    // ๋ฐ์ดํ„ฐ ๋” ๋ถˆ๋Ÿฌ์˜ค๊ธฐ
  }
})

์œ„์— ์ฝ”๋“œ์ฒ˜๋Ÿผ scroll ์ด๋ฒคํŠธ์˜ ๋ฆฌ์Šค๋„ˆ์—์„œ ํ˜„์žฌ ์Šคํฌ๋กค์œ„์น˜๋ฅผ ํŒŒ์•…ํ•ด์„œ ๋งŒ์•ฝ ์ „์ฒด ์Šคํฌ๋กค ๊ธธ์ด๋ณด๋‹ค ๊ธธ๊ฒŒ ๋‚ด๋ ค ๊ฐ”๋‹ค๋ฉด ๋ฐ์ดํ„ฐ๋ฅผ ์š”์ฒญํ•˜๋ฉด ๋œ๋‹ค.

scroll properties ์Šคํฌ๋กค ํฌ์ง€์…˜์€ ์—ฌ๋Ÿฌ๊ฐ€์ง€ ์š”์†Œ๊ฐ€ ์žˆ๋Š”๋ฐ ์œ„์— ๊ทธ๋ฆผ์„ ๋ณด๋ฉด ๋Œ€์ถฉ ์•Œ ์ˆ˜ ์žˆ๋‹ค.

๊ตฌํ˜„ํ•˜๊ธฐ๋Š” ๋งค์šฐ ๊ฐ„๋‹จํ•˜์ง€๋งŒ ์กฐ๊ธˆ๋” ๋งŒ์ ธ์ค˜์•ผํ•  ๋ถ€๋ถ„์ด ์žˆ๋‹ค. scroll ์ด๋ฒคํŠธ๋Š” ๋งค์šฐ ๋นˆ๋ฒˆํ•˜๊ฒŒ ์ผ์–ด๋‚˜๊ธฐ ๋•Œ๋ฌธ์— ๊ทธ๋•Œ๋งˆ๋‹ค ์ € ์ฝœ๋ฐฑํ•จ์ˆ˜๊ฐ€ ๋™์ž‘ํ•œ๋‹ค๋ฉด ์›น ์–ดํ”Œ๋ฆฌ์ผ€์ด์…˜ ์„ฑ๋Šฅ์— ์•ˆ์ข‹์„ ์˜ํ–ฅ์„ ๋ผ์นœ๋‹ค. ์™œ๋ƒํ•˜๋ฉด scroll ์„ ํ•˜๋ฉด์„œ ํ˜„์žฌ ์œ„์น˜๋ฅผ ์•Œ์•„๋‚ด๊ธฐ ์œ„ํ•ด clientHeight ๊ฐ™์€ ๊ณ„์‚ฐ๋œ ํ”„๋กœํผํ‹ฐ ๊ฐ’์„ ์š”๊ตฌํ•˜๋Š”๋ฐ ์ด๋Š” ๋ธŒ๋ผ์šฐ์ €์˜ reflow ๋ฅผ ์œ ๋ฐœํ•˜๊ธฐ ๋•Œ๋ฌธ์ด๋‹ค. - reflow ์œ ๋ฐœํ•˜๋Š” ๊ฒƒ๋“ค

  • Throttle : ๋งˆ์ง€๋ง‰ ํ•จ์ˆ˜๊ฐ€ ํ˜ธ์ถœ๋œ ํ›„ ์ผ์ • ์‹œ๊ฐ„์ด ์ง€๋‚˜๊ธฐ ์ „์— ๋‹ค์‹œ ํ˜ธ์ถœ๋˜์ง€ ์•Š๋„๋ก ํ•˜๋Š” ๊ฒƒ
  • Debounce : ์—ฐ์ด์–ด ํ˜ธ์ถœ๋˜๋Š” ํ•จ์ˆ˜๋“ค ์ค‘ ๋งˆ์ง€๋ง‰ ํ•จ์ˆ˜(๋˜๋Š” ์ œ์ผ ์ฒ˜์Œ)๋งŒ ํ˜ธ์ถœํ•˜๋„๋ก ํ•˜๋Š” ๊ฒƒ

๊ทธ๋ž˜์„œ throttle ์ด๋‚˜ debounce ๋ฅผ ๊ฑธ์–ด์„œ ๋ชจ๋“  ์š”์ฒญ์„ ๊ฑธ์ง€ ์•Š๊ณ  ์„ฑ๋Šฅ์„ ๋†’์—ฌ์ฃผ๋Š” ๋“ฑ์˜ ์ถ”๊ฐ€์ž‘์—…์„ ํ•ด์ค˜์•ผ ํ•œ๋‹ค.

Intersection Observer

Intersection Observer API๋Š” ํƒ€๊ฒŸ ์š”์†Œ์™€ ์ƒ์œ„ ์š”์†Œ ๋˜๋Š” ์ตœ์ƒ์œ„ document ์˜ viewport ์‚ฌ์ด์˜ intersection ๋‚ด์˜ ๋ณ€ํ™”๋ฅผ ๋น„๋™๊ธฐ์ ์œผ๋กœ ๊ด€์ฐฐํ•˜๋Š” ๋ฐฉ๋ฒ•์ž…๋‹ˆ๋‹ค. - MDN

์‚ฌ์‹ค ์ด ๋ง์„ ์•„์ง๋„ ์ดํ•ดํ•˜์ง€ ๋ชปํ•˜๊ฒ ๋‹ค. ์–ด์จ‹๋“  element ๊ฐ€ ํ™”๋ฉด์— ๋ณด์ด๋ฉด ๊ทธ๊ฑธ ์•Œ์•„์ฑŒ ์ˆ˜ ์žˆ๋„๋ก ํ•ด์ค€๋‹ค๋Š” ๊ฑฐ๊ณ  ์—ฌ๋Ÿฌ๊ฐ€์ง€ ์šฉ๋„๋กœ ์‚ฌ์šฉ๋  ์ˆ˜ ์žˆ๋‹ค. ์นดํ‚ค์˜คํ†ก์—์„œ๋Š” 2๋ฒˆ์˜ ์ด์œ ๋กœ ์‚ฌ์šฉํ•œ๋‹ค.

  1. ํŽ˜์ด์ง€๊ฐ€ ์Šคํฌ๋กค ๋˜๋Š” ๋„์ค‘์— ๋ฐœ์ƒํ•˜๋Š” ์ด๋ฏธ์ง€๋‚˜ ๋‹ค๋ฅธ ์ปจํ…์ธ ์˜ ์ง€์—ฐ ๋กœ๋”ฉ.
  2. ์Šคํฌ๋กค ์‹œ์—, ๋” ๋งŽ์€ ์ปจํ…์ธ ๊ฐ€ ๋กœ๋“œ ๋ฐ ๋ Œ๋”๋ง๋˜์–ด ์‚ฌ์šฉ์ž๊ฐ€ ํŽ˜์ด์ง€๋ฅผ ์ด๋™ํ•˜์ง€ ์•Š์•„๋„ ๋˜๊ฒŒ ํ•˜๋Š” infinite-scroll ์„ ๊ตฌํ˜„.
  3. ๊ด‘๊ณ  ์ˆ˜์ต์„ ๊ณ„์‚ฐํ•˜๊ธฐ ์œ„ํ•œ ์šฉ๋„๋กœ ๊ด‘๊ณ ์˜ ๊ฐ€์‹œ์„ฑ ๋ณด๊ณ .
  4. ์‚ฌ์šฉ์ž์—๊ฒŒ ๊ฒฐ๊ณผ๊ฐ€ ํ‘œ์‹œ๋˜๋Š” ์—ฌ๋ถ€์— ๋”ฐ๋ผ ์ž‘์—…์ด๋‚˜ ์• ๋‹ˆ๋ฉ”์ด์…˜์„ ์ˆ˜ํ–‰ํ•  ์ง€ ์—ฌ๋ถ€๋ฅผ ๊ฒฐ์ •.

Intersection Observer ๊ฐ™์€ ๊ฒฝ์šฐ scroll ์ด๋ฒคํŠธ๋ณด๋‹ค ์•ฝ๊ฐ„ ์ œ์•ฝ์‚ฌํ•ญ์ด ์žˆ๋Š”๊ฒŒ ๋ชจ๋“  ๋ธŒ๋ผ์šฐ์ €์—์„œ ์ง€์›ํ•˜์ง€ ์•Š๋Š”๋‹ค. ์™ ๋งŒํ•œ ๋ธŒ๋ผ์šฐ์ €๋Š” ๋‹ค ์ง€์›๋˜์ง€๋งŒ IE ๋ฅผ ๊ผญ ์ง€์›ํ•ด์•ผํ•˜๋Š” ์ƒํ™ฉ์ด๋ผ๋ฉด ๊ทธ๊ฒƒ๋„ ๊ทธ๊ฒƒ๋Œ€๋กœ ๊ท€์ฐฎ๊ฒ ๋‹ค. can-i-use-intersection-observer

์ผ๋‹จ ๋น„๋™๊ธฐ์ ์œผ๋กœ ๋™์ž‘ํ•˜๊ธฐ ๋•Œ๋ฌธ์— ๋ฉ”์ธ์Šค๋ ˆ๋“œ๋ฅผ ์‚ฌ์šฉํ•˜์ง€ ์•Š๊ณ  ๊ทธ๋งŒํผ ์„ฑ๋Šฅ์ƒ์˜ ์ด์ ์„ ์–ป์„ ์ˆ˜ ์žˆ๋‹ค. scroll ์ด๋ฒคํŠธ๋กœ ๊ตฌํ˜„ํ•  ๋•Œ ์ฒ˜๋Ÿผ throttle ์ด๋‚˜ debounce ์ฒ˜๋ฆฌ๋ฅผ ํ•ด์ค„ ํ•„์š”๊ฐ€ ์—†๋‹ค.

๊ตฌํ˜„

/* eslint-disable @typescript-eslint/no-explicit-any */
import {
  DependencyList, useCallback, useEffect, useRef,
} from 'react'

interface UseIntersectionObserver {
  (cb: (entry: IntersectionObserverEntry) => void,
  options: IntersectionObserverInit,
  deps: DependencyList): (node: any) => void
}

const useIntersectionObserver: UseIntersectionObserver = (cb, options, deps) => {
  const intersectionObserver = useRef<IntersectionObserver | null>(null)

  return useCallback((node) => {
    if (intersectionObserver.current) {
      intersectionObserver.current.disconnect()
    }

    intersectionObserver.current = new IntersectionObserver(([entry]) => {
      cb(entry)
    }, options)

    if (node) intersectionObserver.current.observe(node)
  }, deps)
}

export default useIntersectionObserver

์ผ๋‹จ ์ด๋ ‡๊ฒŒ ์ƒ๊ธด custom hook ์„ ๋งŒ๋“ค์–ด ๋†“๋Š”๋‹ค. ์—ฌ๊ธฐ์„œ ํ•ด์ฃผ๋Š” ์ผ์€ ํŠน์ • ์˜์—ญ์— ๋ถ™์ผ ์ˆ˜ ์žˆ๋Š” ํ•จ์ˆ˜๋ฅผ ์ƒ์„ฑํ•ด์ฃผ๋Š”๊ฒƒ. ์ด ํ•จ์ˆ˜๋Š” (node) => void ํ˜•ํƒœ์ธ๋ฐ ์ด๋Ÿฐ ํ˜•ํƒœ์˜ ํ•จ์ˆ˜๋Š” jsx ์—์„œ ref ์†์„ฑ์— ๋„ฃ์–ด์ค„ ์ˆ˜ ์žˆ๋‹ค. ๊ทธ๋Ÿฌ๋ฉด ํ•ด๋‹น element ๊ฐ€ viewport ์™€ ๊ต์ฐจ๋˜๋ฉด ํ•จ์ˆ˜๊ฐ€ ์‹คํ–‰๋˜๋Š” ํ˜•ํƒœ๋‹ค.

์ง€๊ธˆ ์นดํ‚ค์˜คํ†ก ์ฑ„ํŒ…๋ฐฉ์—์„œ๋Š” ๋ฐฉ ์ปดํฌ๋„ŒํŠธ ์•ˆ์— ์ฑ„ํŒ…์„ ๋ณด์—ฌ์ฃผ๋Š” ์ปดํฌ๋„ŒํŠธ๊ฐ€ ์žˆ๊ณ  ๊ฑฐ๊ธฐ์„œ๋„ ์ปดํฌ๋„ŒํŠธ๊ฐ€ ๋˜ ์ชผ๊ฐœ์ง„๋‹ค. ํ•œ๋ฒˆ์— ๋ชฐ์•„์“ฐ๋ฉด ๊ตฌํ˜„์ด ์‰ฌ์›Œ์ง€๊ฒ ์ง€๋งŒ ๊ด€๋ฆฌํ•˜๊ธฐ๊ฐ€ ๋„ˆ๋ฌด ๋”๋Ÿฌ์›Œ์ง€๊ธฐ ๋•Œ๋ฌธ์— ์ ์ ˆํžˆ ์ชผ๊ฐœ๋‹ค ๋ณด๋‹ˆ forward ref ๋ฅผ ์ด์šฉํ•ด์„œ useIntersectionObserver hook ์„ ํ†ตํ•ด ์ƒ์„ฑํ•œ ํ•จ์ˆ˜๋ฅผ ์ „๋‹ฌํ•ด์„œ ref ์†์„ฑ์— ๋„ฃ์–ด์ฃผ๊ฒŒ ๋˜์—ˆ๋‹ค.

// firstChat ๋Š” ์ฒซ๋ฒˆ์งธ ์ฑ„ํŒ…์„ ์˜๋ฏธํ•จ
// ์ตœ์ƒ๋‹จ chat ์ปดํฌ๋„ŒํŠธ์— ๋ถ™์—ฌ์ค€๋‹ค.
const firstChat = useIntersectionObserver(
  (entry: IntersectionObserverEntry) => {
    if (entry.time < 5000) return
    if (!entry.isIntersecting) {
      return
    }
    dispatch(loadMoreRequest(roomUuid))
  },
  { root: root.current },
  [roomUuid]
)
...

chats.map((chat, idx) => (
  <div key={chat.uuid} ref={idx === chats.length - 1 ? lastTop : null}>
    <ChatBox
      chat={chat}
      isMine={userUuid === chat.metaInfo.sender.uuid}
      // ์ตœ์ƒ๋‹จ ์ฑ„ํŒ…์ด๋ฉด์„œ ์ฒซ๋ฒˆ์งธ ์ฑ„ํŒ…์ด ์•„๋‹Œ๊ฒฝ์šฐ (์ฒซ๋ฒˆ์จฐ ์ฑ„ํŒ…์ด๋ฉด ๋” ์š”์ฒญํ•  ๊ฒŒ ์—†์œผ๋ฏ€๋กœ)
      ref={idx === chats.length - 1 && chat.uuid !== firstChat.uuid ? first : null}
    />
  </div>
))

demo

infinite-scroll-demo

Reference