본문 바로가기
Frontend2026년 9월 18일9분 읽기

긴 목록 렌더링 최적화 — content-visibility와 가상 스크롤 중 무엇을 쓸까

YS
김영삼
조회 159
긴 목록 렌더링 최적화 — content-visibility와 가상 스크롤 중 무엇을 쓸까

항목이 수천 개인 목록을 그리면 느려진다. 그런데 "느리다"의 원인은 하나가 아니다. DOM 노드 생성 비용, 스타일 계산과 레이아웃 비용, 페인트 비용, 그리고 메모리. 어떤 비용이 지배적인지에 따라 해법이 달라진다.

content-visibility: auto는 화면 밖 요소의 렌더링 작업을 건너뛴다. 가상 스크롤은 화면 밖 요소를 DOM에서 아예 제거한다. 전자는 적용이 쉽고 후자는 효과가 크다.

나는 두 기법을 한동안 같은 것으로 취급했다. 실제로 측정해 보고 나서야 차이를 알았다. content-visibility를 적용해도 DOM 노드는 그대로 있어서 메모리는 줄지 않았고, 노드 수가 수만 개를 넘자 스타일 재계산에서 다시 느려졌다. 반대로 항목이 수백 개 수준이었다면 CSS 한 줄로 충분했을 문제에 가상 스크롤을 넣어 코드를 복잡하게 만든 적도 있다.

무엇이 비싼지 먼저 구분하기

비용언제 문제가 되나효과적인 해법
DOM 노드 생성초기 렌더에서 수천 개를 한 번에가상 스크롤, 점진 렌더
스타일 계산·레이아웃노드가 많고 복잡한 선택자content-visibility, contain, 선택자 단순화
페인트·합성큰 이미지, 그림자·필터 다수이미지 지연 로딩, 효과 축소
메모리노드 수만 개 이상, 모바일가상 스크롤
스크롤 중 끊김스크롤 이벤트 핸들러가 무거움패시브 리스너, 작업 분할
측정이 먼저 개발자 도구 성능 패널에서 Recalculate Style, Layout, Paint의 비중을 보면 어느 해법이 맞는지 바로 드러난다. 추측으로 가상 스크롤부터 넣으면 복잡도만 늘고 효과가 없을 수 있다.

content-visibility — CSS 한 줄로 얻는 것

.list-item {
  content-visibility: auto;
  contain-intrinsic-size: auto 120px;   /* 렌더 전 예상 높이 */
}

/* contain-intrinsic-size 를 빼면 스크롤바 길이가 요동친다.
   auto 키워드를 쓰면 한 번 렌더된 뒤 실제 크기를 기억한다. */

동작 원리는 단순하다. 화면에서 충분히 멀리 있는 요소의 스타일 계산·레이아웃·페인트를 건너뛴다. 스크롤해서 가까워지면 그때 렌더한다. DOM에는 그대로 있으므로 검색(Ctrl+F), 접근성 트리, 앵커 링크가 대체로 유지된다는 것이 가장 큰 장점이다.

장점
  • CSS 한 줄 — 기존 코드 변경 최소
  • 브라우저 내 검색이 동작
  • 접근성 도구가 항목을 인식
  • 스크롤 위치 복원이 자연스럽다
한계
  • DOM 노드는 그대로 — 메모리 절감 없음
  • 수만 개 노드에서는 여전히 느림
  • contain-intrinsic-size를 잘못 주면 스크롤이 튄다
  • 컨테인먼트로 인한 레이아웃 차이 주의

가상 스크롤 — 큰 효과, 큰 책임

가상 스크롤은 보이는 범위의 항목만 DOM에 유지하고 나머지는 제거한다. 수만~수십만 항목도 다룰 수 있지만, 직접 구현하면 챙길 것이 많다.

가상 스크롤에서 깨지기 쉬운 것들
브라우저 검색 — 화면 밖 항목은 DOM에 없으므로 Ctrl+F로 못 찾는다
접근성 — 스크린 리더에 전체 목록 크기를 알리려면 aria 속성으로 보완해야 한다
가변 높이 — 항목 높이가 다르면 위치 계산이 복잡해진다(측정 후 캐싱 필요)
스크롤 위치 복원 — 뒤로 가기 시 원래 위치로 돌아가는 처리
앵커 링크 — 특정 항목으로 바로 이동하는 기능은 별도 구현
인쇄 — 인쇄 시 전체가 나오지 않는다
선택 상태 — 체크박스 선택이 스크롤 후에도 유지되는가
// 가상 스크롤의 핵심 계산 (고정 높이 기준)
const ITEM_H = 120;
const OVERSCAN = 5;                     // 위아래 여유분

function visibleRange(scrollTop, viewportH, total) {
  const start = Math.max(0, Math.floor(scrollTop / ITEM_H) - OVERSCAN);
  const count = Math.ceil(viewportH / ITEM_H) + OVERSCAN * 2;
  const end = Math.min(total, start + count);
  return { start, end };
}

