블로그 목록

transform·will-change는 왜 부드러운가 — GPU 레이어 합성의 원리

2026-06-258 min read
브라우저렌더링성능will-changeGPU

transform·opacity로 애니메이션하면 부드럽다는 건 안다. 하지만 왜 부드러운가? "GPU라서"는 절반만 맞는 답이다. 진짜 이유는 메인 스레드 경합에서 벗어나기 때문이다.

1. 출발점 — 어느 단계를 건너뛰는가

브라우저가 화면을 그리는 순서는 정해져 있다. (URL부터 렌더까지에서 전체 흐름을 다뤘다.)

JS → Style → Layout → Paint → Composite
                ↑        ↑         ↑
             기하 계산  픽셀 채우기  레이어 합치기
  • Layout (reflow) — width·top·margin 등. 요소의 위치·크기를 다시 계산하고, 주변 형제·자식까지 연쇄로 번진다. 가장 비싸다.
  • Paint (repaint) — color·background·box-shadow 등. 픽셀을 다시 칠한다.
  • Composite — 이미 칠해진 레이어들을 합친다. 가장 싸다.

transform과 opacity는 Layout과 Paint를 둘 다 건너뛰고 Composite만 수행한다.

/* ❌ 매 프레임 Layout — 위치 재계산 */
.box { left: 100px; transition: left 0.3s; }

/* ✅ Composite만 — 레이어를 옮기기만 */
.box { transform: translateX(100px); transition: transform 0.3s; }

여기까지는 흔한 설명이다. 한 단계 더 들어가 보자.

2. 진짜 이유 — 메인 스레드 vs 컴포지터 스레드

같은 픽셀 변화라도 어느 스레드에서 처리되느냐가 부드러움을 가른다🧵 메인 스레드 — JS · 이벤트 핸들러와 경합 (바쁘면 끊김)width · top · colorStyleLayoutPaint칠한 픽셀을 레이어 텍스처로⚡ 컴포지터 스레드 + GPU — 메인 스레드와 독립, JS가 바빠도 60fpstransform · opacityStyle·Layout·Paint 건너뜀Composite레이어 합성GPU 레이어 텍스처will-change → 레이어를 미리 승격해 첫 프레임 jank 제거핵심은 GPU라서 빠른 게 아니라, 메인 스레드 경합에서 벗어나기 때문이다

Layout과 Paint는 메인 스레드에서 돈다. 그런데 메인 스레드는 JS 실행과 이벤트 처리도 같이 한다. 무거운 JS가 돌면 그동안 Layout·Paint가 밀리고, 애니메이션이 끊긴다(jank).

반면 Composite는 컴포지터 스레드 + GPU에서 처리된다. 메인 스레드가 JS로 아무리 바빠도, transform 애니메이션은 별도 스레드에서 60fps를 유지한다.

그래서 transform이 빠른 진짜 이유는 "GPU라서"가 아니라 메인 스레드라는 병목에서 빠져나오기 때문이다. 단순 연산 속도의 문제가 아니라 경합의 문제다.

3. 레이어 합성 (Layer Composition)이란

브라우저는 특정 조건에서 요소를 별도 레이어(Compositor Layer) 로 분리한다. Photoshop 레이어처럼, 각 레이어는 독립된 텍스처(GPU 메모리상의 비트맵)를 가진다.

레이어로 분리되면:

  • 그 요소만 미리 한 번 칠해 텍스처로 들고 있는다.
  • 움직일 때는 텍스처를 GPU에서 이동·확대·투명도 조절만 한다 → 다시 칠할 필요가 없다.

이게 합성의 핵심이다. transform은 "이미 찍어둔 사진을 옮기는" 작업이라, 픽셀을 새로 칠하는 Paint가 일어나지 않는다.

4. 레이어 승격 트리거

