브라우저는 어떻게 화면을 그리는가?

2026-01-15

front

평소 개발을 하면서 "브라우저가 화면을 그리는 과정"에 대해 얼마나 깊게 이해하고 있었는지 되돌아보게 되었다. 대충 "그렇겠지" 하고 넘겨짚던 사실들을 CRP(Critical Rendering Path) 과정을 통해, 특히 CSR 관점에서 딥하게 파헤쳐 보고자 한다.

1. CSR이란 무엇인가?

CSR(Client Side Rendering)은 이름 그대로 클라이언트인 브라우저에서 렌더링이 일어나는 방식이다. 서버는 단순히 데이터가 거의 없는 HTML 껍데기와 자바스크립트 파일 링크만 던져주고 실제 화면을 구성하는 로직은 브라우저에 탑재된 자바스크립트가 담당한다.
notion image

CSR의 장점

  • 사용자 경험(UX): 첫 로딩 이후에는 페이지 전환이 매우 부드럽다. 서버에 매번 새로운 HTML을 요청할 필요가 없기 때문이다.
  • 서버 부하 감소: 렌더링의 책임을 클라이언트에 넘기기 때문에 서버는 API 응답에만 집중할 수 있다.

CSR의 단점

  • 초기 로딩 속도: 자바스크립트 번들 파일이 커질수록 사용자가 첫 화면을 보기까지 걸리는 시간이 길어진다.
  • SEO(검색 엔진 최적화) 문제: 서버에서 빈 HTML을 받기 때문에 검색 엔진이 사이트 내용을 파악하기 어렵다는 인식이 강하다.

2. CSR은 정말 SEO에 취약할까?

흔히 "CSR은 빈 HTML만 오기 때문에 크롤링 봇이 빈 화면만 보고 돌아간다"고들 한다. 하지만 과연 이게 100% 사실일까?
결론부터 말하면, 현대의 크롤링 봇은 생각보다 똑똑하다. 구글의 WRS(Web Rendering Service) 같은 시스템은 자바스크립트를 실행해 실제 화면을 렌더링한 뒤 내용을 수집한다.
그러나 문제는 비용과 시간이다. 크롤링 봇이 자바스크립트를 실행하는 과정은 일반 HTML을 읽는 것보다 훨씬 무겁고 오래 걸린다. 여기서 크롤 버짓(Crawl Budget) 개념이 등장한다. 구글봇이 특정 사이트에 할애하는 크롤링 자원은 무한하지 않다. 렌더링에 시간이 오래 걸리면 그만큼 크롤링할 수 있는 페이지 수가 줄어들고, 결과적으로 인덱싱이 누락되거나 순위에서 밀릴 가능성이 높아진다. 즉, "안 되는 건 아니지만, SSR에 비해 불리한 점이 명확하다"는 것이 팩트다.
notion image

3. 브라우저가 화면을 그리는 험난한 여정 (CRP)

이제 본론으로 들어가서, CSR 환경에서 브라우저가 빈 HTML을 받아 어떻게 화면을 채워나가는지 딥하게 살펴보자.

3-1. HTML 파싱과 블로킹 요소

브라우저가 HTML을 받으면 DOM 파서가 한 줄씩 읽으며 DOM(Document Object Model) 트리를 만든다.
  • CSS와 CSSOM: 파싱 중 link 태그를 만나 CSS 파일을 로드하면 CSSOM(CSS Object Model) 트리를 형성한다. CSSOM은 렌더링을 막는 렌더 블로킹(Render Blocking)을 유발한다. 스타일이 없는 상태에서 화면을 그리면 사용자에게 엉망인 화면을 보여주게 되기 때문이다. 단, DOM 파싱 자체를 멈추지는 않는다.
  • JavaScript와 파싱 블로킹: 파서가 script 태그를 만나면 자바스크립트를 실행하기 위해 DOM 파싱을 중단한다. 이를 파싱 블로킹(Parsing Blocking)이라 한다. 여기서 한 가지 더 알아야 할 점이 있다. 자바스크립트는 스타일을 조작할 수 있기 때문에, CSSOM 구축이 완료될 때까지 스크립트 실행을 기다린다. 즉, CSS 로딩이 지연되면 JS 실행도 덩달아 늦어지고, 결국 파싱 블로킹 시간이 길어지는 연쇄 효과가 발생할 수 있다.

3-2. 스크립트 실행의 전략: async와 defer

