๋ฌดํ•œ์Šคํฌ๋กค ์„ฑ๋Šฅ ์ธก์ •/๊ฐœ์„ ํ•˜๊ธฐ(2)

2026. 4. 4. 03:44ใ†Trouble Shooting & Issues/Linkiving

๋ฐ˜์‘ํ˜•

์•„ ์˜ˆ์˜๋‹ค

 

useEffect ์˜์กด์„ฑ ํ•ด๊ฒฐ๋กœ fetch ๋ฐ˜๋ณต ๋ฌธ์ œ ํ•ด๊ฒฐ
์ปจํ…Œ์ด๋„ˆ ๋†’์ด ํ™•์‹คํ•˜๊ฒŒ ์žก์•„์„œ ์Šคํฌ๋กค ์‹œ fetch ๋˜๋„๋ก ํ•ด๊ฒฐ

๋ชฉ์ฐจ

1. ๊ธฐ์กด InfiniteScroll ์ƒํƒœ

2. ๋ฌธ์ œ1) useEffect ๋‚ด๋ถ€์—์„œ์˜ ๋ฐ˜๋ณต ํ˜ธ์ถœ ๋ฌธ์ œ

3. ๋ฌธ์ œ2) DOM ๋ˆ„์  ๋ฌธ์ œ

4. ์ˆ˜์ • ํ›„ ์„ฑ๋Šฅ

5. ๋ฐฐํฌ ํ›„ ํ…Œ์ŠคํŠธ

6. ์ •๋ฆฌ

 


1. ํ˜„์žฌ ๋ณด์ด๋Š” ๋ฌธ์ œ์™€ ๊ธฐ์กด InfiniteScroll ์ƒํƒœ

1. ํ˜„์žฌ ๋ณด์ด๋Š” ๋ฌธ์ œ

๋‘๊ฐ€์ง€ ์ƒํ™ฉ์ด ๋ฐ˜๋ณต๋˜์–ด์„œ ๋‚˜ํƒ€๋‚˜๊ณ  ์žˆ์—ˆ๋‹ค.

1. ์ฒ˜์Œ ํŽ˜์ด์ง€ ๋กœ๋“œ ์‹œ Fetch๊ฐ€ ์—ฐ์†ํ•ด์„œ ์‹คํ–‰๋˜์–ด DOM์— ๋ชจ๋“  ์นด๋“œ๊ฐ€ ๋ Œ๋”๋ง๋จ

2. 1์„ ํ•ด๊ฒฐํ•ด๋ณด๋ฉด Scroll์„ ๋ฐ”๋‹ฅ๊นŒ์ง€ ๋‚ด๋ ค๋„ fetch๊ฐ€ ์•„์˜ˆ ๋˜์ง€ ์•Š์Œ

 

2. ๊ธฐ์กด InfiniteScroll ์ƒํƒœ

์ˆ˜์ •์„ ํ•˜๊ณ  ํ•˜๋‹ค๊ฐ€ ๊ธฐ์กด ์ƒํƒœ๋Š” ์•„๋ž˜์™€ ๊ฐ™๋‹ค.

์‹œ๊ฐ„์ด ์ข€ ๋ถ€์กฑํ•œ ๊ด€๊ณ„๋กœ ์ผ๋‹จ ๊ธฐ์กด ์ฝ”๋“œ๋ฅผ ๋ƒ…๋‹ค ๋‘ฌ๋ณด๊ฒ ๋‹ค.

๋”๋ณด๊ธฐ
'use client';

import { type Link } from '@/types/link';
import { Virtualizer, useVirtualizer } from '@tanstack/react-virtual';
import clsx from 'clsx';
import React, { useCallback, useEffect, useMemo, useRef } from 'react';

import Spinner from '../Spinner/Spinner';
import { styles } from './InfiniteScroll.style';

const { root: rootCls, statusRow, loader: loaderCls, end, error } = styles();

const CARD_ASPECT = 58 / 47;
const ROW_GAP = 16;
const FALLBACK_ROW_HEIGHT = 232;
const RESIZE_DEBOUNCE_MS = 150;
const LOAD_AHEAD_PX = 300;

function useColumns() {
  const [columns, setColumns] = React.useState(() =>
    typeof window !== 'undefined' && window.innerWidth >= 768 ? 4 : 2
  );

  useEffect(() => {
    let timer: ReturnType<typeof setTimeout>;
    const handleResize = () => {
      clearTimeout(timer);
      timer = setTimeout(() => {
        setColumns(window.innerWidth >= 768 ? 4 : 2);
      }, 100);
    };
    window.addEventListener('resize', handleResize);
    return () => {
      window.removeEventListener('resize', handleResize);
      clearTimeout(timer);
    };
  }, []);

  return columns;
}

export type InfiniteScrollProps = Omit<React.HTMLAttributes<HTMLDivElement>, 'children'> & {
  items: Link[];
  renderItem: (item: Link, index: number) => React.ReactNode;
  onLoadMore: () => void | Promise<void>;
  hasMore: boolean;
  isLoading?: boolean;
  errorMessage?: string | null;
  loader?: React.ReactNode;
  endMessage?: React.ReactNode;
  errorSlot?: (msg: string) => React.ReactNode;
};

