일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | ||||||
2 | 3 | 4 | 5 | 6 | 7 | 8 |
9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 | 17 | 18 | 19 | 20 | 21 | 22 |
23 | 24 | 25 | 26 | 27 | 28 | 29 |
30 | 31 |
- python
- HTTP
- 타입스크립트 올인원
- 리액트
- 알고리즘
- 코어 자바스크립트
- 타임어택
- codestates
- 리덕스
- 제로초
- 코드스테이츠
- 렛츠기릿 자바스크립트
- 백준
- js
- 타입스크립트
- javascript
- 파이썬
- 4주 프로젝트
- 자바스크립트
- 프로그래머스
- 토익
- 회고
- LeetCode
- 정재남
- til
- 2주 프로젝트
- programmers
- 리트코드
- SQL 고득점 Kit
- 손에 익히며 배우는 네트워크 첫걸음
- Today
- Total
Jerry
#17. HTML 렌더링 중에 JavaScript가 실행되면 렌더링이 멈추는 이유는? 본문
HTML을 렌더링하는 동안 JavaScript가 <script> 태그에서 동기적으로 실행되면 브라우저의 렌더링이 일시 중단됩니다.
이는 브라우저의 렌더링 엔진과 JavaScript 엔진이 단일 스레드에서 동작하기 때문입니다.
렌더링 도중 JS가 실행되면,
JS 코드 안에서 document.write()처럼 DOM 구조를 변경할 수 있는 가능성이 있기 때문에,
렌더링 엔진은 안전하게 JS 실행이 끝날 때까지 기다려야 합니다.
즉, HTML 파싱 → JS 실행 → 다시 HTML 파싱 순으로 동작합니다
<p>문단 1</p>
<script>
console.log("실행 중...");
</script>
<p>문단 2</p>
- 이 경우 <script> 실행 중에는 <p>문단 2</p>는 렌더링되지 않습니다.
- 이유는 <script> 내부에서 DOM을 조작할 수도 있으므로, 브라우저는 JS 실행이 끝난 후 나머지 HTML 파싱을 계속합니다.
따라서, 따라서 렌더링 성능을 위해 <script> 태그를 defer 또는 async 속성과 함께 사용하는 것이 권장됩니다.
defer와 async는 HTML 문서에 포함된 JS를 비동적으로 로드하기 위한 속성이며,
브라우저가 스크립트를 다운로드하고 실행하는 시점을 제어합니다.
defer와 async의 차이는?
- defer: JS는 HTML 파싱이 끝난 후 순서대로 실행 (순서 보장 O)
- async: JS는 HTML 파싱과 병렬로(동시에) 다운로드 -> 다운로드 완료 즉시 실행 (순서 보장 X)
<!-- defer -->
<script src="script1.js" defer></script>
<script src="script2.js" defer></script>
<!-- async -->
<script src="script1.js" async></script>
<script src="script2.js" async></script>
- defer: HTML 파싱이 끝난 후, 순서대로 실행 → 항상 script1.js → script2.js.
- async: 다운로드가 끝난 순서대로 실행 → script2.js가 먼저 실행될 수도 있음.
속성 | HTML 파싱 중 JS 다운로드 | HTML 파싱 중단 여부 | JS 실행 시점 | 실행 순서 보장 |
없음 | O | O (동기 실행) | 파싱 중단 후 즉시 | O |
async | O | X | 다운로드 완료 즉시 | X |
defer | O | X | HTML 파싱 완료 후 | O |
렌더링 차단을 피하려면 어떻게 해야 하나요?
렌더링 차단(Render-blocking)은 HTML 파싱 중 JS나 CSS의 로딩/실행이 파싱을 멈추는 현상입니다.
이를 피하기 위해 JS와 CSS의 로딩을 비동기화하거나, 필요한 시점에만 불러오도록 최적화해야 합니다
* React 환경에서는 React.lazy()와 Suspense를 활용한 컴포넌트 단위 코드 분할이 렌더링 차단을 피하는 데 유효합니다.
다만 Next.js 같은 프레임워크는 페이지 단위 코드 분할이 기본 적용되어 있으며, 추가적으로 next/dynamic을 통해 필요한 컴포넌트만 동적으로 로딩할 수 있습니다.
- JS는 defer 또는 async 사용
- 가능하면 JS를 <body> 끝에 배치해 렌더링 후에 실행되도록 함
- 또는 <script defer>를 <head>에 두어 렌더링과 병렬로 처리
- 필요 없는 JS는 lazy loading으로 관리 (예: IntersectionObserver로 동적 로딩)
- Code Splitting & Lazy Load (JS 모듈이나 페이지 단위로 분할 로딩)
> 예: React + Webpack에서 React.lazy() 사용
- 이미지 및 리소스에 loading="lazy" 사용
렌더링 차단 리소스란?
- HTML 문서 로딩 도중 렌더링을 멈추게 만드는 리소스
- 일반적으로 <script>(동기), <link rel="stylesheet">, @import된 CSS 등이 해당
Lazy Loading
- 필요한 순간에만 JS를 로드하여 초기 렌더링 속도 향상 시키는 기법
- 초기 페이지 진입 시 모든 JS를 로딩하지 않고, 사용자가 특정 인터렉션을 할 때 또는 화면에 보일 때 필요한 JS만 로드
const observer = new IntersectionObserver((entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
import('./expensiveModule.js').then((module) => {
module.init();
});
}
});
});
observer.observe(document.querySelector('#lazy-component'));
// 이렇게 하면, 특정 요소가 뷰포트에 들어올 때만 JS 모듈을 불러와서 실행하게 돼
Code Splitting
- 애플리케이션의 전체 JavaScript 번들을 작은 청크(chunk) 단위로 나누어, 필요한 시점에 필요한 코드만 로딩하는 기법
Code Splitting & Lazy Load
- 번들링된 JS 파일을 라우트 단위 또는 컴포넌트 단위로 분할하여, 필요한 시점에 불러오는 전략
- React에서는 React.lazy()와 Suspense를 사용
- Create React App(CRA) 환경이나 직접 Webpack 설정하는 프로젝트에서는 React.lazy() + Suspense 조합이 가장 표준적인 코드 분할 방식
- 사용자 인터랙션 시점에 필요한 컴포넌트를 지연 로딩(Lazy Load)하면서 초기 번들 사이즈를 줄이고, 렌더링 차단을 피하는 목적으로 널리 사용
그러나 Next.js, Remix, Vite 같은 프레임워크는 접근이 조금 다름
- 대부분 자동으로 설정되어 있다고 함
import React, { Suspense } from 'react';
const LazyComponent = React.lazy(() => import('./MyComponent'));
export default function App() {
return (
<Suspense fallback={<div>로딩 중...</div>}>
<LazyComponent />
</Suspense>
);
}
// MyComponent는 최초 로딩 시 번들에 포함되지 않고, 해당 컴포넌트가 마운트될 때 네트워크로 다운로드
// 이를 통해 초기 렌더링 속도를 단축할 수 있음
이미지 및 리소스에 loading="lazy" 사용
- <img> 요소나 <iframe>에서 아직 뷰포트에 보이지 않은 리소스를 나중에 로드함
<img src="large-image.jpg" loading="lazy" alt="설명" />
// 이미지가 스크롤을 내려서 뷰포트에 들어올 때 로드됨
// 브라우저가 자동으로 처리하므로 가장 간단한 Lazy Loading 방식
async는 언제 쓰는 게 좋을까요?
async는 스크립트의 실행 순서가 중요하지 않고, 빠르게 로드해서 곧바로 실행하고 싶은 외부 스크립트에 적합합니다.
광고 스크립트 (Google Ads), 웹 분석 도구 (Google Analytics), SNS 공유 버튼 삽입 스크립트
이런 스크립트는 DOM에 의존하지 않고, 기능만 수행하면 되기 때문에 async가 이상적입니다.
Lazy Loading과 Code Splitting은 언제 써야 하나요?
초기 페이지 로딩 속도를 개선하고 싶을 때, 사용자와 직접 연관 없는 JS나 이미지 등은 나중에 불러오는 게 효과적입니다.
- 라우팅 기반 앱에서 페이지 단위 JS 분리
- 탭 전환 시 보여줄 비동기 컴포넌트
- 뷰포트에 아직 보이지 않는 이미지나 리스트 항목
Q. 왜 CSS도 렌더링을 차단하나요?
CSS는 HTML 구조의 스타일을 정의하기 때문에, 브라우저는 스타일 적용 전 요소를 화면에 그릴 수 없습니다.
따라서 CSS 로딩이 끝날 때까지 렌더링을 지연합니다.
defer는 DOMContentLoaded 전에 실행되나요?
네. defer 스크립트는 문서 파싱이 완료된 직후(DOMContentLoaded 이벤트 발생 직전)에 실행됩니다.
<script async>를 여러 개 쓰면 순서가 보장되나요?
아니요. async는 순서가 보장되지 않습니다.
다운로드가 끝난 순서대로 바로 실행됩니다. 병렬 처리되기 때문에, 의존성이 있는 JS에는 적합하지 않습니다.
async와 defer가 동시에 있으면 어떻게 되나요?
브라우저는 async 우선 → async가 있으면 defer는 무시됩니다
<script src="main.js" async defer></script> <!-- => async만 동작 -->
'CS > Terminology' 카테고리의 다른 글
#16. null, undefined, undeclared, NaN (0) | 2025.03.20 |
---|---|
#15. 실행 컨텍스트란? (0) | 2025.03.19 |
#14. 타입스크립트란? (0) | 2025.03.18 |
#13. 이벤트 전파 (0) | 2025.03.17 |
#12. 마이크로태스크 큐 vs 태스크 큐 (0) | 2025.03.16 |