// 전체 높이를 유지해 스크롤바 길이를 맞춘다
// <div style="height: {total * ITEM_H}px; position: relative">
//   <div style="transform: translateY({start * ITEM_H}px)">
//     ...보이는 항목만...
//   </div>
// </div>

가변 높이를 다루려면 항목별 실제 높이를 측정해 누적 오프셋을 관리해야 한다. 여기서 복잡도가 급증하므로, 직접 구현보다 검증된 라이브러리를 쓰는 편이 대개 낫다.

선택 기준

항목 수권장이유
~200아무것도 안 해도 됨최적화 비용이 이득보다 크다
200~2,000content-visibilityCSS 한 줄로 충분한 경우가 많다
2,000~10,000content-visibility + 페이지네이션/무한 스크롤노드 수 자체를 줄인다
10,000 이상가상 스크롤DOM 노드 수가 근본 문제

현실적으로는 페이지네이션이나 점진 로딩이 가장 자주 정답이다. 사용자가 수만 개 항목을 스크롤해서 보는 경우는 드물다. 대개는 검색과 필터가 필요한 것이지, 전부 그려 주는 게 필요한 게 아니다.

함께 적용할 작은 개선들

  • 이미지 지연 로딩loading="lazy"decoding="async". 목록에서 효과가 크다.
  • 크기 지정 — 이미지에 width·height를 주면 레이아웃 이동(CLS)이 줄고 재계산이 준다.
  • 선택자 단순화 — 깊은 자손 선택자와 복잡한 :has()는 노드가 많을수록 비싸다.
  • CSS containment — 항목 단위로 contain: layout style을 주면 영향 범위가 격리된다.
  • 이벤트 위임 — 항목마다 리스너를 붙이지 말고 컨테이너에서 한 번만.
  • 애니메이션 제한 — 목록 항목의 전환 효과는 스크롤 중 프레임을 잡아먹는다.
/* 항목 단위 격리 + 지연 렌더 */
.list-item {
  contain: layout style;               /* 레이아웃·스타일 영향 격리 */
  content-visibility: auto;
  contain-intrinsic-size: auto 120px;
}

/* 이미지 */
.list-item img {
  width: 96px; height: 64px;           /* 명시적 크기 */
  object-fit: cover;
}

INP 관점에서 한 가지

목록이 길면 스크롤보다 상호작용 응답이 더 문제가 되는 경우가 있다. 항목을 클릭했을 때 상태가 바뀌며 전체 목록이 리렌더되면, 입력 지연이 길어진다. 상태를 항목 단위로 격리하고, 큰 리렌더는 startTransition 같은 수단으로 우선순위를 낮추는 것이 효과적이다.

자주 묻는 질문

content-visibility와 가상 스크롤의 차이는 무엇인가요?

content-visibility는 화면 밖 요소의 렌더링 작업을 건너뛰지만 DOM에는 그대로 남깁니다. 가상 스크롤은 화면 밖 요소를 DOM에서 제거합니다. 전자는 적용이 쉽고 검색·접근성이 유지되며, 후자는 효과가 크지만 구현 복잡도가 높습니다.

contain-intrinsic-size는 왜 필요한가요?

렌더되지 않은 요소의 크기를 브라우저가 알 수 없어 스크롤바 길이가 부정확해지기 때문입니다. 예상 높이를 지정하면 스크롤 동작이 안정되며, auto 키워드를 함께 쓰면 한 번 렌더된 뒤 실제 크기를 기억합니다.

항목이 몇 개부터 최적화가 필요한가요?

수백 개 수준에서는 대개 필요 없습니다. 수천 개부터 content-visibility가 효과를 보이고, 수만 개를 넘어가면 DOM 노드 수 자체가 문제가 되어 가상 스크롤을 검토하게 됩니다. 다만 먼저 성능 패널로 어떤 비용이 지배적인지 확인하세요.

가상 스크롤을 쓰면 브라우저 검색이 안 되나요?

화면 밖 항목이 DOM에 없으므로 Ctrl+F로 찾을 수 없습니다. 이것이 가상 스크롤의 대표적인 단점이며, 자체 검색 기능을 제공하거나 검색 시 해당 항목으로 스크롤하는 동작을 별도로 구현해 보완해야 합니다.

접근성은 어떻게 챙기나요?

가상 스크롤에서는 전체 항목 수와 현재 위치를 aria 속성으로 알려야 스크린 리더 사용자가 맥락을 잃지 않습니다. content-visibility는 접근성 트리를 대체로 유지하므로 이 부담이 작습니다.

목록이 느린데 원인을 어떻게 찾나요?

개발자 도구 성능 패널에서 스타일 재계산, 레이아웃, 페인트의 비중을 확인하세요. 스타일·레이아웃이 지배적이면 content-visibility와 containment가, 노드 생성과 메모리가 문제면 가상 스크롤이나 페이지네이션이 적합합니다.

댓글 0

아직 댓글이 없습니다.
Ctrl+Enter로 등록