const InfiniteScroll = React.forwardRef<HTMLDivElement, InfiniteScrollProps>(
  function InfiniteScroll(
    {
      items,
      renderItem,
      className,
      onLoadMore,
      hasMore,
      isLoading = false,
      errorMessage = null,
      loader,
      endMessage,
      errorSlot,
      ...rest
    }: InfiniteScrollProps,
    ref: React.ForwardedRef<HTMLDivElement>
  ) {
    const innerRef = useRef<HTMLDivElement | null>(null);
    const loadingRef = useRef<boolean>(false);
    const containerWidthRef = useRef(0);
    const columns = useColumns();

    const setRef = (el: HTMLDivElement | null) => {
      innerRef.current = el;
      if (typeof ref === 'function') ref(el);
      else if (ref) ref.current = el;
    };

    useEffect(() => {
      loadingRef.current = isLoading;
    }, [isLoading]);

    // canLoadRef: render-time snapshot read synchronously inside event handlers
    const canLoadRef = useRef(false);
    canLoadRef.current = hasMore && !isLoading && !errorMessage;

    const rows = useMemo(() => {
      const result: Link[][] = [];
      for (let i = 0; i < items.length; i += columns) {
        result.push(items.slice(i, i + columns));
      }
      return result;
    }, [items, columns]);

    const estimateSize = useCallback(() => {
      const width = containerWidthRef.current;
      if (!width) return FALLBACK_ROW_HEIGHT;
      const cardWidth = (width - ROW_GAP * (columns - 1)) / columns;
      return cardWidth * CARD_ASPECT + ROW_GAP;
    }, [columns]);

    const observeElementRect = useCallback(
      (
        instance: Virtualizer<HTMLDivElement, Element>,
        cb: (rect: { width: number; height: number }) => void
      ) => {
        const el = instance.scrollElement;
        if (!el) return;

        const notify = () => {
          containerWidthRef.current = el.clientWidth;
          cb({ width: el.clientWidth, height: el.clientHeight });
        };

        notify();

        let timer: ReturnType<typeof setTimeout>;
        const ro = new ResizeObserver(() => {
          clearTimeout(timer);
          timer = setTimeout(notify, RESIZE_DEBOUNCE_MS);
        });
        ro.observe(el);

        return () => {
          ro.disconnect();
          clearTimeout(timer);
        };
      },
      []
    );

    const virtualizer = useVirtualizer({
      count: rows.length,
      getScrollElement: () => innerRef.current,
      estimateSize,
      overscan: 3,
      observeElementRect,
    });

    useEffect(() => {
      virtualizer.measure();
    }, [columns, virtualizer]);

    useEffect(() => {
      const el = innerRef.current;
      if (!el) return;

      const handleScroll = () => {
        if (loadingRef.current || !canLoadRef.current) return;
        const { scrollTop, scrollHeight, clientHeight } = el;
        if (scrollTop + clientHeight >= scrollHeight - LOAD_AHEAD_PX) {
          loadingRef.current = true;
          onLoadMore();
        }
      };

      el.addEventListener('scroll', handleScroll, { passive: true });
      return () => el.removeEventListener('scroll', handleScroll);
    }, [onLoadMore]);

    useEffect(() => {
      const el = innerRef.current;
      if (!el || !canLoadRef.current || loadingRef.current) return;
      if (el.scrollHeight <= el.clientHeight) {
        loadingRef.current = true;
        onLoadMore();
      }
    }, [items.length, onLoadMore]);

    return (
      <div ref={setRef} className={clsx(className, rootCls())} {...rest}>
        <div style={{ height: virtualizer.getTotalSize(), position: 'relative' }}>
          {virtualizer.getVirtualItems().map(virtualRow => {
            const row = rows[virtualRow.index];
            if (!row) return null;
            return (
              <div
                key={virtualRow.key}
                data-index={virtualRow.index}
                style={{
                  position: 'absolute',
                  top: 0,
                  left: 0,
                  width: '100%',
                  transform: `translateY(${virtualRow.start}px)`,
                }}
              >
                <div className="grid grid-cols-2 gap-4 pb-4 md:grid-cols-4">
                  {row.map((item, i) => (
                    <React.Fragment key={virtualRow.index * columns + i}>
                      {renderItem(item, virtualRow.index * columns + i)}
                    </React.Fragment>
                  ))}
                </div>
              </div>
            );
          })}
        </div>

        {errorMessage && (
          <div className={clsx(statusRow(), error())} role="status" aria-live="polite">
            {errorSlot ? errorSlot(errorMessage) : <span>{errorMessage}</span>}
          </div>
        )}

        {isLoading && (
          <div className={clsx(statusRow(), loaderCls())} role="status" aria-live="polite">
            {loader ?? <Spinner />}
          </div>
        )}

        {!hasMore && !isLoading && !errorMessage && (
          <div className={clsx(statusRow(), end())} role="status" aria-live="polite">
            {endMessage ?? <span>๋งˆ์ง€๋ง‰์ž…๋‹ˆ๋‹ค</span>}
          </div>
        )}
      </div>
    );
  }
);

