브라우저는 화면을 어떻게 그리는가 — 렌더링 · 저장소 · CORS · 캐싱
면접에서 "말로 설명한다"는 가정으로 작성. 답을 외우기보다 "왜"와 "트레이드오프"를 설명할 수 있는 것이 목표입니다.
2-1. 브라우저 렌더링 과정
- HTML 파싱 → DOM 트리 생성
- CSS 파싱 → CSSOM 트리 생성
- DOM + CSSOM 결합 → 렌더 트리 (화면에 보이는 것만)
- 레이아웃(Reflow): 각 요소의 위치·크기 계산
- 페인트(Paint): 픽셀로 그리기
- 컴포지팅: 레이어를 합성해 최종 화면 출력
JS는 DOM 파싱을 막을 수 있어 async/defer로 로딩을 제어합니다.
async: 다운로드 끝나는 즉시 실행(순서 보장 X)defer: HTML 파싱이 끝난 뒤 순서대로 실행
2-2. Reflow vs Repaint
- Reflow(레이아웃): 요소의 크기·위치가 바뀌어 레이아웃을 다시 계산 (비용 큼)
- Repaint: 위치는 그대로지만 색상 등 외형만 다시 그림
최적화:
- 레이아웃을 유발하는 속성(width, top 등) 대신
transform,opacity사용 → 컴포지터에서 처리되어 reflow/repaint 회피 - DOM 변경은 모아서 한 번에 (DocumentFragment 등)
- 레이아웃 값을 읽고 쓰는 작업을 번갈아 하면 강제 동기 레이아웃(layout thrashing) 발생 → 읽기/쓰기 분리
2-3. Cookie / LocalStorage / SessionStorage
| Cookie | LocalStorage | SessionStorage | |
|---|---|---|---|
| 용량 | ~4KB | ~5-10MB | ~5-10MB |
| 만료 | 설정 가능 | 영구 | 탭 닫으면 삭제 |
| 서버 전송 | 매 요청마다 자동 | 안 됨 | 안 됨 |
| 용도 | 인증 토큰, 세션 | 지속 데이터 | 일시적 데이터 |
쿠키는 HttpOnly(JS 접근 차단, XSS 방어), Secure(HTTPS만), SameSite(CSRF 방어) 속성으로 보안을 강화합니다.
2-4. CORS
브라우저의 동일 출처 정책(Same-Origin Policy) 때문에 다른 출처(프로토콜+도메인+포트)로의 요청이 제한되는데, 서버가 Access-Control-Allow-Origin 등의 헤더로 허용하면 통신이 가능합니다.
- 단순 요청이 아니면(커스텀 헤더, PUT/DELETE 등) 브라우저가 먼저 OPTIONS Preflight 요청을 보내 허용 여부를 확인
- 해결: 서버에서 CORS 헤더 설정(정석), 개발 중엔 프록시 서버 사용
2-5. 브라우저에 URL을 입력하면 무슨 일이?
- URL 파싱
- DNS 조회로 IP 획득
- TCP 연결 (+ HTTPS면 TLS 핸드셰이크)
- HTTP 요청 전송
- 서버 응답 수신
- 브라우저 렌더링(2-1 과정)
2-6. CSR / SSR / SSG
- CSR(Client-Side Rendering): 빈 HTML + JS로 클라이언트에서 렌더. 초기 로딩 느리고 SEO 불리, 이후 인터랙션은 빠름
- SSR(Server-Side Rendering): 서버에서 완성된 HTML 생성. 초기 로딩·SEO 유리, 서버 부하 있음. 하이드레이션(서버가 보낸 HTML에 JS 이벤트를 연결하는 과정)으로 인터랙티브화
- SSG(Static Site Generation): 빌드 시 HTML 미리 생성. 가장 빠르고 SEO 좋지만 데이터가 자주 바뀌면 부적합
2-7. HTTP 버전별 개선점
기초 용어
- RTT(Round-Trip Time): 요청을 보내고 응답을 받기까지의 왕복 시간. 연결 수립에 몇 RTT가 필요한지가 초기 속도를 좌우. 거리가 멀수록 1 RTT가 커짐(한국↔미국은 수백 ms).
- TLS(Transport Layer Security): 통신을 암호화하는 보안 프로토콜. HTTP + TLS = HTTPS. 암호화·무결성·인증을 제공. 과거 SSL의 후속(현재 TLS 1.3). 연결 시 암호화 방식·키를 합의하는 과정이 TLS 핸드셰이크.
- HOL(Head-of-Line) Blocking: 앞의 요청이 끝나야 뒤가 처리되어 줄줄이 막히는 현상.
HTTP/1.1 — 순차 처리, HOL Blocking
한 TCP 연결에서 요청을 순차 처리. 앞 요청 응답이 늦으면 뒤가 대기(HOL Blocking). 브라우저는 도메인당 6개 연결을 열어 우회했지만 근본 해결은 아니었음. (CSS 스프라이트, 도메인 샤딩 같은 꼼수 등장)
HTTP/2 — 멀티플렉싱
- 멀티플렉싱(핵심): 한 TCP 연결에서 여러 요청·응답을 동시 처리. 데이터를 "스트림"으로 쪼개 섞어 보내고 다시 조립 → HTTP 레벨 HOL Blocking 해결
- 헤더 압축(HPACK): 반복 헤더 압축·테이블 관리로 중복 전송 감소
- 바이너리 프로토콜: 텍스트 대신 바이너리 프레임 → 파싱 효율 향상
- 서버 푸시: 요청 전 리소스 선전송. 실효성 문제로 사실상 폐기 수순
HTTP/2의 한계 — TCP HOL Blocking: HTTP 레벨은 풀었지만 여전히 TCP 위에서 동작. TCP는 순서를 보장하므로 패킷 하나가 유실되면 뒤 패킷이 다 와도 재전송까지 대기. 멀티플렉싱으로 여러 스트림이 한 연결을 공유하기에, 한 스트림의 손실이 무관한 다른 스트림까지 막음 → TCP 레벨 HOL Blocking.
HTTP/3 — QUIC(UDP 기반)
HTTP/2: HTTP/2 → TLS → TCP
HTTP/3: HTTP/3 → QUIC(TLS 1.3 내장) → UDP
- TCP HOL Blocking 해결: QUIC은 스트림을 독립 관리 → 한 스트림 패킷 유실이 다른 스트림에 영향 없음
- 연결 수립이 빠름: TCP(3-way) + TLS를 따로 하면
2 RTT인데, QUIC은 연결+암호화를 통합해 01 RTT - 연결 마이그레이션: Connection ID로 연결 식별 → Wi-Fi ↔ LTE 전환에도 연결 유지(모바일 강점)
HTTP/3의 단점
- UDP 차단: 일부 방화벽·네트워크 장비가 UDP를 막아 TCP(HTTP/2)로 폴백 필요
- CPU 부하: TCP는 커널·하드웨어가 최적화돼 있지만 QUIC은 상당 부분 사용자 공간에서 처리 → CPU 사용량 높음
- 성숙도: 도구·디버깅·생태계가 TCP만큼 무르익지 않아 운영 난도 있음
- 구현 복잡도: 혼잡 제어·재전송·순서 보장을 직접 구현해야 함
정리 표
| HTTP/1.1 | HTTP/2 | HTTP/3 | |
|---|---|---|---|
| 전송 계층 | TCP | TCP | QUIC(UDP) |
| 요청 처리 | 순차 | 멀티플렉싱 | 멀티플렉싱 |
| HTTP HOL | 있음 | 해결 | 해결 |
| TCP HOL | 있음 | 남음 | 해결 |
| 헤더 압축 | X | HPACK | QPACK |
| 핸드셰이크 | TCP+TLS 분리 | TCP+TLS 분리 | 통합(0~1 RTT) |
| 형식 | 텍스트 | 바이너리 | 바이너리 |
면접 멘트: "HTTP/2는 멀티플렉싱으로 HTTP 레벨 HOL Blocking을 풀었지만, TCP 순서 보장 때문에 패킷 유실 시 TCP 레벨 HOL Blocking이 남았습니다. HTTP/3는 UDP 기반 QUIC으로 스트림을 독립 처리하고 핸드셰이크를 통합해 연결도 빨라졌습니다. 다만 UDP 차단 시 폴백이 필요하고 CPU 부하가 높습니다."
2-8. 캐싱 전략
브라우저 캐싱은 강한 캐싱과 조건부 캐싱(검증) 두 종류로 나눠 이해합니다.
1) 강한 캐싱 (Strong Cache) — 서버에 안 물어봄
캐시가 유효한 동안 네트워크 요청 없이 캐시에서 바로 사용. 가장 빠름.
Cache-Control (주력 헤더)
max-age=31536000: 해당 초 동안 신선하다고 간주(1년)no-cache: 캐시는 저장하되 쓰기 전 매번 서버에 검증 (이름과 달리 "캐시 안 함"이 아님)no-store: 아예 저장 안 함(민감 정보)private: 브라우저만 캐시 /public: 중간 서버(CDN)도 캐시 가능immutable: 만료 전엔 재검증조차 하지 말 것
2) 조건부 캐싱 (Revalidation) — 서버에 물어봄
캐시 만료 시 서버에 "바뀌었어?"라고 검증. 안 바뀌었으면 서버는 **본문 없이 304 Not Modified**만 보냄 → 본문 전송 생략으로 트래픽 절약.
- ETag / If-None-Match (콘텐츠 기반): 서버가 리소스 식별자(ETag)를 주고, 다음 요청 때
If-None-Match로 비교. 같으면 304, 다르면 200 + 새 본문 - Last-Modified / If-Modified-Since (시간 기반): 마지막 수정 시각으로 비교. 초 단위라 정밀도가 낮아 ETag가 더 정확하고 우선됨
no-cache는 왜 저장하는가?
no-cache는 저장하되 매번 검증합니다. 검증이 성공(304)하면 본문 없이 응답이 오므로 저장본을 그대로 재사용합니다. 만약 저장을 안 했다면 304를 받아도 쓸 게 없어 전체를 다시 받아야 합니다. 즉 no-cache가 절약하는 건 검증 요청이 아니라 무거운 본문 다운로드입니다.
요청 → 저장된 캐시 + ETag 있음
→ 서버에 조건부 요청 (If-None-Match)
├─ 안 바뀜 → 304 (본문 없음) → 저장본 재사용 ✅
└─ 바뀜 → 200 + 새 본문 → 새로 저장
비유: no-cache는 책을 집에 두고(저장) 쓸 때마다 도서관에 "개정판 나왔어요?"라고 전화(검증). 안 나왔으면 집 책을 보고(304), 나왔으면 새로 빌려옴(200). no-store는 매번 통째로 빌려오는 것.
3) 전체 흐름
요청
├─ 강한 캐시 유효?(max-age 안 지남)
│ └─ YES → 캐시 즉시 사용 (네트워크 X, 가장 빠름)
└─ NO(만료) → 조건부 요청 (If-None-Match / If-Modified-Since)
├─ 안 바뀜 → 304 (본문 없음, 캐시 재사용)
└─ 바뀜 → 200 + 새 본문 + 새 캐시 헤더
4) 실무 전략 — 자산 종류별
- 정적 자산(JS/CSS/이미지): 파일명에 내용 해시(
app.3f9a2b.js) +max-age=31536000, immutable. 내용이 바뀌면 해시(파일명)가 달라져 자동으로 새 파일 인식 → 캐시 버스팅. 영구 캐싱 + 즉시 반영 동시 달성 - HTML: 어떤 해시 파일을 부를지 알려주는 진입점이라
no-cache로 두고 항상 검증 → 새 해시 파일명을 가리키게 함 - API 응답: 보통
no-store또는 짧은max-age+ ETag
정리 표
| 구분 | 동작 | 헤더 | 결과 |
|---|---|---|---|
| 강한 캐싱 | 서버에 안 물음 | Cache-Control: max-age | 캐시 즉시 사용 |
| 조건부 캐싱 | 서버에 검증 | ETag/Last-Modified | 304 or 200 |
| 캐시 안 함 | 항상 새로 받음 | no-store | 200 |
면접 멘트: "캐싱은 강한 캐싱과 조건부 캐싱으로 나뉩니다. 정적 자산은 파일명 해시 + immutable로 영구 캐싱하면서 변경 시 파일명이 바뀌어 자동 무효화(캐시 버스팅)되게 하고, HTML은 진입점이라 no-cache로 매번 검증합니다. 검증은 ETag가 Last-Modified보다 정확해 우선됩니다."