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

useEffect ์์กด์ฑ ํด๊ฒฐ๋ก fetch ๋ฐ๋ณต ๋ฌธ์ ํด๊ฒฐ
์ปจํ ์ด๋ ๋์ด ํ์คํ๊ฒ ์ก์์ ์คํฌ๋กค ์ fetch ๋๋๋ก ํด๊ฒฐ
๋ชฉ์ฐจ
1. ๊ธฐ์กด InfiniteScroll ์ํ
2. ๋ฌธ์ 1) useEffect ๋ด๋ถ์์์ ๋ฐ๋ณต ํธ์ถ ๋ฌธ์
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(...)
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. ๋ฐฐํฌ ํ ํ ์คํธ


๋ฆฌํฉํ ๋ง ์ด์ ๊ณผ๋ ํ์ฐํ ๋ฌ๋ผ์ง ๋ชจ์ต์ด๋ค.
- CPU ์ฌ์ฉ๋ ๊ทธ๋ํ
- ๋นจ๊ฐ ๋ง๋(50ms์ด๊ณผ)๊ฐ ๋ณด์ด๋ ๋ถ๋ถ์ด ํจ์ฌ ์ ์ด์ก๋ค.
- ๋ ธ๋ ๋ง๋(JS ์คํ)๋ ๊ฑฐ์ ์์ด์ก๋ค.
- Network ์น์
- ๋คํธ์ํฌ ์์ฒญ ๊ฐ๊ฒฉ์ด ์ผ์ ํด์ก๋ค.
ํ๋ฉด ๋ฒ๋ฒ ์์ด ์ค์ด ์คํฌ๋กคํ๋ ์ฆ์ ๋ฐ์ดํฐ๋ฅผ ๋ถ๋ฌ์ค๊ณ ๋ค์ ์์ฒญ์ ๋ณด๋ผ ์ ์๊ฒ ๋์๊ธฐ ๋๋ฌธ์ด๋ค.
- ๋คํธ์ํฌ ์์ฒญ ๊ฐ๊ฒฉ์ด ์ผ์ ํด์ก๋ค.
- Frames ์น์
- ์ด์ ์๋ ๋ค๋ก๊ฐ์๋ก ํ๋ ์ ๋ ๋๋ง ์๊ฐ์ด ๊ธธ์ด์ ธ์ ๋ง์ง๋ง์๋ 7,534.1ms๊ฐ ๋์์์ผ๋,
์ด์ ๋ ํ๋ฉด์ด ๋ฉ์ถ์ง ์์ ๋ง์ง๋ง ํ๋ ์ ๋ ๋๋ง ์๊ฐ๋ 13.2ms๋ก ์ ์ ๋ฒ์ ๋ด๋ก ์ค์๋ค.
- ์ด์ ์๋ ๋ค๋ก๊ฐ์๋ก ํ๋ ์ ๋ ๋๋ง ์๊ฐ์ด ๊ธธ์ด์ ธ์ ๋ง์ง๋ง์๋ 7,534.1ms๊ฐ ๋์์์ผ๋,

- 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 ์ ๋ฐ์ดํธ

์ฐธ๊ณ
์ฐธ๊ณ ์๋ฃ๋งํฌ
'Trouble Shooting & Issues > Linkiving' ์นดํ ๊ณ ๋ฆฌ์ ๋ค๋ฅธ ๊ธ
| hoverํ ๋ ๋ฑ์ฅํ๋ ๋ฒํผ์ ์ฐ๊ทธ๋ฌ์ง ๋ฌธ์ (feat.flex-shrink ๋ก์ง) (0) | 2026.04.11 |
|---|---|
| ๋ฌดํ์คํฌ๋กค ์ฑ๋ฅ ์ธก์ /๊ฐ์ ํ๊ธฐ(1) (0) | 2026.04.03 |
| 401 Unauthorization ์๋ฌ์ ์์ธ ์ฐพ๊ธฐ (1) | 2026.03.12 |
| Docker ์ค์นํด์ ๋ฐฑ์๋ ๋ก์ปฌ๋ก ๋ก๊ทธ์ธ ํ ์คํธํ๊ธฐ [Linkiving] (0) | 2026.02.27 |
| safeFetch ์ ํธํจ์ ๋ง๋ค๊ธฐ(2): safeFetch ํจ์ ์์ฑ (0) | 2025.12.12 |
GitHub