export default React.memo(InfiniteScroll);

 


2. ๋ฌธ์ œ1) useEffect ๋‚ด๋ถ€์—์„œ์˜ ๋ฐ˜๋ณต ํ˜ธ์ถœ ๋ฌธ์ œ

๊ธฐ์กด ๊ตฌํ˜„์€ ์Šคํฌ๋กค ์ด๋ฒคํŠธ ๊ธฐ๋ฐ˜ + ์ƒํƒœ ์กฐํ•ฉ ๋ฐฉ์‹์ด์—ˆ๋‹ค.

useEffect(() => {
  const handleScroll = () => {
    if (loadingRef.current || !canLoadRef.current) return;

    if (scrollTop + clientHeight >= scrollHeight - LOAD_AHEAD_PX) {
      loadingRef.current = true;
      onLoadMore();
    }
  };
}, [onLoadMore]);

์Šคํฌ๋กค ์ด๋ฒคํŠธ๋Š” ์ˆ˜ ms ๋‹จ์œ„๋กœ ๋งค์šฐ ๋นˆ๋ฒˆํ•˜๊ฒŒ ๋ฐœ์ƒํ•œ๋‹ค. ์กฐ๊ฑด์„ ๋งŒ์กฑํ•˜๋Š” ์ˆœ๊ฐ„ ์—ฐ์† ํ˜ธ์ถœ ๊ฐ€๋Šฅ์„ฑ์ด ์žˆ๋Š” ๊ฒƒ์ด๋‹ค.

์ด๊ฒƒ์„ loadingRef๋งŒ์œผ๋กœ ๋ง‰๋Š” ๊ฒƒ์€ ๋ถˆ์™„์ „ํ•˜๋‹ค.

loadingRef๋Š” ์™ธ๋ถ€ state๋กœ, ๋ฐ˜์˜ ํƒ€์ด๋ฐ์ด ์ง€์—ฐ๋  ์ˆ˜ ์žˆ์œผ๋ฉฐ, ์ด ์ง€์—ฐ๋˜๋Š” ์‹œ๊ฐ„ ์‚ฌ์ด์— race condition์ด ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ๋‹ค.

→ API ์ค‘๋ณต ํ˜ธ์ถœ

 ํŽ˜์ด์ง€ ๊ผฌ์ž„

 ๋ถˆํ•„์š”ํ•œ ๋ Œ๋”๋ง ์ฆ๊ฐ€

์˜ ๋ฌธ์ œ๊ฐ€ ๋ฐœ์ƒํ•˜๊ฒŒ ๋œ๋‹ค.

 

 

1. isFetching ์ƒํƒœ ๊ด€๋ฆฌํ•˜๊ธฐ

isFetching์˜ ํ˜„์žฌ ์ƒํƒœ๋ฅผ ๋ฆฌ์•กํŠธ์˜ ๋ Œ๋”๋ง ์ฃผ๊ธฐ์™€ ๋ฌด๊ด€ํ•˜๊ฒŒ ์ฆ‰๊ฐ์ ์œผ๋กœ ์•Œ ์ˆ˜ ์žˆ๋„๋ก useRef๋ฅผ ์‚ฌ์šฉํ•œ isFetchingRef๋ฅผ ๋„์ž…

const isFetchingRef = useRef(false);

React State๊ฐ€ ์•„๋‹Œ ์ฆ‰์‹œ ๋ฐ˜์˜๋˜๋Š” mutable ๊ฐ’์„ ์ฃผ์–ด ๋ฐ˜์˜ ํƒ€์ด๋ฐ ์ง€์—ฐ์„ ๋ง‰๋Š”๋‹ค.

 

๋ฐ”๋‹ฅ ๊ฐ์ง€ ๋˜ํ•œ ๊ธฐ์กด ๋ฌผ๋ฆฌ์  ์œ„์น˜์ธ scrollTop ๊ธฐ๋ฐ˜์—์„œ virtual row๋ฅผ ํ™œ์šฉํ•œ ๋งˆ์ง€๋ง‰ 2์ค„ ๊ทผ์ฒ˜๋กœ ๋” ์•ˆ์ •์ ์ด๋„๋ก ํ•œ๋‹ค.

const lastItem = virtualRows[virtualRows.length - 1];
const isNearBottom = lastItem.index >= rows.length - 2;

 

2. onLoadMore ํ˜ธ์ถœ ์กฐ๊ฑด ์—„๊ฒฉํ•˜๊ฒŒ ์ œํ•œ

ํ˜ธ์ถœ ์กฐ๊ฑด์„ ์—„๊ฒฉํ•˜๊ฒŒ ์ œํ•œํ•ด์„œ ๊ฐ€๋“œ๋ฅผ ํ•œ๋‹ค.

