Part 1. 언어와 웹의 본질

브라우저 렌더링 원리: Reflow와 Repaint를 이해해야 최적화가 보인다.

브라우저 렌더링 원리: Reflow와 Repaint를 이해해야 최적화가 보인다.

크리스가 야심 차게 만든 애니메이션 메뉴가 왠지 모르게 버벅거린다.

"분명 최신 맥북인데 왜 뚝뚝 끊기지?"

코드를 열어보니 메뉴를 열기 위해 자바스크립트로 width 값을 1px씩 늘리고 있었다. 크리스는 방금 브라우저에게 엄청난 과로를 시킨 셈이다.

리액트의 가상 DOM(Virtual DOM)이 아무리 효율적이라 해도, 결국 브라우저가 화면을 그리는 원리를 모르면 성능 최적화는 반쪽짜리가 된다. 오늘은 브라우저가 픽셀을 화면에 찍어내는 과정, 중요 렌더링 경로(Critical Rendering Path)를 파헤쳐 본다.

브라우저의 공장 라인: 렌더링 파이프라인

브라우저가 HTML 파일을 받아서 화면에 띄우기까지는 일련의 조립 라인을 거친다.

  • DOM 트리 생성: HTML을 파싱 하여 태그 간의 관계인 DOM Tree를 만든다.
  • CSSOM 트리 생성: CSS 파일을 파싱 하여 스타일 규칙인 CSSOM Tree를 만든다.
  • 렌더 트리(Render Tree) 결합: DOM과 CSSOM을 합쳐서 실제 화면에 그려질 요소들만 추려낸다. (display: none인 요소는 여기서 제외된다.)
  • 레이아웃(Layout) 💥: 각 요소가 화면의 어느 위치어떤 크기로 배치될지 계산한다. (이 과정이 바로 Reflow다.)
  • 페인트(Paint): 계산된 위치에 색을 칠하고, 텍스트를 쓰고, 이미지를 그린다. (Repaint)
  • 합성(Composite): 여러 개의 레이어로 그려진 요소들을 순서대로 차곡차곡 합쳐서 최종 화면을 만든다.
  • 성능 최적화의 핵심은 4번(Layout)과 5번(Paint) 과정을 얼마나 줄이느냐에 달려 있다.

    Reflow와 Repaint: 성능을 잡아먹는 주범

    1. Reflow (Layout): "다시 계산해!"

    가장 비용이 비싼 작업이다. 요소의 **기하학적인 정보(너비, 높이, 위치)**가 변경되면, 브라우저는 해당 요소뿐만 아니라 영향을 받는 자식, 부모, 심지어 문서 전체의 배치를 다시 계산해야 한다.

    마치 거실에 있는 소파 위치를 1cm 옮기기 위해, 나머지 가구들의 위치까지 다시 자로 재는 것과 같다.

    Reflow를 유발하는 속성들:

  • width, height, padding, margin, border
  • font-size, font-weight
  • position (relative, absolute 등)
  • 창 크기 조절 (Window Resizing)
  • 2. Repaint (Paint): "색칠만 다시 해!"

    Reflow보다는 저렴하지만 여전히 비용이 든다. 레이아웃(위치, 크기)은 변하지 않고, 눈에 보이는 스타일만 바뀔 때 발생한다.

    Repaint를 유발하는 속성들:

  • color, background-color
  • visibility (display: none은 Reflow 유발)
  • box-shadow
  • 중요: Reflow가 발생하면 무조건 Repaint도 함께 발생한다. 위치가 바뀌면 당연히 다시 그려야 하기 때문이다.

    최적화 전략: GPU를 활용하라 (Composite)

    그렇다면 애니메이션을 부드럽게(60fps) 만들려면 어떻게 해야 할까?

    정답은 Layout과 Paint 단계를 아예 건너뛰는 것이다.

    브라우저에는 합성(Composite) 단계만 거치는 속성들이 있다. 이들은 CPU가 아닌 GPU가 처리하여 매우 빠르다.

    ❌ 나쁜 예시: CPU 괴롭히기

    크리스가 작성했던 코드다. left 속성을 변경하면 매 프레임마다 Reflow가 발생한다.

    /* style.css */
    .box {
      position: absolute;
      left: 10px;
      transition: left 0.5s;
    }
    
    .box.move {
      /* Layout(Reflow) 발생! -> Paint 발생! -> 느림 */
      left: 100px; 
    }

    ✅ 좋은 예시: GPU에게 떠넘기기

    transformopacity는 레이아웃과 페인트 단계를 건너뛰고, 바로 합성(Composite) 단계로 넘어간다.

    /* style.css */
    .box {
      /* GPU 가속을 위해 별도의 레이어로 분리됨 */
      transform: translateX(10px);
      transition: transform 0.5s;
    }
    
    .box.move {
      /* Composite만 발생! -> 빠름 */
      transform: translateX(100px);
    }

    강제 동기 레이아웃 (Forced Synchronous Layout)

    자바스크립트 코드 한 줄이 성능을 망칠 수도 있다. 브라우저는 원래 렌더링을 최적화하기 위해 변경 사항을 모아서 한 번에 처리(Batch)한다. 하지만 우리가 특정 값을 읽으려고 하면, 브라우저는 정확한 값을 주기 위해 큐를 비우고 즉시 레이아웃을 계산해버린다.

    // layout-thrashing.js
    const box = document.getElementById('box');
    
    // 1. 스타일 쓰기 (Write)
    box.style.width = '100px';
    
    // 2. 스타일 읽기 (Read) -> 💥 강제 레이아웃 발생!
    // 브라우저: "잠깐, 너가 width를 바꿨으니까 정확한 clientWidth를 알려주려면 지금 당장 계산(Reflow)해야 해."
    console.log(box.clientWidth); 
    
    // 3. 다시 쓰기
    box.style.height = '100px';

    이를 방지하려면 읽기(Read)쓰기(Write)를 섞어 쓰지 말고, 읽기를 먼저 다 한 뒤에 쓰기를 하거나 requestAnimationFrame을 활용해야 한다.

    핵심 정리

  • Reflow (Layout): 요소의 크기나 위치가 바뀌면 발생한다. 가장 비싸다. (width, top, left)
  • Repaint (Paint): 색상이나 스타일만 바뀌면 발생한다. (color, background)
  • Composite: transformopacity는 레이아웃과 페인트를 건너뛰고 GPU를 사용한다. 애니메이션은 무조건 이것으로 처리한다.
  • 최적화 팁: 애니메이션을 만들 때는 개발자 도구의 'Performance' 탭이나 'Rendering' 탭에서 Paint Flashing을 켜고, 녹색 영역이 얼마나 다시 그려지는지 눈으로 확인하자.
  • 브라우저가 그림을 그리는 법을 이해했으니, 이제 네트워크 비용을 줄이는 법을 알아볼 차례다. 사용자가 한 번 받은 이미지를 또 받지 않게 하려면 어떻게 해야 할까?

    "HTTP 캐싱 완벽 가이드: Cache-Control, ETag로 네트워크 비용 줄이기" 편에서 계속된다.


    🔗 참고 링크

  • Critical Rendering Path
  • Reflow vs Repaint
  • CSS Triggers (속성별 렌더링 비용 확인)