해결된 질문
작성
·
15
0
안녕하세요~!
궁금한 점이 생겨 문의드립니다!
DOM + CSSOM > Render Tree를 만들기 때문에 CSS 배치에 따라 CRP에 영향을 주는 것까지는 이해했는데요!
script는 async 또는 defer 속성을 알맞게 적어주기만 하면 실행 순서만 보장이 되고 안되고의 차이이고 CRP에는 영향을 주진 않는건가요?
답변 2
0
안녕하세요, 정수지님 🙂
DOM과 CSSOM이 결합되어 렌더 트리가 만들어진 다음에야 실제 화면이 그려진다는 원리를 정확히 이해하고 계시네요. 이 개념 위에 script
태그의 동작 방식을 더 깊이 이해하면, 브라우저가 어떻게 렌더링을 멈추거나 이어가며, async
와 defer
속성이 실제로 CRP(Critical Rendering Path, 중요 렌더링 경로) 에 어떤 영향을 주는지를 완전히 체감하실 수 있습니다.
브라우저는 HTML 문서를 위에서 아래로 읽어가며 DOM(Document Object Model) 을 생성하고, 동시에 외부 CSS 파일을 요청해 CSSOM(CSS Object Model) 을 만듭니다.
이 두 구조가 모두 준비되어야 브라우저는 Render Tree를 만들 수 있습니다.
Render Tree가 완성되면 그때서야 Layout(요소의 위치 계산)과 Paint(픽셀 그리기) 단계를 거쳐 화면을 표시하게 됩니다.
이 일련의 과정 전체를 Critical Rendering Path라고 부릅니다.
여기서 “HTML을 읽는다”는 것은 사람의 눈으로 읽는 것이 아니라,
브라우저가 <div>
, <p>
같은 태그를 하나씩 해석해서 트리 구조로 쌓는 과정을 의미합니다.
그리고 CSS 파일은 이 트리 위에 “색, 크기, 위치” 정보를 입히는 설계도라고 생각하시면 됩니다.
여기서 한 가지 중요한 사실이 있습니다.
CSS 파일 또한 렌더링 경로를 차단(blocking) 할 수 있습니다.
브라우저는 <link rel="stylesheet">
를 만나면 CSS 파일을 다운로드하면서, JS 실행을 잠시 멈춥니다. 왜냐하면 자바스크립트가 getComputedStyle()
이나 DOM 변경을 통해 스타일에 영향을 줄 수 있기 때문입니다. 즉, CSS가 완전히 로드되고 CSSOM이 만들어질 때까지는 JS 실행이 연기될 수 있습니다.
여기서 “차단(blocking)”이란, 브라우저가 다른 일을 잠시 멈추고
“이거 먼저 끝내야지!” 하고 우선순위를 주는 상태를 말합니다.
예를 들어, 요리를 하다 말고 갑자기 물을 끓여야 해서 모든 일을 멈추는 것과 비슷합니다.
CSS 파일을 다 불러와야 브라우저가 다시 HTML을 계속 읽을 수 있기 때문입니다.
(정확히는, CSS는 렌더링을 차단하고, 동기 <script>
실행을 지연시킵니다.
HTML 파싱 자체를 즉시 멈추는 것은 아니지만,
곧바로 이어지는 스크립트 때문에 실제로는 파싱이 잠시 멈춘 것처럼 보입니다.)
이때 HTML 안에 <script>
태그가 등장하면,
브라우저는 “여기서 JavaScript를 실행해야 한다”고 인식합니다.
문제는 이 스크립트가 파서-블로킹(parser-blocking) 이라는 점입니다.
즉, 브라우저는 스크립트를 다운로드하고 실행하는 동안 HTML 파싱을 멈춥니다.
여기서 ‘파싱을 멈춘다’는 건, 브라우저가 HTML 문서를 더 이상 아래로 내려가지 않고
“이 스크립트를 먼저 실행할게요” 하고 일시 정지하는 상태입니다.
그래서 JS가 오래 걸리면, HTML의 나머지 부분(예: <body>
)이 늦게 그려지는 겁니다.
예를 들어 다음과 같은 코드가 있다고 해보겠습니다.
<!DOCTYPE html>
<html>
<head>
<link rel="stylesheet" href="style.css">
<script src="main.js"></script>
</head>
<body>
<h1>Hello, world!</h1>
<p>This text might not appear immediately.</p>
</body>
</html>
이 경우 브라우저는 main.js
를 다운로드하고 실행할 때까지 HTML 파싱을 중단합니다.main.js
가 네트워크에서 내려받는 데 시간이 오래 걸린다면, <h1>
과 <p>
는 그동안 화면에 그려지지 않습니다.
즉, 스크립트의 실행 순서뿐 아니라 CRP 자체가 지연되는 것이죠.
“네트워크에서 내려받는다”는 것은, 브라우저가 인터넷을 통해 main.js
파일을 서버에서 요청해서 받아오는 과정입니다.
만약 인터넷 속도가 느리면, 화면이 하얗게 멈춰 있는 시간이 길어질 수 있습니다.
이 문제를 해결하기 위해 등장한 속성이 async
와 defer
입니다.
async
를 붙이면 브라우저는 HTML을 파싱하면서 스크립트를 병렬로 다운로드합니다.
여기서 “병렬”이란 동시에 여러 일을 하는 것입니다.
즉, HTML을 계속 읽으면서 백그라운드에서는 JS 파일도 내려받습니다.
하지만 다운로드가 완료되는 순간, 파싱을 잠시 멈추고 스크립트를 즉시 실행합니다.
즉, 다운로드는 비동기(=따로따로 동시에)지만 실행은 즉시 일어나므로,
실행 시점마다 파서가 잠깐씩 중단될 수 있습니다.
또한 여러 개의 async
스크립트가 있을 경우,
도착한 순서대로 실행되므로 실행 순서가 보장되지 않습니다.
이 말은, 먼저 내려받은 파일이 먼저 실행된다는 뜻으로,
코드 사이에 의존 관계가 있으면 문제가 생길 수 있습니다.
반면 defer
는 다운로드는 비동기이지만 실행은 HTML 파싱이 끝난 직후에 이루어집니다.
즉, 모든 DOM 파싱이 완료된 다음, 문서의 순서대로 스크립트가 실행됩니다.
이렇게 되면 HTML 파싱을 막지 않기 때문에, 초기 렌더링 속도에 거의 영향을 주지 않습니다.
쉽게 말해 async
는 “먼저 도착하면 바로 실행”,defer
는 “HTML 다 읽고 나서 한꺼번에 실행”이라고 기억하시면 됩니다.
둘 다 HTML과 JS를 같이 다운로드하지만, 실행 시점이 다릅니다.
현대 자바스크립트에서는 type="module"
속성을 사용할 수도 있습니다.
모듈은 기본적으로 defer
와 같은 동작 방식을 취하므로,
파싱을 방해하지 않고 HTML이 모두 읽힌 뒤에 실행됩니다.
단, 모듈 간의 의존 관계에 따라 여러 파일이 병렬로 다운로드되며,
실행은 import 순서를 따릅니다.
이 경우 defer
속성을 함께 붙이더라도 무시됩니다.
(예외적으로, 모듈 스크립트에 async
속성을 함께 붙이면,
다운로드가 끝나는 즉시 실행되어 defer
와 달리 파싱 완료 전에 실행될 수도 있습니다.)
추가로 import()
문법을 사용하면 완전히 비동기적 실행이 가능하며,
이는 파서나 렌더링을 전혀 블로킹하지 않습니다.
즉, HTML과 상관없이 나중에 “필요할 때만” JS를 불러오는 방식입니다.
즉, async
와 defer
는 단순히 스크립트의 “실행 순서”만 바꾸는 속성이 아니라,
HTML 파싱과 렌더링의 타이밍 자체를 바꾸는 성능 조절 도구입니다.
실무에서 자주 쓰이는 시나리오를 하나 만들어 보겠습니다.
예제 상황:
전자상거래 페이지를 예로 들어보죠.
사용자가 index.html
을 처음 방문할 때, 메인 이미지가 빠르게 보여야 전환율이 높습니다.
하지만 개발자가 다음과 같이 스크립트를 작성했다고 가정해봅시다.
<!DOCTYPE html>
<html>
<head>
<title>My Shop</title>
<link rel="stylesheet" href="style.css">
<script src="analytics.js"></script>
<script src="main.js"></script>
</head>
<body>
<img src="hero.jpg" alt="Main Product">
<button>Buy Now</button>
</body>
</html>
이 코드는 analytics.js
와 main.js
를 순차적으로 블로킹 방식으로 실행하므로,
사용자가 페이지를 열면 메인 이미지가 한참 동안 보이지 않습니다.
“순차적 실행”이란, 첫 번째 스크립트를 완전히 끝내야 두 번째 스크립트가 실행된다는 뜻입니다.
즉, 한 번에 한 일만 하는 거죠.
그래서 JS가 길거나 네트워크가 느리면, 이미지조차 늦게 보이게 됩니다.
이를 수정하면 이렇게 됩니다.
<!DOCTYPE html>
<html>
<head>
<title>My Shop</title>
<link rel="stylesheet" href="style.css">
<script async src="analytics.js"></script>
<script defer src="main.js"></script>
</head>
<body>
<img src="hero.jpg" alt="Main Product">
<button>Buy Now</button>
</body>
</html>
이제 두 스크립트가 CRP에 미치는 영향은 완전히 달라집니다.
브라우저가 HTML을 읽으면서 동시에 CSS와 두 스크립트를 병렬로 다운로드합니다.
(즉, 세 가지 일이 동시에 일어납니다: HTML 읽기, CSS 받기, JS 받기)
analytics.js
는 async
이므로 도착 즉시 실행되지만,
DOM에 의존하지 않는 분석 코드라 페이지 구조에는 영향을 주지 않습니다.
main.js
는 defer
이므로, HTML 파싱이 모두 끝난 후에 실행됩니다.
DOM이 완전히 준비된 뒤 실행되므로 안전하며, 첫 페인트에도 영향을 주지 않습니다.
그 결과, 사용자는 메인 이미지와 버튼을 즉시 볼 수 있고,
동시에 백그라운드에서는 스크립트 로딩이 병렬로 진행됩니다.
이런 변화 하나로 첫 페인트(First Paint) 와 DOMContentLoaded 이벤트 시점이 훨씬 빨라집니다.
참고로, defer
스크립트는 DOM 파싱이 끝난 직후 실행되고 나서DOMContentLoaded
이벤트가 발생합니다.
반면 async
스크립트는 다운로드 완료 시점에 즉시 실행되므로,
어떤 것은 DOMContentLoaded
보다 먼저, 어떤 것은 나중에 실행될 수도 있습니다.
따라서 DOMContentLoaded 전후 실행 타이밍이 중요한 로직이라면 defer
가 훨씬 안정적입니다.
다음은 이해를 돕기 위한 간단한 비교 요약입니다.
기본 <script>
동기적 실행 방식으로, 파일을 바로 실행하며 순서를 보장하지만 HTML 파싱을 완전히 멈춥니다. 가장 느린 방식입니다.
async
비동기 다운로드 방식으로, HTML을 읽는 도중 JS를 동시에 내려받고, 다운로드가 끝나는 즉시 실행합니다. 실행 순서는 보장되지 않으며, HTML 파싱이 잠깐씩 멈출 수 있습니다. 광고나 통계 코드처럼 독립적인 코드에 적합합니다.
defer
비동기 다운로드 방식이지만, HTML이 전부 파싱된 이후에 문서 순서대로 실행됩니다. HTML 파싱을 멈추지 않기 때문에 렌더링 속도에 거의 영향을 주지 않습니다. 메인 코드에 적합합니다.
type="module"
모듈 시스템을 사용하는 최신 방식으로, 자동으로 defer
처럼 동작하며 여러 파일을 동시에 다운로드합니다. import한 순서대로 실행됩니다. 단, async
속성을 함께 주면 도착 즉시 실행될 수도 있습니다.
요약하자면, 기본 스크립트는 차단이 심하고 느리고,async
는 빠르지만 순서가 불안정하며,defer
와 type="module"
은 안정적이면서 렌더링을 방해하지 않습니다.
표에서 나왔던 “동기”는 한 번에 한 가지 일만 하는 것이고,
“비동기”는 여러 가지 일을 동시에 처리하는 것을 의미합니다.
“차단 없음”은 브라우저가 멈추지 않고 계속 HTML을 읽을 수 있다는 뜻입니다.
정리하자면, async
와 defer
는 단순한 순서 옵션이 아니라,
브라우저의 렌더링 경로를 최적화하는 속성입니다.async
는 다른 코드와 의존성이 없는 외부 스크립트(예: 광고, 분석, 채팅 위젯 등)에 적합하고,defer
나 type="module"
은 메인 애플리케이션 코드에 적합합니다.
이 원리를 이해하면,
“어떤 스크립트를 언제 실행해야 가장 빠르게 사용자에게 첫 화면을 보여줄 수 있을까?”
라는 질문에 직접 답을 낼 수 있게 됩니다.
마지막으로, Chrome DevTools의 Network 탭에서
실제 async
/defer
스크립트의 다운로드 타이밍과 실행 시점을 관찰할 수 있습니다.async
스크립트는 ‘Received’ 시점에 즉시 실행되며,defer
스크립트는 DOMContentLoaded
직전에 실행되는 것을 확인할 수 있습니다.
만약 정수지님께서 실제로 작성 중인 페이지의 <head>
구조를 보여주신다면,
어디에 어떤 속성을 적용하면 최적의 성능이 나올지
구체적인 리팩터링 예시까지 함께 도와드릴 수 있습니다.
감사합니다😊😊
0
안녕하세요, 인프런 AI 인턴입니다. 정수지님.
브라우저의 CRP(Critical Rendering Path) 과정을 이해하는 것은 웹 성능 최적화에 매우 중요한 요소입니다. 질문해주신 내용에 답변을 드리겠습니다.
먼저, 브라우저는 HTML 파일을 받아 DOM(Document Object Model)을 생성하고, CSS를 분석하여 CSSOM(CSS Object Model)을 만듭니다. 이 두 구조를 합쳐서 Render Tree를 구성합니다. 따라서 CSS의 위치와 로딩 순서는 DOM 생성에 영향을 미쳐 CRP에 영향을 줍니다.
JavaScript는 기본적으로 DOM이나 CSSOM의 생성 이후에 실행되는데, 일반 스크립트는 HTML 파싱을 막습니다. 이 때문에 자칫하면 성능에 영향을 줄 수 있습니다. 하지만, async
와 defer
를 사용하면 상황이 조금 달라집니다.
따라서 async
와 defer
는 스크립트가 CRP에 미치는 영향을 조정할 수 있는 중요한 도구입니다.
아래 링크에는 script 태그의 async
와 defer
가 전반적인 웹 성능에 어떻게 영향을 미치는지에 대해 좀 더 상세히 설명된 자료가 있습니다.
저는 질문자님의 이해를 돕고자 지식공유자님의 기존 답변을 참고하여 유사한 답변 링크를 추천해드리고 있습니다. 현재 베타 기능으로 답변이 만족스럽지 않을 수 있는 점 양해 부탁드립니다. 🙏 추가적으로 궁금한 점이 있으시면, 이어서 질문해 주세요. 곧 지식공유자께서 답변해 주실 것입니다.