if (
  isNearBottom &&
  hasMore &&
  !isLoading &&
  !errorMessage &&
  !isFetchingRef.current
)
  • hasMore: ๋” ๋ถˆ๋Ÿฌ์˜ฌ ๋ฐ์ดํ„ฐ๊ฐ€ ์žˆ๋Š”๊ฐ€
  • isLoading: ์™ธ๋ถ€์—์„œ loading ์ƒํƒœ์ด์ง€๋Š” ์•Š์€๊ฐ€
  • errorMessage: ์—๋Ÿฌ ์ƒํƒœ๋Š” ์•„๋‹Œ๊ฐ€
  • isFetchingRef: ๋‚ด๋ถ€ ๋™๊ธฐ์ƒํƒœ๋Š” ์•„๋‹Œ๊ฐ€

๋˜ํ•œ Promise ๊ธฐ๋ฐ˜ ์™„์ „ ์ฐจ๋‹จ ์ฝ”๋“œ๋„ ์ถ”๊ฐ€ํ•œ๋‹ค.

const result = safeLoadMore();

if (result instanceof Promise) {
  result.finally(() => {
    isFetchingRef.current = false;
  });
}

finally์— false๋ฅผ ๋„ฃ์–ด, ์š”์ฒญ์ด ๋๋‚  ๋•Œ๊นŒ์ง€๋Š” ์ ˆ๋Œ€ ์žฌํ˜ธ์ถœ์ด ๋ถˆ๊ฐ€๋Šฅํ•˜๋„๋ก ํ•˜์˜€๋‹ค.

 

AbortController๋กœ ์ด์ „ ์š”์ฒญ ์ทจ์†Œ๋„ ๊ฐ€๋Šฅํ•˜๋„๋ก ํ–ˆ๋‹ค.

if (controllerRef.current) {
  controllerRef.current.abort();
}

๋น ๋ฅธ ์Šคํฌ๋กค ์‹œ ์ด์ „ ์š”์ฒญ์„ ๋ฌดํšจํ™” ํ•˜์—ฌ ์ตœ์‹  ์š”์ฒญ๋งŒ ์œ ์ง€ํ•˜๋„๋ก ํ•œ๋‹ค.


3. ๋ฌธ์ œ2) DOM ๋ˆ„์  ๋ฌธ์ œ

useVirtualizer๋ฅผ ์‚ฌ์šฉํ•˜๊ณ  ์žˆ์Œ์—๋„ DOM์— ๋ฐ์ดํ„ฐ๊ฐ€ ์Œ“์ธ๋‹ค๋Š” ๊ฒƒ์€

์ปจํ…Œ์ด๋„ˆ์˜ ๋†’์ด๊ฐ€ ์ œ๋Œ€๋กœ ์žกํžˆ์ง€ ์•Š์•„์„œ ๋ธŒ๋ผ์šฐ์ € ๊ธฐ๋ณธ ์Šคํฌ๋กค์„ ์‚ฌ์šฉํ•˜๊ณ  ์žˆ๊ฑฐ๋‚˜, ๊ฐ€์ƒํ™” ๋ ˆ์ด์•„์›ƒ์ด ์ •์ƒ์ž‘๋™ ํ•˜์ง€ ์•Š๋Š” ๊ฒƒ์‹ค์ œ ๋†’์ด๋ฅผ ์ œ๋Œ€๋กœ ๊ณ„์‚ฐํ•˜์ง€ ๋ชปํ•˜๋Š” ๊ฐ€๋Šฅ์„ฑ์ด ์žˆ๋‹ค.

 

๊ธฐ์กด ์ฝ”๋“œ์—์„œ๋Š” estimateSize()๋ฅผ ์‚ฌ์šฉํ•ด์„œ ์ปจํ…Œ์ด๋„ˆ width ๊ธฐ๋ฐ˜ ๊ณ„์‚ฐ์„ ํ•˜๋Š”๋ฐ,

์ด๊ฒŒ ์‹ค์ œ DOM๊ณผ mismatch ๋ฐœ์ƒํ•  ๊ฐ€๋Šฅ์„ฑ์ด ์žˆ๋‹ค.

 

estimateSize()๋Š” ์นด๋“œ ๋น„์œจ์ด ์ผ์ •ํ•˜๊ณ  ์ฝ˜ํ…์ธ  ๋†’์ด๊ฐ€ ๊ณ ์ •๋˜์–ด์žˆ์Œ์„ ๊ฐ€์ •ํ•œ ์ถ”์ •๊ฐ’์ธ๋ฐ,

์‹ค์ œ DOM์€ ์ด๋ฏธ์ง€ ๋กœ๋”ฉ ์ „/ํ›„ ๋†’์ด ๋ณ€ํ™”, ํฐํŠธ ๋ Œ๋”๋ง ์ดํ›„์˜ ๋ณ€ํ™”, ํ…์ŠคํŠธ ๊ธธ์ด์— ๋”ฐ๋ฅธ ์ค„๋ฐ”๊ฟˆ ๋“ฑ์˜ ๋ณ€ํ™”๊ฐ€ ์žˆ๊ธฐ ๋•Œ๋ฌธ์ด๋‹ค.

 

๋˜ํ•œ estimate์—์„œ๋Š” padding, border, gap ์ฒ˜๋ฆฌ ๋ฐฉ์‹ ๋“ฑ์˜ ์š”์†Œ๋ฅผ ํฌํ•จํ•˜์ง€ ์•Š์•„ ์‹ค์ œ DOM์ด ๋” ์ปค์งˆ ์ˆ˜ ์žˆ๋‹ค.

 