요소가 별도 레이어로 분리되는 걸 레이어 승격(promotion) 이라 한다. 대표적 트리거:

  • will-change: transform / will-change: opacity
  • transform: translateZ(0), translate3d(0,0,0) (예전부터 쓰던 핵)
  • opacity 애니메이션이 진행 중일 때
  • position: fixed, <video>, <canvas>, CSS 필터 등

translateZ(0) 핵이 통하는 이유도 같다. 3D 변환은 GPU 합성이 필요하므로 브라우저가 그 요소를 레이어로 올려버린다.

5. will-change — 미리 승격시키는 힌트

레이어 승격에는 비용이 든다 — 텍스처 메모리 할당 + 한 번 칠하기. 이걸 애니메이션이 시작되는 순간에 하면 첫 프레임이 버벅인다.

will-change는 "이 속성이 곧 바뀔 거야"라고 브라우저에 미리 알리는 힌트다. 미리 주면 브라우저가 사전에 레이어를 준비해, 첫 프레임부터 매끄럽다.

.card {
  transition: transform 0.3s;
}
.card:hover {
  will-change: transform; /* hover 진입 시 미리 승격 */
  transform: scale(1.05);
}

핵심은 will-change가 무언가를 빠르게 만드는 속성이 아니라, 준비 시점을 앞당기는 힌트라는 점이다.

6. 남용하면 안 되는 이유

will-change의 함정은 "좋아 보이니 다 걸어두자"는 유혹이다. 구체적으로 왜 위험한가:

① 메모리 폭증. 각 레이어는 너비 × 높이 × 4byte 텍스처를 차지한다. 전체화면(1920×1080) 레이어 하나가 약 8MB다. 리스트 100개에 will-change를 걸면 수백 MB가 사라진다.

② 상시 적용 금지. CSS에 will-change를 항상 켜두면 브라우저가 레이어를 영구히 들고 있는다. 필요한 순간에만 켜고 끝나면 빼는 게 정석이다.

// 애니메이션 직전 켜고, 끝나면 제거
el.style.willChange = "transform";
el.addEventListener("transitionend", () => {
  el.style.willChange = "auto";
});

③ 레이어 폭발(Layer Explosion). 레이어가 너무 많으면 합성 단계 자체가 병목이 된다. GPU↔메인 스레드 간 레이어 트리 관리 오버헤드가 이득을 잡아먹는다.

그래서 MDN도 will-change를 "최후의 수단"으로 쓰라고 권한다. 성능 문제가 실제로 관측됐을 때, 그 요소에만, 가능하면 동적으로.

7. 한 단계 더 — transform도 승격돼야 보장된다

면접에서 깊이를 드러내고 싶다면 이 점을 덧붙이자.

transform을 쓴다고 무조건 Composite-only가 보장되는 건 아니다. 그 요소가 레이어로 승격된 상태여야 한다. 승격되지 않았다면, transform 변경도 결국 그 요소가 속한 레이어 전체의 Paint를 다시 유발할 수 있다.

그래서 지속적으로 애니메이션할 요소에는 will-change(또는 translateZ(0))로 명시적으로 레이어를 확보해 두는 게 안전하다 — 단, 6번의 비용을 감수하면서.

8. 정리

개념핵심
transform·opacityLayout·Paint 건너뛰고 Composite만
빠른 진짜 이유GPU라서가 아니라 메인 스레드 경합 회피
레이어 합성요소를 독립 텍스처로 분리해 다시 칠하지 않고 변형
will-change승격을 미리 시켜 첫 프레임 jank 제거 (힌트일 뿐)
남용 주의레이어당 메모리 ↑, 상시 적용 금지, 동적 토글이 정석

한 줄 정리 — transform·opacity는 비싼 단계(Layout·Paint)를 건너뛰고 컴포지터 스레드로 빠져나와 메인 스레드 경합을 피한다. will-change는 그 우회를 위한 레이어를 미리 준비시키는 힌트이며, 메모리 비용 때문에 필요한 순간에만 켜야 한다.