스크립트로 인한 파싱 중단을 제어하기 위해 asyncdefer 옵션을 사용한다.
  • async: 비동기적으로 스크립트를 다운로드하고, 다운로드가 완료되는 즉시 실행한다. 실행 시점에는 파싱이 중단된다. 주의할 점은 실행 순서가 보장되지 않는다는 것이다. 먼저 다운로드된 스크립트가 먼저 실행되기 때문에, 스크립트 간 의존성이 있다면 예상치 못한 버그가 발생할 수 있다.
  • defer: 파싱과 병렬로 다운로드하되, HTML 파싱이 모두 끝난 뒤 DOMContentLoaded 이벤트 직전에 순서대로 실행한다. CSR 프레임워크에서 가장 권장되는 방식이다.
  • type="module": 참고로 Vite, Webpack 같은 모던 번들러가 생성하는 type="module" 스크립트는 기본적으로 defer처럼 동작한다. 별도로 defer를 명시하지 않아도 파싱을 블로킹하지 않으니, 요즘 프로젝트에서는 이 부분을 크게 신경 쓰지 않아도 되는 이유이기도 하다.
  • 아무 옵션 없음: 동기적으로 실행하며, 다운로드와 실행이 끝날 때까지 파싱이 멈춘다.
notion image
이 과정에서 메인 파서가 멈춰있을 때도 프리로드 스캐너(Preload Scanner)라는 보조 파서가 뒤쪽을 훑으며 미리 로드할 자원을 찾아 다운로드를 시작하는 최적화가 일어난다.

3-3. 렌더 트리와 레이아웃 (리플로우)

DOM과 CSSOM이 합쳐지면 비로소 화면에 실제 나타날 요소들만 모은 렌더 트리(Render Tree)가 만들어진다.
notion image
  • display: none: 렌더 트리에 포함되지 않는다.
  • visibility: hidden: 보이지는 않지만 공간을 차지하므로 렌더 트리에 포함된다.
트리가 완성되면 각 요소의 정확한 크기와 위치를 계산하는 레이아웃(Layout) 단계를 거친다. 흔히 리플로우(Reflow)라고 부르는 과정이다.

3-4. 페인트와 컴포지트 (리페인트와 합성)

위치가 결정되었으니 이제 색을 칠하는 페인트(Paint) 단계다. 주의할 점은 레이아웃이 다시 계산되면 필연적으로 페인트도 다시 실행된다는 것이다.
마지막으로 브라우저는 성능 최적화를 위해 전체 화면을 한꺼번에 그리지 않고 레이어(Layer)로 나눠서 렌더링한 뒤 이를 합친다. 이 과정을 컴포지트(Composite/합성)라고 한다. 똑똑한 브라우저는 변경 사항이 있는 레이어만 다시 계산하여 효율을 극대화한다.
그렇다면 어떤 요소가 별도의 레이어로 분리될까? transform, opacity, will-change 같은 CSS 속성을 사용하면 해당 요소는 자체 레이어를 갖게 된다. 이렇게 분리된 레이어는 GPU에서 처리되기 때문에, 애니메이션 시 레이아웃과 페인트를 건너뛰고 컴포지트만 다시 수행할 수 있다. 부드러운 60fps 애니메이션을 구현할 때 transform을 권장하는 이유가 바로 여기에 있다.
notion image
개발자 도구로 레이어로 나뉜 실제 모습을 확인할 수 있다.
개발자 도구로 레이어로 나뉜 실제 모습을 확인할 수 있다.

결론

CSR 기반의 앱은 초기에 빈 화면을 보여주지만, 그 이면에서는 브라우저가 파싱 블로킹을 최소화하고 프리로드 스캐너를 돌리며 렌더 트리를 구성하기 위해 고군분투하고 있다.
CRP 과정을 이해하고 나니, 우리가 무심코 넣은 script 태그 하나나 불필요한 레이아웃 유발 코드가 사용자 경험에 얼마나 큰 영향을 주는지 다시금 깨닫게 되었다. 단순히 "라이브러리가 해주겠지"라고 생각하기보다, 브라우저의 동작 원리를 이해하고 그에 최적화된 코드를 짜는 것이 프론트엔드 개발자의 본질 아닐까?
 
 
web.devweb.dev브라우저 미리 로드 스캐너와 싸우지 마세요.  |  Articles  |  web.dev
NAVER D2