Intersection Observer API๋ฅผ ์‚ฌ์šฉํ•ด ๋ทฐํฌํŠธ๋ฅผ ๋ชจ๋‹ˆํ„ฐ๋งํ•˜๋Š” ์ปดํฌ๋„ŒํŠธ ๋งŒ๋“ค๊ธฐ

2025. 2. 5. 18:42ยทWhat I Learn

์ด์ „์— offset ๊ธฐ๋ฐ˜ ๋ฌดํ•œ์Šคํฌ๋กค์„ ๊ตฌํ˜„ํ–ˆ์„ ๋•Œ๋Š” ์Šคํฌ๋กค ์ด๋ฒคํŠธ๋ฅผ ์‚ฌ์šฉํ•ด์„œ ๋ทฐํฌํŠธ๋ฅผ ๊ฐ์ง€ํ•˜๊ณ  ๋ฐ์ดํ„ฐ๋ฅผ ์ถ”๊ฐ€๋กœ๋“œํ•˜๋„๋ก ํ–ˆ์—ˆ๋Š”๋ฐ, ์ž๋ฐ”์Šคํฌ๋ฆฝํŠธ ๋ฌธ์„œ๋ฅผ ์ญ‰ ์ฝ์–ด๋ณด๋‹ค๊ฐ€ ํŽ˜์ด์ง€ ๋‚ด์— ๊ฐ ์š”์†Œ๊ฐ€ ๊ฐ๊ฐ์˜ ๋ชฉ์ (๊ด‘๊ณ , ๋ ˆ์ด์ง€ ๋กœ๋”ฉ, ๋ฌดํ•œ ์Šคํฌ๋กค ๋“ฑ)์˜ ์ด์œ ๋กœ scroll ์ด๋ฒคํŠธ๋ฅผ ๋ฆฌ์Šค๋‹ํ•˜๊ธฐ ๋•Œ๋ฌธ์— ์ด์— ์ƒ์‘ํ•˜๋Š” ์ฝœ๋ฐฑ์ด ๋ฌด์ˆ˜ํžˆ ์‹คํ–‰๋  ์ˆ˜๋„ ์žˆ๊ณ  ์ด๋Š” ๋ฉ”์ธ ์Šค๋ ˆ๋“œ์— ํฐ ๋ถ€ํ•˜๋ฅผ ์ค„ ์ˆ˜ ์žˆ๋‹ค๋Š” ๊ฒƒ์„ ์•Œ๊ฒŒ ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.

 

๊ทธ๋ž˜์„œ ๋‹ค๋ฅธ ๊ตฌํ˜„ ๋ฐฉ๋ฒ•์„ ์•Œ์•„๋ณด๋‹ค๊ฐ€ ํƒ€๊ฒŸ ์š”์†Œ์™€ ๋ทฐํฌํŠธ์˜ ๊ต์ฐจ์ ์„ ๋น„๋™๊ธฐ์ ์œผ๋กœ ๊ด€์ฐฐํ•ด์„œ ์š”์†Œ๊ฐ€ ๋ทฐํฌํŠธ์— ํฌํ•จ๋˜๋Š”์ง€ ์•„๋‹Œ์ง€ ๊ตฌ๋ณ„ํ•˜๋Š” ๊ธฐ๋Šฅ์„ ์ œ๊ณตํ•˜๋Š” Intersection Observer API๋ฅผ ์•Œ๊ฒŒ ๋˜์—ˆ์Šต๋‹ˆ๋‹ค. ์Šคํฌ๋กค ์ด๋ฒคํŠธ์™€ ๋น„๊ตํ–ˆ์„ ๋•Œ throttling/debouncing์ด ๋ถˆํ•„์š”ํ•˜๋‹ค๋Š” ์ , ๋น„๋™๊ธฐ ์ฒ˜๋ฆฌ๋กœ ๋ฉ”์ธ ์Šค๋ ˆ๋“œ ๋ธ”๋กœํ‚น์„ ๋ฐฉ์ง€ํ•  ์ˆ˜ ์žˆ๋‹ค๋Š” ์ , ๋ฆฌํ”Œ๋กœ์šฐ๋ฅผ ์ตœ์†Œํ™”์‹œํ‚จ๋‹ค๋Š” ์ ์ด ์žฅ์ ์ด์—ˆ๊ธฐ์— ์ด๊ฒƒ์„ ์‚ฌ์šฉํ•ด์„œ ๋ทฐํฌํŠธ๋ฅผ ๋ชจ๋‹ˆํ„ฐ๋งํ•˜๋Š” ์ปดํฌ๋„ŒํŠธ๋ฅผ ๋งŒ๋“ค๊ฒŒ ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.

 