measureElement๋กœ rowVirtualizer์˜ ๋†’์ด๋ฅผ ์ธก์ •ํ•˜๋ฉด

์‹ค์ œ DOM height์„ ์ง์ ‘ ์ธก์ •ํ•˜์—ฌ virtualizer์— ๋ฐ˜์˜ํ•ด์„œ ๋น„์šฉ์€ ์กฐ๊ธˆ ์ƒ๊ธฐ๋‚˜ ์ •ํ™•๋„๊ฐ€ ์˜ฌ๋ผ๊ฐ„๋‹ค.

 

๊ณ ์ • ๋†’์ด์— ์ฝ˜ํ…์ธ  ๋ณ€ํ™”๊ฐ€ ์—†๊ณ , ์ด๋ฏธ์ง€๊ฐ€ ์—†๊ฑฐ๋‚˜ aspect ratio๊ฐ€ ์™„์ „ํžˆ ๊ณ ์ •๋˜๋ฉด estimate๋กœ๋„ ์ถฉ๋ถ„ํ•˜๋‚˜,

์นด๋“œ ๋‚ด๋ถ€ ์ฝ˜ํ…์ธ ๊ฐ€ ๋™์ ์ด๊ณ  ์ด๋ฏธ์ง€๊ฐ€ ํฌํ•จ๋˜๊ณ , ๋ฐ˜์‘ํ˜• grid์— ์ค„๋ฐ”๊ฟˆ์ด ์žˆ๋Š” ์š”์†Œ๋ฅผ ๊ทธ๋ฆด ๋•Œ๋Š” mismatch๊ฐ€ ๋ฐœ์ƒํ•˜๋ฏ€๋กœ

measureElement๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ์ •ํ™•๋„๊ฐ€ ํ›จ์”ฌ ์˜ฌ๋ผ๊ฐ„๋‹ค.

 

 

1. measureElement ์ ์šฉ

ref={rowVirtualizer.measureElement}

measureElement๋กœ ๊ฐ row์˜ ์‹ค์ œ DOM ๋†’์ด๋ฅผ ์ธก์ •ํ•˜์—ฌ virtualizer๊ฐ€ ์ •ํ™•ํ•œ layout์„ ๊ณ„์‚ฐํ•˜๋„๋ก ํ–ˆ๋‹ค.

 

2. strict layout ์กฐ๊ฑด ์ถฉ์กฑ

<div
  className="... overflow-y-auto contain-strict"
>
...
<div style={{ position: 'relative' }}>
  <div style={{ position: 'absolute', transform: translateY(...) }}>

 

๊ฐ€์ƒํ™” ํ•„์ˆ˜ ์กฐ๊ฑด 3๊ฐœ๊ฐ€ ์žˆ๋‹ค.

  • ๋ถ€๋ชจ → scroll container
  • ๋‚ด๋ถ€ → relative
  • row → absolute positioning

 

3. totalSize ๊ธฐ๋ฐ˜ ์Šคํฌ๋กค ์˜์—ญ ์ƒ์„ฑ

height: `${rowVirtualizer.getTotalSize()}px`

 

  • ์ „์ฒด ๋†’์ด๋ฅผ ๊ฐ€์งœ๋กœ ๋งŒ๋“ค์–ด์„œ
  • ์Šคํฌ๋กค์€ ์œ ์ง€ํ•˜๊ณ 
  • ์‹ค์ œ DOM์€ ์ตœ์†Œํ™”

 

4. ๋ฐ˜์‘ํ˜• Grid + Row ๋‹จ์œ„ ๊ฐ€์ƒํ™”

for (let i = 0; i < items.length; i += currentColumns)
  • ์นด๋“œ ๋‹จ์œ„๊ฐ€ ์•„๋‹ˆ๋ผ row ๋‹จ์œ„ virtualizing์œผ๋กœ ์„ฑ๋Šฅ์ƒ ๋” ํšจ์œจ์ ์ธ ๋ฐฉ์‹ ์‚ฌ์šฉ

 5. ResizeObserver ๊ธฐ๋ฐ˜ ์ปฌ๋Ÿผ ๊ด€๋ฆฌ

const observer = new ResizeObserver(...)
๊ธฐ์กด window resize์˜€๋˜ ๊ฒƒ์„ ์ปจํ…Œ์ด๋„ˆ ๊ธฐ์ค€ ๋ฐ˜์‘ํ˜•์œผ๋กœ ์ˆ˜์ •ํ•ด์„œ ๋” ์ •ํ™•ํ•œ ๋ ˆ์ด์•„์›ƒ์„ ์ฃผ๋„๋ก ํ•จ
 
 
์ถ”๊ฐ€๋กœ InfiniteScroll์„ ๊ฐ์‹ธ๋Š” AllPage ๋ถ€๋ชจ์—์„œ h-screen์„ ๋ช…์‹œํ•ด์„œ ์Šคํฌ๋กค ์˜์—ญ์„ ๋ช…ํ™•ํ•˜๊ฒŒ ์ง€์ •
 
 
 
 
 

 


