탐색으로 돌아가기
Browser14 / 24 단계

Layout (Reflow)

위치·크기 계산

렌더 트리의 각 요소에 대해 정확한 좌표와 크기를 계산하는 단계(리플로우).

레이아웃(Layout), 또는 리플로우(Reflow)는 렌더 트리의 각 노드에 대해 뷰포트(viewport) 안에서의 정확한 위치와 크기를 기하학적으로 계산하는 단계로, 결과는 상대값(%, em, vw 등)까지 모두 픽셀 좌표로 확정된다. 계산의 기본 단위는 박스 모델(box model)로, 안쪽부터 content → padding → border → margin 순의 영역과 box-sizing(content-box/border-box)에 따라 크기가 결정된다. 각 요소는 자신이 속한 포매팅 컨텍스트(formatting context)—블록(BFC), 인라인(IFC), 플렉스, 그리드—의 규칙에 따라 배치되며, 이 컨텍스트가 자식들의 흐름·정렬·마진 병합 여부를 지배한다. 레이아웃은 렌더 트리 크기에 비례하는 비용이 큰 작업이라 브라우저는 변경된 부분만 다시 계산하는 증분 레이아웃(incremental/dirty-bit layout)으로 최적화한다. 하지만 JS가 offsetTop, getBoundingClientRect(), getComputedStyle() 등 레이아웃 의존 값을 스타일 변경 직후 읽으면 브라우저가 큐에 쌓인 변경을 즉시 반영하려 강제 동기 레이아웃(forced synchronous layout)을 유발하고, 이를 읽기/쓰기가 번갈아 반복되면 레이아웃 스래싱(layout thrashing)이 되어 프레임을 떨어뜨린다.

내부 구성

박스 모델 - Content
실제 콘텐츠(텍스트·이미지)가 차지하는 영역. width/height의 기준(box-sizing에 따라 달라짐)
박스 모델 - Padding
content와 border 사이의 안쪽 여백. 배경색이 칠해지는 영역에 포함
박스 모델 - Border
padding 바깥 테두리. 요소의 시각적 경계이자 크기 계산에 포함되는 두께
박스 모델 - Margin
border 바깥의 요소 간 여백. 세로 마진은 인접 시 병합(margin collapsing)됨
뷰포트 (Viewport)
레이아웃의 최상위 기준 좌표계이자 초기 포함 블록(initial containing block)
블록 포매팅 컨텍스트 (BFC)
블록 박스를 수직으로 쌓고 float 격리·마진 병합을 지배하는 독립 배치 영역
인라인 포매팅 컨텍스트 (IFC)
인라인 박스를 line box 안에 수평으로 배치하고 baseline 정렬을 처리
플렉스 포매팅 컨텍스트 (Flex FC)
main/cross 축 기준으로 자식을 1차원 정렬·분배(justify/align)
그리드 포매팅 컨텍스트 (Grid FC)
행·열 트랙으로 2차원 배치를 수행하는 컨텍스트
레이아웃(메인) 스레드
렌더 트리를 순회하며 기하 계산을 수행. 대체로 메인 스레드에서 동기 실행됨
증분 레이아웃 (Incremental / Dirty-bit Layout)
변경된(dirty) 노드와 그 조상만 재계산해 전체 리플로우를 피하는 최적화
강제 동기 레이아웃 (Forced Synchronous Layout)
offsetTop·getBoundingClientRect·getComputedStyle 등 읽기가 대기 중인 변경을 즉시 반영하도록 강제
레이아웃 스래싱 (Layout Thrashing)
한 프레임 안에서 읽기/쓰기가 교대로 반복되며 리플로우가 다중 발생하는 안티패턴

핵심 포인트

  • 레이아웃/리플로우는 각 노드의 위치·크기를 뷰포트 기준 픽셀로 확정하는 단계
  • 박스 모델: content → padding → border → margin, box-sizing이 크기 해석을 바꿈
  • 포매팅 컨텍스트가 배치 규칙 결정: BFC(블록), IFC(인라인), Flex, Grid
  • 리플로우 트리거: DOM 추가/삭제, 크기·폰트·위치 변경, 창 리사이즈, 콘텐츠 변경 등
  • 증분 레이아웃(dirty bit): 변경된 서브트리만 다시 계산해 비용 절감
  • 강제 동기 레이아웃: 레이아웃 의존 속성(offsetWidth, getBoundingClientRect 등)을 읽으면 즉시 재계산
  • 레이아웃 스래싱: 읽기↔쓰기 반복으로 한 프레임에 리플로우가 여러 번 → 읽기 batch로 해결(FastDOM 등)

심화

레이아웃 스래싱의 본질은 '읽기가 이전 쓰기를 무효화된 레이아웃 위에서 강제 계산'하게 만든다는 데 있다. 브라우저는 스타일 변경을 큐에 모아 프레임 끝에 한 번에 처리(batch)하려 하지만, JS가 중간에 offsetHeight 같은 값을 읽으면 '최신 값을 주려면 지금 계산해야 하므로' 동기 리플로우가 일어난다. for 루프 안에서 el.style.height를 쓰고 바로 el.offsetHeight를 읽는 코드는 매 반복마다 리플로우를 유발한다. 해결책은 모든 읽기를 먼저 몰아서 하고(캐싱) 그 다음 쓰기를 몰아 하는 read-then-write 분리이며, requestAnimationFrame으로 쓰기를 다음 프레임에 배치하거나 FastDOM 같은 라이브러리로 read/write 페이즈를 스케줄링한다. Chrome DevTools Performance 패널은 30ms를 넘는 강제 리플로우를 'Forced reflow' 경고로 표시한다. 심화로 알아둘 것: getBoundingClientRect()가 반환하는 좌표는 뷰포트 기준이고 스크롤에 영향을 받으며, 여러 요소의 사각형을 한 프레임에 읽어야 한다면 IntersectionObserver나 ResizeObserver로 대체하면 강제 동기 레이아웃 없이 비동기로 정보를 얻을 수 있다. 또한 현대 flexbox의 레이아웃 성능은 float와 대체로 비슷하며(단일 패스인 일반 블록 레이아웃이 오히려 더 빠를 수 있다) — 흔히 인용되는 벤치마크는 구형 flexbox(display:box)가 현대 flexbox(display:flex)보다 약 2.3배 느리다는 것이지 flexbox가 float보다 빠르다는 게 아니다 — contain: layout이나 content-visibility로 레이아웃 범위를 격리하면 서브트리 변경이 전체로 번지는 것을 막을 수 있다. 이런 격리·관측 API 활용이 대규모 앱의 INP(Interaction to Next Paint) 개선의 핵심이다.

쉽게 말하면 무대 위 배우들의 정확한 위치와 동선을 좌표로 정하는 리허설.

면접 예상 질문

#Layout#Reflow#뷰포트#성능