๋จผ์ € observerElement๋ผ๋Š” ref๋ฅผ ์ƒ์„ฑํ•ด์„œ ๊ด€์ฐฐํ•  DOM ์š”์†Œ๋ฅผ ์ฐธ์กฐํ•˜๊ณ , useEffect๋กœ ์ƒํƒœ๋ฅผ ๊ฐ์ง€ํ•˜๊ณ  ์ถ”๊ฐ€ ๋กœ๋“œ๋ฅผ ์ฒ˜๋ฆฌํ–ˆ์Šต๋‹ˆ๋‹ค.

type Props = {
	isLoadingInitial: boolean;
	isLoadingMore: boolean;
	children: React.ReactNode;
	loadMore: () => void;
}

 

์ƒˆ๋กœ์šด IntersectionObserver ์ธ์Šคํ„ด์Šค๋ฅผ ์ƒ์„ฑํ•˜๊ณ , ๊ด€์ฐฐ์ž์—๊ฒŒ ์‚ฌ์šฉํ•  ์ฝœ๋ฐฑ handleIntersection๊ณผ ์„ค์ •์„ ์ „๋‹ฌํ•ฉ๋‹ˆ๋‹ค. Intersection Observer๊ฐ€ ๊ฐ์ง€ํ•œ ์—”ํŠธ๋ฆฌ๋“ค์„ ์ˆœํšŒํ•˜๋ฉด์„œ ์กฐ๊ฑด์„ ๋งŒ์กฑํ•  ๊ฒฝ์šฐ loadMore ํ•จ์ˆ˜๋ฅผ ํ˜ธ์ถœํ•ฉ๋‹ˆ๋‹ค.

  • ๋ทฐํฌํŠธ๋ฅผ ๊ธฐ์ค€์œผ๋กœ, 100px ์—ฌ์œ ๋ฅผ ๋‘์–ด ๋ทฐํฌํŠธ์— ๊ฐ€๊นŒ์›Œ์กŒ์„ ๋•Œ ๋กœ๋“œ๊ฐ€ ์‹œ์ž‘๋˜๋Š”๋ฐ ์š”์†Œ์˜ ์ผ๋ถ€๋ผ๋„ ๋ณด์ผ ๊ฒฝ์šฐ ์ฝœ๋ฐฑ์ด ํ˜ธ์ถœ๋˜๋„๋ก ์„ค์ •ํ–ˆ์Šต๋‹ˆ๋‹ค.
  • observerElement๊ฐ€ ํ˜„์žฌ ์กด์žฌํ•œ๋‹ค๋ฉด ์ด๋ฅผ ๊ด€์ฐฐํ•˜๊ฒŒ ์„ค์ •ํ–ˆ๊ณ , ์ปดํฌ๋„ŒํŠธ ์–ธ๋งˆ์šดํŠธ ์‹œ observer์˜ ๊ด€์ฐฐ์„ ์ค‘์ง€ํ•˜๋„๋ก ํ•ฉ๋‹ˆ๋‹ค.

์˜์กด์„ฑ์—๋Š” isLoadingMore, isLoadingInitial, loadMore์„ ๋‹ด์•„์„œ ํ•„์š”ํ•  ๊ฒฝ์šฐ์—๋งŒ useEffect๊ฐ€ ์‹คํ–‰๋˜๋„๋ก ํ–ˆ์Šต๋‹ˆ๋‹ค.