4. ์ˆ˜์ • ํ›„ ์„ฑ๋Šฅ

์ด์ œ ์ˆ˜์ •์„ ๋งˆ์ณค์œผ๋‹ˆ ๋‹ค์‹œ ์„ฑ๋Šฅ ์ฝ˜์†”์„ ๋ณด์ž.

ํ•œ ๋ฒˆ fetchํ•œ ๋ฐ์ดํ„ฐ๋Š” ์บ์‹œ๋กœ ์ €์žฅํ•ด์„œ ๋‹ค์‹œ ์Šคํฌ๋กค์„ ์˜ฌ๋ฆด ๋•Œ๋Š” fetch๊ฐ€ ๋ฐœ์ƒํ•˜์ง€ ์•Š๋Š”๋‹ค.

  API Total Time Scroll Fetch Latency Render Time
first load 133.5 ms
199.29999999701977 ms
209.30000000447035 ms 235.20000000298023 ms
1 92.29999999701977 ms 107.29999999701977 ms 107.59999999403954 ms
2 65.29999999701977 ms 123.30000000447035 ms 123.70000000298023 ms
3 119.39999999850988 ms 121.10000000149012 ms 169 ms
4 116.19999999552965 ms 130.79999999701977 ms 131 ms
5 46.79999999701977 ms 61.80000000447035 ms 62 ms
6 110.70000000298023 ms 125.69999999552965 ms 126 ms
7 110.89999999850988 ms 112.69999999552965 ms 128.69999999552965 ms
8 70.39999999850988 ms 116.20000000298023 ms 116.5 ms

 

๊ฐ€์ƒํ™” ์ด์ „ ์ˆ˜์น˜

 

๊ฐ€์ƒํ™” ์ด์ „์—๋Š” ๋ชจ๋“  ์ˆ˜์น˜๊ฐ€ ์ ์  ์ฆ๊ฐ€ํ–ˆ๋‹ค๋ฉด, ์ด์ œ๋Š” ์ฒ˜์Œ๋ถ€ํ„ฐ ๋๊นŒ์ง€ ์ผ์ •ํ•˜๊ฒŒ ๋ณด์ธ๋‹ค.

 

๋˜ํ•œ element ํƒญ์—์„œ ํ™•์ธํ•˜๋ฉด ํ™”๋ฉด์— ๋ณด์ด๋Š” ์—ด๊ณผ ๊ทธ ์œ„์•„๋ž˜ 3์ค„ ์ •๋„๋งŒ ์ถ”๊ฐ€๋กœ DOM์— ์กด์žฌํ•˜๋Š” ๊ฒƒ์„ ํ™•์ธํ•  ์ˆ˜ ์žˆ๋‹ค.

์•„ ๋„ˆ๋ฌด ์•„๋ฆ„๋‹ค์›€...๐Ÿฅน

 


5. ๋ฐฐํฌ ํ›„ ํ…Œ์ŠคํŠธ

 

before/after

