브라우저는 어떻게 화면을 그릴까?
렌더링은 크게 4단계로 이루어집니다.
1. 브라우저가 HTML과 CSS를 파싱하여 DOM과 CSSOM 생성
2. DOM과 CSSOM을 결합해 렌더 트리 생성
3. 렌더 트리를 기반으로 각 요소의 위치와 크기를 계산하는 레이아웃 수행
4. 계산된 결과를 화면에 색을 입혀 그려내는 페인트 수행
HTTP/1.1과 HTTP/2
브라우저가 서버와 통신할 때 사용하는 규약이 바로 HTTP입니다. 현재 가장 많이 쓰이는 버전은 HTTP/1.1과 HTTP/2이고, 이 둘의 차이가 실제 성능에 큰 영향을 미칩니다.
HTTP/1.1의 한계
HTTP/1.1은 하나의 커넥션에서 요청과 응답을 하나씩 처리합니다. CSS파일, 자바스크립트 파일, 이미지 파일 등 모든 리소스 요청이 개별적으로 순차 처리됩니다.

개발자 도구 Network 패널에서 Queing 시간이 길게 잡히는 것을 확인할 수 있습니다.
이는 연결 제한 때문에 요청이 줄 서서 기다리고 있다는 것을 의미합니다.
같은 도메인에 동시 연결 수를 최대 6개로 제한하고 있기 때문에 이런 상황이 벌어지는거죠..
HTTP/2로 해결
HTTP/2는 하나의 커넥션에서 여러 요청과 응답을 동시에 처리할 수 있습니다. 연결 제한이 없어지기 때문에 Queing 대기 시간이 사라지고, HTTP/1.1 대비 페이지 로드 속도가 약 50% 빠릅니다.
현실적으로 문제를 해결하기 위해서는 HTTP/2를 지원하는 CDN을 사용하는 것입니다.
(대부분의 클라우드 CDN가 기본적으로 HTTP/2를 지원)
▶ 이미지가 많은 페이지에서 느리다면 HTTP 버전을 확인해보세요 !
HTTP/2로 당장 바꾸기 어렵다면, preconnect만으로도 첫 요청의 DNS 조회 + TCP + TLS 핸드셰이크 시간을 없앨 수 있습니다.
<link rel="preconnect" href="https://이미지서버도메인.com" crossorigin />
자바스크립트는 언제 실행될까?
브라우저는 HTML을 위에서 아래로 순서대로 파싱합니다. 그러다가 <script> 태그를 만나면 HTML 파싱을 멈추고 자바스크립트를 먼저 실행합니다.
<head>
<script>
const $apple = document.getElementById('apple'); // null
$apple.style.color = 'red'; // TypeError
</script>
</head>
<body>
<li id="apple">Apple</li>
</body>
브라우저 입장에서 <head>를 먼저 읽고 <script>를 발견해서 자바스크립트를 바로 실행합니다.
자바스크립트가 DOM안에서 id가 apple인 요소를 찾으려고 하지만
아직 <body> 아래의 id="apple"을 읽지 못했기 때문에 apple이라는 요소를 찾지 못해서 null을 출력합니다.
null.style.color가 되어 TypeError가 발생합니다.
> 일반 스크립트는 JS 다운로드 시간동안 HTML 읽기를 멈춤.
> 자바스크립트는 DOM에 이미 존재하는 요소만 찾을 수 있다!
async와 defer로 해결하기
JS 때문에 DOM을 만드는 작업이 중간에 멈춰서 HTML5부터 이 문제를 해결하기 위해 async와 defer 속성이 추가되었습니다.
async는 HTML 파싱과 JS 파일 로드를 동시에 진행하되, 로드가 완료되는 순간 HTML 파싱을 멈추고 즉시 실행합니다. (여러 스크립트가 있을 경우 실행 순서 보장X)
> JS 다운로드 끝나는 순간 바로 실행
defer는 HTML 파싱과 JS 파일 로드를 동시에 진행하되, HTML 파싱이 완전히 끝난 후 실행합니다. DOM이 완성된 뒤 실행이 보장되므로, 대부분의 경우 defer가 권장됩니다.
> DOM 다 만들고 JS 실행
<script defer src="app.js"></script>
리플로우와 리페인트
자바스크립트로 DOM을 변경하면 브라우저는 렌더링을 다시 수행합니다.
리플로우: 요소의 크기, 위치, 레이아웃이 바뀔 때 발생
> 레이아웃 계산을 처음부터 다시 하기 때문에 비용이 큼
- 요소 추가나 삭제 시
- 요소 크기나 위치 변경 시
- 윈도우 리사이징
- offsetWidth, scrollTop 등 레이아웃 관련 값 읽을 때
리페인트: 색상, 배경 등 레이아웃에 영향 없는 스타일만 바뀔 때 발생
> 리플로우 없이 페인트만 재진행
DOM 조작을 반복하면 그때마다 리플로우가 일어나기 때문에 아래 코드처럼 변경을 한 번에 모아서 처리하면 리플로우 횟수를 줄일 수 있습니다.
// 리플로우 3번 발생
element.style.width = '100px';
element.style.height = '100px';
element.style.margin = '10px';
// 리플로우 1번 발생 - good~
element.style.cssText = 'width: 100px; height: 100px; margin: 10px;';
// 또는 클래스를 한 번에 바꾸기
element.className = 'resized';
스크롤 이벤트와 리플로우
scrollTop, scrollLeft, offsetHeight, getBoundingClientRect() 같은 값은 읽는 것만으로도 브라우저가 최신 레이아웃을 계산해야 해서 리플로우를 강제로 발생시킵니다.
// bad: 스크롤할 때마다 리플로우 발생
window.addEventListener('scroll', () => {
const scrollY = window.scrollY; // 레이아웃 읽기 → 리플로우
element.style.opacity = scrollY / 300; // 스타일 변경 → 또 리플로우
});
// good: requestAnimationFrame으로 묶기
window.addEventListener('scroll', () => {
requestAnimationFrame(() => {
const scrollY = window.scrollY;
element.style.opacity = scrollY / 300;
});
});
// good: transform/opacity 사용 (리플로우 없이 GPU 처리)
element.style.transform = `translateY(${scrollY}px)`;
element.style.opacity = scrollY / 300;
top, left, width, height를 바꾸면 리플로우가 발생하지만, transform과 opacity는 레이아웃에 영향을 주지 않아 리플로우 없이 GPU에서 처리됩니다.
https://inpa.tistory.com/entry/%F0%9F%8C%90-requestAnimationFrame-%EA%B0%80%EC%9D%B4%EB%93%9C
🌐 웹 애니메이션 최적화 requestAnimationFrame 가이드
자바스크립트 웹 애니메이션 웹페이지의 애니메이션을 구현할때 CSS의 animatoin , transition , transform 속성을 통해 구현할 수도 있지만, 보다 사용자와의 복잡한 상호작용을 구현하게 하기 위해 Javasc
inpa.tistory.com
여기서 rAF와 setTimeout의 차이를 잘 설명해주고 있으니 한 번 확인하면 좋을 것 같습니다.
setTimeout은 단순히 일정 시간 뒤 실행되는 반면, rAF는 브라우저의 다음 렌더링 프레임 직전에 실행됩니다.
그래서 rAF는 브라우저의 화면 그리기 타이밍과 동기화되어 프레임 누락이 적고 더 부드러운 애니메이션을 만들 수 있습니다.
DOM이란?
DOM은 HTML 문서를 브라우저가 이해할 수 있는 자료구조로 변환한 것입니다. HTML 요소들은 노드 객체로 변환하고, 이 노드들을 계층적인 트리 구조로 표현합니다.
<div class="greeting">Hello</div>
// DOM에서는..
div 노드 (요소 노드)
├── class="greeting" (어트리뷰트 노드)
└── "Hello" (텍스트 노드)
HTML 문서 전체는 이런 노드들이 부모-자식 관계로 연결된 트리 구조를 가지고 있습니다.
자바스크립트는 이 DOM 트리를 DOM API를 통해 읽고 수정할 수 있습니다.
DOM 요소를 가져오고 만들고 바꾸기
요소 가져오기
// id로 가져오기
const apple = document.getElementById('apple');
// CSS 선택자로 가져오기
const first = document.querySelector('.item');
const all = document.querySelectorAll('.item');
텍스트 바꾸기
apple.textContent = '사과'; // 텍스트만 변경
apple.innerHTML = '<b>사과</b>'; // HTML 태그 포함 변경
노드 만들어서 추가하기
// 1. 요소 생성
const li = document.createElement('li');
// 2. 내용 채우기
li.textContent = 'Mango';
// 3. DOM에 추가
document.querySelector('ul').appendChild(li);
innerHTML은 간편하지만 기존 자식 노드를 모두 제거하고 다시 파싱하기 때문에 자주 변경되는 요소에는 성능 부담이 있습니다. 또한 사용자 입력값을 그대로 넣으면 XSS 공격에 취약해지기 때문에
동적으로 요소를 추가할 때는 createElement + appendChild 방식을 권장한다고 하네요,,
XSS(Cross-Site Scripting)란?
공격자가 악의적인 스크립트를 페이지에 주입하는 공격
innerHTML에 사용자 입력값을 그대로 넣었을 때 주의해야합니다.
// 사용자가 검색창에 아래와 같이 입력하면?
const userInput = '<img src="x" onerror="document.cookie를 탈취하는 코드">';
// 그대로 실행돼서 위험하다..
searchResult.innerHTML = userInput;
// 안전한 방법 - 태그로 해석하지 않고 문자열로 처리
searchResult.textContent = userInput;
해결방법은 사용자 입력을 화면에 표시할 땐 innerHTML 대신 textContent를 쓰기!
그렇게 하면 태그가 있어도 그냥 텍스트로 화면에 표시됩니다.
'CS' 카테고리의 다른 글
| 렌더링 최적화: React.memo/useMemo/useCallback (0) | 2026.05.25 |
|---|---|
| 번들 크기를 보자 : Bundle Analyzer (0) | 2026.05.25 |
| [JavaScript] 객체/프로퍼티/프로토타입 (1) | 2026.05.23 |
| [JavaScript] this/클로저 : 코드로 이해하기 (2) | 2026.05.22 |
| Caching과 Prefetching (0) | 2026.05.11 |