export default function InfiniteScroll({ isLoadingInitial, isLoadingMore, children, loadMore }: ScrollProps) {
  const observerElement = useRef<HTMLDivElement | null>(null);
  useEffect(() => {
    function IntersectionAPI(entries: IntersectionObserverEntry[]) {
      entries.forEach((entry) => {
        if (entry.isIntersecting && (!isLoadingMore || !isLoadingInitial)) {
          loadMore();
        }
      });
    }

    const observer = new IntersectionObserver(IntersectionAPI, {
      root: null,
      rootMargin: "100px",
      threshold: 0,
    });

    if (observerElement.current) {
      observer.observe(observerElement.current);
    }
    return () => observer.disconnect();
  }, [isLoadingMore, isLoadingInitial, loadMore]);

  return (
    <>
      {children}
      <div ref={observerElement} id="obs">
      </div>
    </>
  );
}

{children} ์˜ ๊ฐ’๋“ค์ด ์ญ‰ ์Šคํฌ๋กค ๋˜๋‹ค๊ฐ€ ref๊ฐ€ ๊ฑธ๋ฆฐ <div>์— ๋‹ฟ์œผ๋ฉด observerElement๊ฐ€ ์‹คํ–‰๋˜๋„๋ก ํ–ˆ์Šต๋‹ˆ๋‹ค.

data fetching ํ•˜๋Š” ํ•จ์ˆ˜๋Š” zustand๋ฅผ ์‚ฌ์šฉํ•ด์„œ ์ „์—ญ์—์„œ ์ƒํƒœ ๊ด€๋ฆฌ ์ค‘์ด์—ˆ๋Š”๋ฐ, ๋ฌดํ•œ ์Šคํฌ๋กค์„ ๊ตฌํ˜„ํ•˜๋ฉด์„œ tasks, currentPage, hasMore ํ•ญ๋ชฉ์„ ์ถ”๊ฐ€ํ–ˆ์Šต๋‹ˆ๋‹ค.

์‚ฌ์šฉํ•  fetchTaskToday์— ๋งค๊ฐœ๋ณ€์ˆ˜๋กœ page๋ฅผ ์ฃผ์—ˆ๊ณ , ๊ธฐ๋ณธ๊ฐ’์€ 0์ž…๋‹ˆ๋‹ค.

export const useTaskStore = create<TodoState>(set => ({
  tasks: [],
  currentPage: 0, 
  hasMore: true,

  fetchTaskToday: async (page: number = 0) => {
    const startDayTime = new Date()
    startDayTime.setHours(0, 0, 0, 0)
    startDayTime.setHours(startDayTime.getHours() + 9)

    const endDayTime = new Date()
    endDayTime.setHours(23, 59, 59, 999)
    endDayTime.setHours(endDayTime.getHours() + 9)

    const supabase = createClient()
    const { data, error } = await supabase
      .from('todolist')
      .select('*')
      .gte('due_date', startDayTime.toISOString())
      .lte('due_date', endDayTime.toISOString())
      .range(page * 20, (page + 1) * 20 - 1)

    if (error) {
      console.error('Fetching today task ERROR:', error)
      return
    }

    if (data) {
      const tasks: Task[] = data as Task[]

      set(state => ({ 
        tasks: page === 0 ? tasks : [...state.tasks, ...tasks],
        hasMore: tasks.length > 0 && tasks.length === 20, 
        currentPage: page,
      }))
    }
  },
  ...
  }))

๋จผ์ € ํŽ˜์ด์ง€๋„ค์ด์…˜์„ ์œ„ํ•ด range ๋ฉ”์†Œ๋“œ๋กœ ๊ฐ ํŽ˜์ด์ง€์˜ 20๊ฐœ์˜ ํ•ญ๋ชฉ๋งŒ ๊ฐ€์ ธ์˜ค๋„๋ก ์„ค์ •ํ•ฉ๋‹ˆ๋‹ค.