๋ฆฌํŒฉํ† ๋ง ์ด์ „๊ณผ๋Š” ํ™•์—ฐํžˆ ๋‹ฌ๋ผ์ง„ ๋ชจ์Šต์ด๋‹ค.

  • CPU ์‚ฌ์šฉ๋Ÿ‰ ๊ทธ๋ž˜ํ”„
    • ๋นจ๊ฐ„ ๋ง‰๋Œ€(50ms์ดˆ๊ณผ)๊ฐ€ ๋ณด์ด๋Š” ๋ถ€๋ถ„์ด ํ›จ์”ฌ ์ ์–ด์กŒ๋‹ค.
    • ๋…ธ๋ž€ ๋ง‰๋Œ€(JS ์‹คํ–‰)๋„ ๊ฑฐ์˜ ์—†์–ด์กŒ๋‹ค.
  • Network ์„น์…˜
    • ๋„คํŠธ์›Œํฌ ์š”์ฒญ ๊ฐ„๊ฒฉ์ด ์ผ์ •ํ•ด์กŒ๋‹ค.
      ํ™”๋ฉด ๋ฒ„๋ฒ…์ž„์ด ์ค„์–ด ์Šคํฌ๋กคํ•˜๋Š” ์ฆ‰์‹œ ๋ฐ์ดํ„ฐ๋ฅผ ๋ถˆ๋Ÿฌ์˜ค๊ณ  ๋‹ค์Œ ์š”์ฒญ์„ ๋ณด๋‚ผ ์ˆ˜ ์žˆ๊ฒŒ ๋˜์—ˆ๊ธฐ ๋•Œ๋ฌธ์ด๋‹ค.
  • Frames ์„น์„ 
    • ์ด์ „์—๋Š” ๋’ค๋กœ๊ฐˆ์ˆ˜๋ก ํ”„๋ ˆ์ž„ ๋ Œ๋”๋ง ์‹œ๊ฐ„์ด ๊ธธ์–ด์ ธ์„œ ๋งˆ์ง€๋ง‰์—๋Š” 7,534.1ms๊ฐ€ ๋‚˜์™”์—ˆ์œผ๋‚˜,
      ์ด์ œ๋Š” ํ™”๋ฉด์ด ๋ฉˆ์ถ”์ง€ ์•Š์•„ ๋งˆ์ง€๋ง‰ ํ”„๋ ˆ์ž„ ๋ Œ๋”๋ง ์‹œ๊ฐ„๋„ 13.2ms๋กœ ์ •์ƒ ๋ฒ”์œ„ ๋‚ด๋กœ ์ค„์—ˆ๋‹ค.

  • Animations ์„น์…˜
    • ๋ฆฌํŒฉํ† ๋ง ์ „๋ณด๋‹ค ์• ๋‹ˆ๋ฉ”์ด์…˜์ด ๋ฐœ์ƒํ–ˆ๋‹ค๋Š” ๋ณด๋ผ์ƒ‰ ๊ทธ๋ž˜ํ”„๊ฐ€ ๋นผ๊ณกํ•˜๊ฒŒ ๋ณด์ด๋Š”๋ฐ, ์ด ํŽ˜์ด์ง€์—์„œ์˜ ์• ๋‹ˆ๋ฉ”์ด์…˜์€
      ๋กœ๋”ฉ ์Šคํ”ผ๋„ˆ์ด๋‹ค. ์ฆ‰ ๋กœ๋”ฉ์ด ์ฆ‰๊ฐ์ฆ‰๊ฐ ์ผ์–ด๋‚˜๊ณ  ๋ฐ์ดํ„ฐ fetch๊ฐ€ ๋น ๋ฅด๊ฒŒ ๋ฐœ์ƒํ•˜์—ฌ ๋‹ค์Œ ๋กœ๋”ฉ๋„ ๋ฐ”๋กœ ์ง„ํ–‰๋˜์—ˆ๋‹ค๋Š” ๋œป
  • Main Thread ์„น์…˜
    • ๋นจ๊ฐ„ ์„ธ๋ชจ(Long Task)๊ฐ€ ๊ฑฐ์˜ ์‚ฌ๋ผ์กŒ์œผ๋ฉฐ, ๋…ธ๋ž€์ƒ‰ ๊ทธ๋ž˜ํ”„(JS Task)๊ฐ€ ์•„์˜ˆ ์‚ฌ๋ผ์กŒ๋‹ค. ๋ณด๋ผ์ƒ‰ ๊ทธ๋ž˜ํ”„ ๋‚ด์šฉ๋ฌผ๋„ ๋ฐ”๋€Œ์—ˆ๋‹ค.
    • ๋ณด๋ผ(anonymous, processAnnotations): DOM ๋…ธ๋“œ ๋Œ€๋Ÿ‰ ์ƒ์„ฑ/์‚ญ์ œ ๋น„์šฉ์ด๋‹ค. (๊ฐ€์ƒํ™”๋กœ ํ•ด๊ฒฐ)
      ์Šคํฌ๋กค๋งˆ๋‹ค ์ˆ˜๋ฐฑ ๊ฐœ ์•„์ดํ…œ์˜ ๋งˆ์šดํŠธ/์–ธ๋งˆ์šดํŠธ๊ฐ€ ๋ฐœ์ƒํ•˜๋ฉฐ ์ต๋ช…ํ•จ์ˆ˜ ํ˜ธ์ถœ๊ณผ ๋‚ด๋ถ€ ์ฒ˜๋ฆฌ๊ฐ€ ์Œ“์ธ ๋ชจ์Šต์ด๋‹ค.
    • ๋…ธ๋ž‘(Timer fired, Run microtasks, Function call)
      Timer fired: virtual ์ ์šฉ์œผ๋กœ ๋‚ด๋ถ€์ ์œผ๋กœ ์Šคํฌ๋กค์„ ๊ฐ์ง€ํ•ด์„œ ํ•„์š”ํ•  ๋•Œ๋งŒ virtualRows ์—…๋ฐ์ดํŠธํ•˜๋„๋ก ํ•จ
      Run microtasks: onLoadMore ํ˜ธ์ถœ ๊ฐ€๋“œ๋กœ ๋ถˆํ•„์š”ํ•œ ํ˜ธ์ถœ ๋ฌธ์ œ ํ•ด๊ฒฐ
      Function call: onLoadMore ํ˜ธ์ถœ ๊ฐ€๋“œ๋กœ ๋ถˆํ•„์š”ํ•œ ํ˜ธ์ถœ ๋ฌธ์ œ ํ•ด๊ฒฐ

 

ํ˜„์žฌ ๋‚จ์•„์žˆ๋Š” background-color, opacity ๋ธ”๋ก์€ CSS ์• ๋‹ˆ๋ฉ”์ด์…˜ ์ฒ˜๋ฆฌ ๋ธ”๋ก์ด๋‹ค.

์ด๊ฒƒ๋„ main thread๊ฐ€ ์•„๋‹Œ compositor thread๋กœ ๋„˜๊ฒจ์„œ ์ฒ˜๋ฆฌํ•  ์ˆ˜ ์žˆ๋‹ค๊ณ  ํ•˜๋Š”๋ฐ ์ด๋ถ€๋ถ„์€ ์ถ”๊ฐ€๋กœ ์•Œ์•„๋ด์•ผ๊ฒ ๋‹ค.

 


