대용량 데이터 처리 시나리오 테스트
백오피스 서비스에서는 생각보다 데이터 양이 엄청 많은 상황을 마주하게 됩니다.
서울우유 백오피스에서 진행했던 전자세금계산서 진위 검증 업무처럼, 한 번에 수천~수만 건의 데이터가 쏟아질 수 있습니다.
이럴 땐 화면이 버벅이거나 로딩이 오래 걸리는 경우가 있는데,
"만약 기업에서 이런 대용량을 다뤄야 한다면?" 이라는 가정에서 시작했습니다.
무한 스크롤만으로는 부족했던 이유
처음에는 "그냥 무한 스크롤 쓰면 되지 않나?" 생각했습니다
무한 스크롤은 한 번에 모든 데이터를 불러오지 않고, 스크롤이 끝에 닿을 때 다음 데이터를 가져오는 방식이라 네트워크 요청 부담은 줄여줍니다.
하지만 렌더링되는 DOM의 개수는 계속 누적되기 때문에, 페이지를 오래 내리다 보면 브라우저가 점점 무거워집니다.
특히 1만 건 같은 대용량일 때는 TBT(Total Blocking Time)가 확 치솟아, 사용자 경험이 크게 떨어집니다.
그래서 도입한 것이 가상 스크롤(Virtual Scrolling)입니다.
가상 스크롤은 실제로는 화면에 보이는 데이터만 DOM에 렌더링하고, 나머지는 메모리에만 보관합니다.
결과적으로 DOM 개수는 항상 일정하게 유지되고, 스크롤 위치에 따라 필요한 항목만 즉시 그려주니 성능이 훨씬 안정적이게됩니다
"기업 환경에서 대용량 데이터를 처리해야 하는 상황"을 가정하고,
MSW로 서버를 가짜로 만들어 1만 건 데이터까지 응답하도록 구성했고,
이 데이터를 바탕으로 "전체 렌더"와 "가상 스크롤 + 무한 스크롤" 성능을 직접 비교해봤습니다.
무한스크롤, 가상스크롤의 차이
무한 스크롤
- 화면을 아래로 내릴 때마다 서버나 API에서 새로운 데이터를 조금씩 불러오는 방식
- 덕분에 한 번에 모든 데이터를 가져오지 않아도 되기 때문에 네트워크 요청 면에서는 효율적
- 하지만 문제는 불러온 데이터가 계속 DOM에 쌓인다는 점
- 예를 들어 1만 건의 데이터를 전부 불러오면, 브라우저는 1만 개의 DOM 노드를 모두 렌더링하고 유지해야 합니다. 데이터가 많아질수록 렌더링과 레이아웃 계산에 걸리는 시간이 점점 늘어나고, 결국 브라우저 성능이 떨어집니다.
가상 스크롤
- 화면에 보이는 항목만 DOM에 렌더링하고, 나머지는 메모리에만 보관하는 방식
- 예를 들어 현재 화면에 20개의 아이템만 보여주고, 스크롤하면 기존 DOM을 재활용해 새로운 데이터로 교체합니다
- 이 방식은 데이터가 1만 건이든 10만 건이든 브라우저가 처리해야 하는 DOM 개수가 일정하므로, 렌더링 성능이 안정적입니다.
문제 분석 – 전체 렌더링의 병목
처음엔 단순히 limit로 데이터 개수를 줄이는 방식도 고려했지만,
검증 특성상 한 번에 모든 데이터에 접근해야 하는 경우가 많아 제한을 걸기 어려웠습니다.
즉, 불필요하게 보이지 않는 데이터까지 전부 DOM에 올리는 것이 성능 병목의 핵심이었습니다.
해결 방안 – 가상 스크롤 + 무한 스크롤 도입
"보이는 영역만 렌더링하고, 스크롤할 때 필요한 데이터만 가져오자."
구현 전략
1. MSW로 대용량 데이터 환경구축
실제 프로덕션 환경에서 대용량 데이터를 테스트하기는 어렵기 때문에, MSW(Mock Service Worker)를 활용해 가상의 대용량 데이터 환경을 구축했습니다.
// fixtures/cs.ts
export interface CsTaxItem {
ntsTaxId: number;
title: string;
taxDate: string; // YYYY-MM-DD
name: string; // 대리점/센터명
}
const agencies = [
'강남 대리점', '서초 대리점', '마포 대리점', // ... 15개 대리점
];
const titlesApprove = [
'세금계산서(승인)', '세금계산서(추가 승인)',
'세금계산서(정산)', '세금계산서(확정)'
];
export const makeApproveList = (count = 10000): CsTaxItem[] =>
Array.from({ length: count }, (_, i) => ({
ntsTaxId: 92001 + i,
title: `7월 ${titlesApprove[i % titlesApprove.length]}`,
taxDate: `2025-07-${pad2((i % 28) + 1)}`,
name: agencies[i % agencies.length]
}));
가상 스크롤 라이브러리 선택: react-virtuoso
선택 이유:
- 무한 스크롤 지원: endReached 콜백으로 자연스럽게 무한 스크롤 구현 가능
- 동적 높이 지원: 아이템 높이가 다를 때도 자동으로 처리
- TypeScript 친화적: 타입 안정성이 뛰어남
- 메모리 효율적: 화면에 보이는 요소 + 여유분(overscan)만 DOM에 유지
react-virtuoso와 무한 스크롤 연동
<Virtuoso
style={{ height: LIST_HEIGHT, width: LIST_WIDTH }}
data={data} // 누적된 전체 데이터
endReached={() => hasMore && !isLoading && fetchNextPage()} // 스크롤 끝에 도달 시
overscan={200} // 화면 밖 여유분 렌더링 개수
components={{
Footer: () => (
<div className="h-[42px] flex items-center justify-center text-gray-500 text-sm">
{hasMore ? '불러오는 중…' : '더 이상 데이터가 없습니다'}
</div>
)
}}
itemContent={(index, item) => (
<div className="mx-[8px] flex h-[42px] w-[932px] items-center rounded-[12px] hover:bg-gray-100">
<div className="w-[350px] pl-5 text-sm font-medium text-gray-700 truncate">
{item.title}
</div>
<div className="w-[170px] pl-5 text-sm font-medium text-gray-700 tabular-nums">
{item.taxDate}
</div>
<div className="w-[120px] pl-5 text-sm font-medium text-gray-700 truncate">
{item.name}
</div>
</div>
)}
/>
react-virtuoso 핵심 옵션:
- data: 전체 데이터 배열을 넘기면 자동으로 가상 스크롤 처리
- endReached: 스크롤이 끝에 도달했을 때 호출되는 콜백
- overscan: 화면 밖에서 미리 렌더링할 아이템 개수 (성능 튜닝 가능)
- itemContent: 각 아이템을 렌더링하는 함수 (index와 item을 받음)
성과
개선 전: 전체 렌더
- 초록색(페인트)와 보라색(렌더) 블록이 넓게 이어져 있음.
- 한 번에 모든 DOM 노드를 그리다 보니 긴 작업(>50ms)이 연속적으로 발생.
- 이 구간 동안 메인 스레드가 차단되면서 사용자 입력 반응성 저하.
- 결과적으로 TBT가 크게 증가.
개선 후 :가상 스크롤 + 무한 스크롤
노란색(스크립트 실행), 보라색(렌더링), 초록색(페인팅) 구간이 작게 끊어져서 반복됨
-> 한 번에 큰 렌더 작업을 하지 않고, 스크롤 시 필요한 부분만 처리 → 메인 스레드 점유 시간이 짧음.
CPU 부하가 순간적으로만 발생하고 대부분 idle 상태 → TBT(Total Blocking Time)가 낮아짐.
- 가상 스크롤 + 무한 스크롤: DOM에 필요한 요소만 동적으로 렌더링 → 짧고 빈번한 작업 → 반응성 유지, TBT↓
- 전체 렌더: DOM 전체를 한 번에 그림 → 긴 작업 → 초기 렌더링 시 입력 지연, TBT↑
- FCP, LCP가 거의 동일한 건 첫 화면에 보여지는 데이터 범위가 동일하기 때문. 차이는 렌더링 이후 스크롤 시점에 나타남.
- 기존(전체 렌더링): 페이지 로드 초기에 1.75초 동안 브라우저가 멈춘 듯한 상태 → 클릭/스크롤 반응 지연
- 개선(가상 스크롤+무한 스크롤): 초기 로드 시 차단 시간이 0 → 페이지가 즉시 반응 가능
'⛲ 프로젝트' 카테고리의 다른 글
이벤트 페이지 성능최적화 (LCP 37-> 2.5, CLS 0.219→0.006) (4) | 2025.08.18 |
---|---|
카카오톡에서 앱 설치 유도하기: PWA 외부 브라우저 리다이렉션 전략 (5) | 2025.08.06 |
Home 진입 40초 → 2.5초, LCP를 93% 줄인 Next.js 최적화 (3) | 2025.08.05 |
이미지 업로드부터 OCR 자동화까지, 세금계산서 등록 프로세스 (1) | 2025.07.21 |
(husky) Husky와 Lint-staged를 이용한 Lint 자동화 (1) | 2024.11.23 |