๊ทธ๋ฆฌ๊ณ  set ํ•จ์ˆ˜๋ฅผ ์‚ฌ์šฉํ•ด Zustand์˜ ์ƒํƒœ๋ฅผ ์—…๋ฐ์ดํŠธํ–ˆ์Šต๋‹ˆ๋‹ค.

  • tasks๋กœ ๊ธฐ์กด ํƒœ์Šคํฌ ๋ชฉ๋ก์„ ์œ ์ง€ํ•˜๊ณ  ์ดํ›„ ํŽ˜์ด์ง€์— ๋Œ€ํ•œ ์ƒˆ๋กœ์šด ํƒœ์Šคํฌ๋ฅผ ์ถ”๊ฐ€ํ–ˆ์Šต๋‹ˆ๋‹ค.
  • hasMore๋Š” ํƒœ์Šคํฌ ์ˆ˜๊ฐ€ 20์ธ ๊ฒฝ์šฐ true๋กœ ์„ค์ •๋˜์–ด ์ถ”๊ฐ€ ๋ฐ์ดํ„ฐ๊ฐ€ ๋” ์žˆ๋Š”์ง€๋ฅผ ๋‚˜ํƒ€๋ƒ…๋‹ˆ๋‹ค.
  • currentPage๋ฅผ ์—…๋ฐ์ดํŠธํ•˜์—ฌ ํ˜„์žฌ ํŽ˜์ด์ง€๋ฅผ ์ถ”์ ํ•˜๋„๋ก ํ–ˆ์Šต๋‹ˆ๋‹ค.

๋ฌดํ•œ ์Šคํฌ๋กค์„ ๊ตฌํ˜„ํ•  ์ปดํฌ๋„ŒํŠธ์—์„œ useTaskStore์—์„œ ์„ ์–ธํ•œ tasks, fetchTaskToday, currentPage, hasMore์„ ๊ฐ€์ ธ์™”๊ณ 

useState๋กœ isLoadingMore์„ ๊ด€๋ฆฌํ•˜๋„๋ก ํ•˜๋ฉด์„œ InfiniteScroll ์ปดํฌ๋„ŒํŠธ์— props๋กœ ๋‚ด๋ ค์ค„ isLoadingMore={} ์— ํ• ๋‹นํ•ด์ฃผ์—ˆ์Šต๋‹ˆ๋‹ค.

  const tasks = useTaskStore(state => state.tasks)
  const fetchTaskToday = useTaskStore(state => state.fetchTaskToday)
  const currentPage = useTaskStore(state => state.currentPage)
  const hasMore = useTaskStore(state => state.hasMore)
  const [isLoadingMore, setIsLoadingMore] = useState(false)

  useEffect(() => {
    const loadTasks = async () => {
      if (!hasMore) return

      setIsLoadingMore(true)
      await fetchTaskToday()
      setIsLoadingMore(false)
    }
    loadTasks()
  }, [fetchTaskToday, hasMore])
    const loadMoreTasks = async () => {
   if (!hasMore || isLoadingMore) return

   setIsLoadingMore(true)
   await fetchTaskToday(currentPage + 1)
   setIsLoadingMore(false)
 }

์ปดํฌ๋„ŒํŠธ์— ๋งˆ์šดํŠธ ๋˜์—ˆ์„ ๋•Œ fetchTaskToday()๋ฅผ ํ˜ธ์ถœํ•˜๋„๋ก ํ–ˆ์Šต๋‹ˆ๋‹ค.