6. ์ •๋ฆฌ

๋ฌธ์ œ ์ƒํ™ฉ

- ๋ฌดํ•œ์Šคํฌ๋กค ๊ฐ€์ƒํ™” ๋ฐ ์ตœ์ ํ™” ๋ฏธ์ ์šฉ์œผ๋กœ ์ธํ•œ ์„ฑ๋Šฅ ์ €ํ•˜ ๋ฌธ์ œ

- DOM์— ๋…ธ๋“œ๊ฐ€ ์ ์  ์Œ“์—ฌ์„œ ์„ฑ๋Šฅ ์ €ํ•˜์œจ์ด ์ ์  ๋†’์•„์ง

 

๋ฌธ์ œ ํŒŒ์•…

- ๋ฐฐํฌ๋ณธ ๊ฐœ๋ฐœ์ž ๋„๊ตฌ์˜ performanceํƒญ์œผ๋กœ ํŽ˜์ด์ง€ ๋…นํ™” ํ›„ ๊ทธ๋ž˜ํ”„ ์ฒดํฌ

    → CPU ๊ทธ๋ž˜ํ”„ ์ฒดํฌ: ์žฆ์€ Long Task ๋ฐ JS ์‹คํ–‰ ์—ฌ๋ถ€ ํŒŒ์•…

    → Network ์„น์…˜: ์ ์  ๊ธธ์–ด์ง€๋Š” ์š”์ฒญ ๊ฐ„๊ฒฉ

    → Frames ์„น์…˜: ์ ์  ๊ธธ์–ด์ง€๋Š” ๋ Œ๋”๋ง ์‹œ๊ฐ„(ms)

    → Animations ์„น์…˜: ์ ์  ๊ฐ„๊ฒฉ์ด ๋ฒŒ์–ด์ง€๋Š” Animation(ํŽ˜์ด์ง€์—์„œ์˜ ์• ๋‹ˆ๋ฉ”์ด์…˜์€ ๋กœ๋”ฉ ์ค‘ ๋ณด์—ฌ์ฃผ๋Š” Spinner์ •๋„)

    → Main Thread ์„น์…˜: Long Task ๋ฐ ๋ฐœ์ƒํ•˜๋Š” JS Task ํ™•์ธ (Timer fired, Run microtasks, Runction call)

- ๋กœ์ปฌ์—์„œ performance.now()๋กœ ์š”์ฒญ ์‹œ์ž‘ ์‹œ๊ฐ„ ๋ฐ ์‘๋‹ต ๋ฐ›์€ ์งํ›„ ์‹œ๊ฐ์„ ms ๋‹จ์œ„๋กœ ์ฒดํฌํ•˜์—ฌ ๋ ˆ์ดํ„ด์‹œ ์‹œ๊ฐ„ ๊ธฐ๋ก

 

ํ•ด๊ฒฐ1: ๊ฐ€์ƒํ™” ์ ์šฉ

- tanstack/react-virtual ์‚ฌ์šฉํ•ด์„œ ๊ฐ€์ƒํ™” ์ ์šฉํ•˜์—ฌ viewport์— ๋ณด์ด๋Š” row๋งŒ DOM์— ๋ Œ๋”๋ง

- grid ํ˜•ํƒœ๊ฐ€ ๊นจ์ง€๋Š” ๋ฌธ์ œ๊ฐ€ ์ƒ๊ฒจ, ์ •ํ•ด์ง„ ์—ด์˜ rows ๋‹จ์œ„๋กœ height ์ง€์ • ๋ฐ ๊ฐ€์ƒํ™” ์ ์šฉ

 

ํ•ด๊ฒฐ2: ์ตœ์ ํ™”

- useMemo๋กœ items/columns๊ฐ€ ๋ฐ”๋€” ๋•Œ๋งŒ row ๋ฐฐ์—ด ์žฌ๊ณ„์‚ฐ

 

- useCallback์œผ๋กœ ๋ Œ๋”๋งˆ๋‹ค ํ•จ์ˆ˜ ์žฌ์ƒ์„ฑ ๋ฐฉ์ง€

 

- isFetchingRef๋กœ ์ง„ํ–‰ ์ค‘์ธ ์š”์ฒญ์ด ์žˆ์„ ๊ฒฝ์šฐ ์ถ”๊ฐ€ fetch ์ฐจ๋‹จํ•˜์—ฌ ์ค‘๋ณต fetch ๋ฐฉ์ง€

 

- resize ์ด๋ฒคํŠธ ๋Œ€์‹  ResizeObserver๋กœ container width ๋ณ€ํ™”๋งŒ ๊ฐ์ง€

- setCurrentColumns์˜ ์ด์ „๊ฐ’์„ ๋น„๊ตํ•˜์—ฌ ์‹ค์ œ๋กœ columns์ด ๋ฐ”๋€” ๋•Œ๋งŒ state ์—…๋ฐ์ดํŠธ

 

 

 

 

 

 

 

 

 

 

์ฐธ๊ณ 

๋”๋ณด๊ธฐ

์ฐธ๊ณ ์ž๋ฃŒ๋งํฌ

๋ฐ˜์‘ํ˜•