๊ทธ๋ฆฌ๊ณ  hasMore๊ณผ isLoadingMore ๋‘˜ ์ค‘ ํ•˜๋‚˜๋ผ๋„ false๋ผ๋ฉด return ํ•˜๋„๋ก ํ•˜๋ฉด์„œ ๋‘˜๋‹ค ๋งŒ์กฑํ•œ๋‹ค๋ฉด fetchTaskToday์— ํŽ˜์ด์ง€๋ฅผ ๋„˜๊ฒจ์„œ ํ˜ธ์ถœํ•˜๋„๋ก ํ•˜๋Š” ํ•จ์ˆ˜๋ฅผ ์ž‘์„ฑํ•˜๊ณ  InfiniteScroll ์ปดํฌ๋„ŒํŠธ์˜ props๋กœ ๋‚ด๋ ค์ฃผ๋„๋ก ํ•ด์„œ ์Šคํฌ๋กค์ด ๋ฐ”๋‹ฅ์— ๋„๋‹ฌํ–ˆ์„ ๋•Œ ์ถ”๊ฐ€ ๋ฐ์ดํ„ฐ๋ฅผ ๋กœ๋“œํ•˜๋„๋ก ํ–ˆ์Šต๋‹ˆ๋‹ค.

<InfiniteScroll isLoadingInitial={false} isLoadingMore={isLoadingMore} loadMore={loadMoreTasks}>
   {tasks.map(task => (
      <TodoItem
         key={task.todo_id}
         task={task}
         viewTaskBtn={viewTaskBtn}
         checkTask={checkingBox}
         checked={!!checkedTask[task.todo_id.toString()]}
        />
          ))}
        </InfiniteScroll>

์ด๋ ‡๊ฒŒ IntersectionObserver๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ๋ ˆ์ด์•„์›ƒ ๋ณ€ํ™”์— ๋Œ€ํ•œ ์„ฑ๋Šฅ ๋ถ€๋‹ด์„ ์ค„์ด๊ณ , ํด๋ผ์ด์–ธํŠธ์˜ ๋ถ€ํ•˜๋ฅผ ์ตœ์†Œํ™”ํ•˜๋Š” ์Šคํฌ๋กค ๊ฐ์ง€ ์ปดํฌ๋„ŒํŠธ๋ฅผ ๊ตฌํ˜„ํ–ˆ์Šต๋‹ˆ๋‹ค.

๊ตฌํ˜„ ์ „ ๋ชจ์Šต์—์„œ๋Š” ์ฒ˜์Œ ํŽ˜์ด์ง€๊ฐ€ ๋กœ๋“œ๋˜์—ˆ์„ ๋•Œ ๋ชจ๋“  ๋ฐ์ดํ„ฐ๋ฅผ ํ•œ๋ฒˆ์— ๊ฐ€์ ธ์˜ต๋‹ˆ๋‹ค.

๊ตฌํ˜„ ์ „ (๋ชจ๋ฐ”์ผ)

 

๊ตฌํ˜„ ํ›„, ์Šคํฌ๋กค๋ฐ”๋ฅผ ๋ณด๋ฉด rootMargin ์„ค์ •์œผ๋กœ ์ธํ•ด ๋ฐ”๋‹ฅ์˜ ๋‹ฟ๊ธฐ ์ „์— ๋ฐ์ดํ„ฐ๋ฅผ ์ถ”๊ฐ€๋กœ ๊ฐ€์ ธ์˜ค๋Š” ๊ฒƒ์„ ๋ณผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๊ตฌํ˜„ ํ›„

'What I Learn' ์นดํ…Œ๊ณ ๋ฆฌ์˜ ๋‹ค๋ฅธ ๊ธ€

useCustom hook์„ ๋งŒ๋“ค์–ด ์„œ๋น„์Šค ํ—ค๋”์˜ ๋’ค๋กœ๊ฐ€๊ธฐ ๊ธฐ๋Šฅ ๊ตฌํ˜„  (0) 2025.02.05
husky์™€ GitHub Actions์„ ์‚ฌ์šฉํ•œ CICD  (2) 2025.02.05
Next.js ๋ณ‘๋ ฌ ๋ผ์šฐํŒ… & ๊ฒฝ๋กœ ๊ฐ€๋กœ์ฑ„๊ธฐ๋ฅผ ํ†ตํ•œ ๋ชจ๋‹ฌ ํŽ˜์ด์ง€ ๊ตฌํ˜„ 2  (0) 2024.12.26
e.stopPropagation(), ์ด๋ฒคํŠธ ์ „ํŒŒ ๋ฐฉ์ง€  (0) 2024.12.10
format date as 'YYYY.MM.DD AMhh:mm'  (0) 2024.12.10
'What I Learn' ์นดํ…Œ๊ณ ๋ฆฌ์˜ ๋‹ค๋ฅธ ๊ธ€
  • useCustom hook์„ ๋งŒ๋“ค์–ด ์„œ๋น„์Šค ํ—ค๋”์˜ ๋’ค๋กœ๊ฐ€๊ธฐ ๊ธฐ๋Šฅ ๊ตฌํ˜„
  • husky์™€ GitHub Actions์„ ์‚ฌ์šฉํ•œ CICD
  • Next.js ๋ณ‘๋ ฌ ๋ผ์šฐํŒ… & ๊ฒฝ๋กœ ๊ฐ€๋กœ์ฑ„๊ธฐ๋ฅผ ํ†ตํ•œ ๋ชจ๋‹ฌ ํŽ˜์ด์ง€ ๊ตฌํ˜„ 2
  • e.stopPropagation(), ์ด๋ฒคํŠธ ์ „ํŒŒ ๋ฐฉ์ง€
nuew
nuew
๐Ÿคธ ์žฌ์ฃผ ๋„˜๋Š” ์ค‘
  • nuew
    bloggg. . .๐Ÿฆ–๐Ÿ’ฅ
    nuew
  • ์ „์ฒด
    ์˜ค๋Š˜
    ์–ด์ œ
    • ๋ถ„๋ฅ˜ ์ „์ฒด๋ณด๊ธฐ (88)
      • issue (10)
      • baekjoon (41)
      • lecture recap (11)
      • What I Learn (26)
      • retrospective (0)
      • maeil-mail (0)
  • ๋ธ”๋กœ๊ทธ ๋ฉ”๋‰ด

    • ํ™ˆ
    • ํƒœ๊ทธ
    • ๋ฐฉ๋ช…๋ก
  • ๋งํฌ

  • ๊ณต์ง€์‚ฌํ•ญ

  • ์ธ๊ธฐ ๊ธ€

  • ํƒœ๊ทธ

    ํ•œ์ž…ํฌ๊ธฐ๋กœ์ž˜๋ผ๋จน๋Š”ํƒ€์ž…์Šคํฌ๋ฆฝํŠธ
    TailwindCSS
    issue
    ๋ฐฑ์ค€
    Algorithm
    TypeScript
    JavaScript
    ์ž๋ฐ”์Šคํฌ๋ฆฝํŠธ
    Baekjoon
    js
    ํ•œ์ž…ํฌ๊ธฐ๋กœ ์ž˜๋ผ๋จน๋Š” ํƒ€์ž…์Šคํฌ๋ฆฝํŠธ
    Study
    Node.js
    media-query
    ์ฝ”๋”ฉํ…Œ์ŠคํŠธ
    modal
    css
    ์•Œ๊ณ ๋ฆฌ์ฆ˜
    what i learn
    zustand
  • ์ตœ๊ทผ ๋Œ“๊ธ€

  • ์ตœ๊ทผ ๊ธ€

  • hELLOยท Designed By์ •์ƒ์šฐ.v4.10.3
nuew
Intersection Observer API๋ฅผ ์‚ฌ์šฉํ•ด ๋ทฐํฌํŠธ๋ฅผ ๋ชจ๋‹ˆํ„ฐ๋งํ•˜๋Š” ์ปดํฌ๋„ŒํŠธ ๋งŒ๋“ค๊ธฐ
์ƒ๋‹จ์œผ๋กœ

ํ‹ฐ์Šคํ† ๋ฆฌํˆด๋ฐ”