웹 개발
브라우저가 HTML·CSS·JS를 받아 화면을 그리는 순서에서 출발해, 스크립트를 싣는 방법, HTTP 캐시와 HTTP/2·3, CSR·SSR·SSG·아일랜드 같은 렌더링 전략, Core Web Vitals, 이미지·폰트, 접근성, SPA 라우팅과 서비스 워커까지를 숫자로 계산해 가며 정리한다.
목차 (15)
주소를 넣은 뒤 DNS·TCP·TLS를 거쳐 HTML이 도착하기까지는 네트워크 노트에 있고, XSS·CSP·CORS 같은 브라우저 보안은 보안 › 웹 노트에 있다. 이 노트는 그 사이, HTML이 도착한 뒤 브라우저 안에서 일어나는 일과 그것을 빠르고 누구나 쓸 수 있게 만드는 방법을 다룬다.
중요 렌더링 경로
브라우저가 바이트를 픽셀로 바꾸는 데 필요한 작업과 의존성을 중요 렌더링 경로(critical rendering path)라 한다. HTML은 DOM으로, CSS는 CSSOM으로 바뀐다. 스타일을 적용해 위치·크기를 계산하고(레이아웃), 그리기 명령을 기록하고(페인트), 명령을 픽셀로 바꾸고(래스터화), 레이어를 합쳐 표시할 프레임을 만든다(합성). 첫 화면이 늦는 까닭은 대개 필요한 자원이나 앞 단계의 결과를 기다리기 때문이다.
서버가 보낸 HTML 바이트가 도착한다. 브라우저는 다 받기 전부터 앞부분을 읽기 시작한다.
- CSS는 렌더를 막을 수 있다: 스타일이 덜 된 화면을 보였다가 고치면 깜박이므로, 처음 화면에 적용되는 렌더 차단 스타일시트를 기다린다. 모든 매체·로딩 방식의 CSS가 똑같이 차단하는 것은 아니다. 첫 화면에 필요한 CSS를 작게 두고 나머지는 필요할 때 싣는다.
- 동기 스크립트는 파싱을 막는다: 스크립트가
document.write나 DOM 조작으로 문서를 바꿀 수 있으니, 파서는 멈추고 스크립트를 내려받아 실행한다. 다음 절의 async·defer가 이것을 푸는 방법이다. - 다시 하는 단계를 줄인다: 너비·높이처럼 배치에 영향을 주는 변경은 레이아웃부터 필요할 수 있고, 배경색 변경은 보통 페인트·래스터화가 필요하다. 조건이 맞는
transform·opacity애니메이션은 준비된 픽셀을 재사용해 합성 위주로 처리할 수 있다. 언제나 GPU 처리만 남는다는 보장은 아니다.
브라우저 렌더링 파이프라인: 계산과 픽셀 생성을 구분한다
렌더링 파이프라인은 한 프로그램이 전부 순서대로 실행한다는 뜻이 아니다. 다음은 Chromium RenderingNG의 역할을 단순화한 구분이다. 브라우저마다 내부 자료 구조와 스레드 배치는 다르고, 실제 구현에는 pre-paint·commit·레이어 구성 같은 중간 단계도 있다.
| 단계 | 계산하는 것과 결과물 | 실행 경로의 예 |
|---|---|---|
| 스타일 계산 | CSS 선택자·상속·우선순위를 적용해 요소의 계산된 스타일을 정한다. | 렌더러의 메인 스레드 |
| 레이아웃 · 리플로우 | 줄바꿈·상자 크기·위치를 계산한다. 아직 픽셀 색을 그린 것은 아니다. | 렌더러의 메인 스레드 |
| 페인트 | 글자·배경·테두리 등을 그릴 순서를 display list 같은 명령 목록으로 기록한다. | 렌더러의 메인 스레드 |
| 래스터화 | 명령 목록을 실제 픽셀 타일로 바꾼다. 이미지 디코딩과 자원 준비도 필요하다. | 래스터 작업·GPU 경로, 상황에 따른 CPU 폴백 |
| 합성·표시 | 타일·레이어의 위치·클립·효과를 조합하고 표시할 프레임을 제출한다. | 합성 스레드·디스플레이 합성기·GPU |
페인트와 래스터화는 같은 말이 아니다. 페인트가 "어떤 순서로 무엇을 그릴지"를 기록한다면 래스터화는 그 명령을 픽셀로 계산한다. 레이어도 DOM 요소와 일대일이 아니다. 브라우저가 재사용·클립·효과·메모리 비용을 고려해 구성하므로 모든 요소에 will-change를 넣는 것은 해법이 아니다. 합성 전용 속성과 레이어 관리 안내가 이 절충을 설명한다.
최초 방문에는 파싱·이미지·폰트·스타일 준비가 필요하지만, 이후 갱신은 바뀐 부분만 다시 계산하거나 이전 결과를 재사용한다. 합성 스레드에서 처리 가능한 스크롤·애니메이션은 메인 스레드의 일부 단계를 건너뛸 수 있다. 반대로 새로 보이는 영역의 픽셀을 아직 만들지 않았다면 추가 래스터화가 필요하다. 변경마다 어느 단계가 필요한지는 렌더링 성능 안내에서도 구분한다.
이 과정도 결국 컴퓨터 그래픽스다. Canvas·WebGL·WebGPU로 그린 결과 역시 페이지의 다른 내용과 함께 표시되지만, 캔버스 안의 물체 배치까지 DOM 레이아웃이 담당하는 것은 아니다. 컴퓨터 그래픽스 노트의 브라우저·게임·편집기 비교가 그 경계를 다룬다.
리플로우와 레이아웃 시프트는 다른가
리플로우는 위치·크기를 계산하는 작업이고, 레이아웃 시프트는 표시된 프레임 사이에서 요소의 시작 위치가 이동한 결과다. 같은 위치를 다시 계산할 수도 있으므로 리플로우가 있었다고 반드시 화면이 흔들리지는 않는다. 계산 비용이 커서 느린 페이지도 CLS는 0일 수 있다.
| 용어 | 뜻 | 무엇을 측정하는가 |
|---|---|---|
| 레이아웃 · 리플로우 | 배치 규칙으로 위치·크기를 계산하는 정상적인 렌더링 작업이다. | 작업 시간·범위·횟수 |
| 강제 동기 리플로우 | JS가 최신 크기·위치를 요구해, 브라우저가 미뤄 둔 스타일·레이아웃 계산을 그 자리에서 실행한다. | 동기 계산 때문에 JS·입력 처리가 기다린 시간 |
| 레이아웃 시프트 | 이미 보이던 요소의 시작 위치가 프레임 사이에 달라진다. 크기만 바뀌고 기존 요소의 시작 위치가 유지되면 그것만으로 시프트는 아니다. | 움직인 영역과 이동 거리 |
| CLS | 예상하지 못한 시프트 점수 중 가장 큰 세션 창의 합이다. 모든 리플로우를 세는 지표가 아니다. | 시각적 안정성의 점수이며 시간 단위가 아니다. |
예를 들어 이미지 자리를 미리 예약해 두면 이미지가 도착해도 본문 위치가 유지된다. 반대로 읽는 본문 위에 공간 없이 배너를 넣으면 JS가 크기를 읽지 않아도, 브라우저의 정상적인 갱신에서 본문이 밀릴 수 있다. CLS의 구체적인 계산과 일부 최근 사용자 입력에 따른 변화의 제외 규칙은 web.dev의 CLS 정의와 아래 Core Web Vitals 절에 있다. CSS transform으로 움직인 모습도 이 레이아웃 시프트 측정과는 구별한다.
강제 리플로우는 언제 생기는가
getBoundingClientRect()·offsetHeight를 읽는 행위가 언제나 느린 것은 아니다. 사용할 수 있는 계산 결과가 있으면 재사용한다. 그러나 배치에 영향을 주는 스타일을 바꾼 직후 최신 값을 읽으면 계산을 먼저 끝내야 한다. 이런 쓰기 → 읽기를 반복하는 것이 layout thrashing이다. 레이아웃 반복 방지 안내와 Forced reflow 인사이트가 이 비용을 다룬다.
코드 예시: 변경 후 크기가 필요하면 쓰기와 읽기를 각각 모은다
/** 각 요소를 바꾼 직후 측정해 동기 레이아웃을 반복시킬 수 있다. */
function resizeAndMeasureEach(boxes) {
const heights = [];
for (const box of boxes) {
box.style.width = '240px';
heights.push(box.getBoundingClientRect().height);
}
return heights;
}
/** 변경을 먼저 모은 뒤 변경 후 크기를 한꺼번에 읽는다. */
function resizeThenMeasureAll(boxes) {
for (const box of boxes) {
box.style.width = '240px';
}
return boxes.map((box) => box.getBoundingClientRect().height);
}두 함수는 요소 배열을 받아 변경 후 높이 배열을 돌려준다. 두 번째 방식도 첫 측정에서 동기 계산이 필요할 수 있지만, 변경과 측정을 교대로 반복하는 것을 피한다. 실제 계산 범위와 횟수는 스타일·격리·브라우저 최적화에 따라 달라진다. 변경 전 값이면 읽기를 먼저 모으고 그다음에 쓴다.
requestAnimationFrame도 자동 해결책은 아니다. 콜백 안에서 스타일을 바꾸고 즉시 측정하면 강제 계산이 생길 수 있다. 필요 없는 측정은 생략하고, 보이는지 확인하는 목적이라면 IntersectionObserver처럼 이미 전달되는 관측 결과를 쓰는 편이 낫다. DevTools에서는 Layout 작업·강제 리플로우 호출 위치와 Layout Shift 기록을 따로 확인한다.
자바스크립트 싣기: async와 defer
<script>에 아무 속성이 없으면 파서가 멈춘 채 내려받고 실행한다. async는 파싱과 나란히 내려받다가 다 받는 즉시 실행하고, defer는 나란히 내려받되 파싱이 끝난 뒤 문서 순서대로 실행한다. type="module" 스크립트는 기본이 defer처럼 동작한다(HTML 표준).
그림의 가정에서 HTML 파싱이 끝나는 시각은 일반 스크립트 500ms, async 380ms, defer 300ms다. async는 언제 실행될지 몰라 서로 순서가 없으니 분석 도구처럼 홀로 도는 스크립트에, defer는 DOM을 만지고 순서가 중요한 앱 코드에 맞다. 첫 화면에 꼭 필요한 자원은 <link rel="preload">로 일찍 요청해 둘 수 있다.
이벤트 루프: 태스크와 마이크로태스크
페이지의 자바스크립트는 메인 스레드 하나에서 돈다. 스크립트 실행, 클릭 처리, 타이머 콜백이 모두 태스크로 큐에 들어가고, 이벤트 루프가 가장 오래된 태스크를 하나씩 꺼내 끝까지 실행한다. Promise의 then과 queueMicrotask는 따로 마이크로태스크 큐에 들어간다. 태스크 하나가 끝나 실행 스택이 비면 마이크로태스크를 큐가 빌 때까지 모두 실행하고, 그다음에야 브라우저가 화면을 갱신할 기회를 얻고 다음 태스크로 넘어간다(MDN 마이크로태스크 안내, HTML 표준).
- 순서 읽기: 그림의 출력은 A G D E B C F다. 동기 코드(A, G)가 먼저 끝나고, 그 태스크에 딸린 마이크로태스크(D, E), 그다음 타이머 태스크(B)와 그 마이크로태스크(C), 마지막 타이머(F) 순이다.
- 긴 태스크가 입력을 막는다: 태스크는 중간에 끊기지 않으므로 오래 도는 동안 클릭도 화면 갱신도 기다린다. web.dev는 50ms를 넘는 태스크를 긴 태스크로 부른다(Optimize long tasks). 아래 Core Web Vitals 절의 INP가 나빠지는 주된 원인이다. 일을 쪼개 중간에 메인 스레드를 양보하는데,
setTimeout은 이어서 할 일을 큐 맨 뒤로 보내고,scheduler.yield()는 이어서 할 일을 같은 종류의 다른 태스크보다 먼저 돌려준다. - 마이크로태스크는 화면을 굶길 수 있다: 마이크로태스크가 계속 새 마이크로태스크를 넣으면 큐가 비지 않아 다음 태스크와 렌더링이 오지 않는다. 쪼개서 양보하려면 마이크로태스크가 아니라 새 태스크로 넘겨야 한다.
서버의 Node.js도 이벤트 루프로 돌지만 목적은 I/O 기다림을 겹치는 것이다. 그쪽 이야기는 병렬 프로그래밍 노트의 동시성과 병렬 절에 있다.
HTTP 캐시
가장 빠른 요청은 보내지 않는 요청이다. 서버는 응답에 Cache-Control을 달아 얼마나 오래 다시 묻지 말지 정한다(RFC 9111). max-age 안의 응답은 신선해서 네트워크를 타지 않는다. 기한이 지나면 브라우저는 저장해 둔 ETag를 If-None-Match에 실어 묻고, 내용이 같으면 서버가 본문 없이 304 Not Modified로 답한다.
- 해시 붙은 파일 이름:
app.3f9a1c.js처럼 내용이 바뀌면 이름이 바뀌는 자원은 1년 캐시와immutable을 걸고, 그 이름을 가리키는 HTML은no-cache로 둔다. 배포하면 HTML만 새로 확인되고 바뀐 파일만 새로 받는다(MDN HTTP caching). - stale-while-revalidate: 기한이 조금 지난 응답은 일단 보여 주고 뒤에서 새로 받아 둔다(RFC 5861). 기다림 없이 다음 방문에 새 내용이 보인다.
- no-cache ≠ no-store: 이름과 달리
no-cache는 저장을 허락하고 매번 재검증하라는 뜻이다. 저장 자체를 막는 것은no-store이며, 개인 정보가 담긴 응답에 쓴다.
HTTP/2와 HTTP/3
HTTP/1.1은 연결 하나에서 요청을 하나씩만 주고받을 수 있어, 브라우저가 서버마다 연결을 6개쯤 열어 나눠 받았다. HTTP/2는 연결 하나에 여러 요청을 스트림으로 섞어 보내고 헤더를 압축한다. 그래서 파일을 하나로 합치거나 도메인을 쪼개던 옛 요령이 대부분 필요 없어졌다.
그림의 모델에서 HTTP/1.1은 700ms, HTTP/2는 300ms, HTTP/3은 200ms다. HTTP/2도 아래는 TCP 하나라 패킷 하나가 사라지면 모든 스트림이 그 재전송을 기다린다(줄 막힘, head-of-line blocking). HTTP/3은 UDP 위의 QUIC을 써서 스트림마다 따로 재전송하고, 전송과 암호 연결을 한 왕복에 맺는다. 손실이 잦은 모바일 망에서 차이가 커진다. TCP와 UDP의 차이는 네트워크 노트에 있다.
렌더링 전략: CSR·SSR·SSG·ISR·아일랜드
HTML을 어디서, 언제 만드느냐에 따라 전략이 갈린다(Rendering on the Web).
- CSR(클라이언트 렌더링): 빈 껍데기 HTML과 큰 JS를 보내고 브라우저가 데이터를 받아 그린다. 서버는 단순하지만 JS가 다 돌기 전까지 화면이 비어 있다.
- SSR(서버 렌더링): 요청마다 서버가 데이터를 받아 HTML을 만든다. 내용은 빨리 보이지만 같은 컴포넌트의 JS가 와서 이벤트를 붙이는 하이드레이션이 끝나야 조작된다. 그 사이 버튼이 눌리지 않는 구간이 생긴다.
- SSG(정적 생성): 빌드 때 HTML을 미리 만들어 CDN에 둔다. 첫 바이트가 가장 빠르지만 내용이 바뀌면 다시 빌드해야 한다.
- ISR(점진적 재생성): SSG 페이지를 정한 주기나 요청에 따라 서버가 뒤에서 다시 만들어 갈아 끼운다. 정적의 속도와 갱신을 섞은 방식으로, 캐시의 stale-while-revalidate와 같은 생각이다.
- 아일랜드: 페이지 대부분은 JS 없는 정적 HTML로 두고, 상호작용이 필요한 부분(섬)만 따로 하이드레이션한다(Jason Miller). 이 사이트가 쓰는 Astro가 이 구조다.
그림에서 내용이 보이는 시각은 CSR 1000ms, SSR 550ms, SSG 200ms이고, 조작할 수 있는 시각은 각각 1000ms, 1350ms, 1000ms, 아일랜드 360ms다. 서버 렌더링이 화면을 앞당겨도 JS 양이 그대로면 조작 시각은 줄지 않는다. 결국 보내는 JS를 줄이는 것이 어느 전략에서나 가장 큰 손잡이다.
번들링: 트리 셰이킹과 코드 분할
앱 코드는 수백 개의 모듈 파일로 나뉘어 있다. 번들러(webpack, Rollup 등)는 import·export를 따라 의존 그래프를 만들고, 배포용 파일 몇 개로 묶으며 줄인다. ES 모듈의 import·export는 실행 전에 구조를 알 수 있어서, 어디서도 가져다 쓰지 않는 내보내기를 결과에서 지울 수 있다. 이것이 트리 셰이킹(죽은 코드 제거)이다(MDN). 그래서 라이브러리를 통째로 가져오는 대신 필요한 함수만 이름으로 가져오는 편이 번들이 작다.
- 코드 분할: 첫 화면에 필요 없는 코드는
import()(동적 import)로 따로 떼어 두고, 그 기능을 처음 쓸 때 받아 온다. 첫 실행에 보내는 JS가 줄어 메인 스레드 작업이 줄고, 조작할 수 있는 시각이 앞당겨진다(web.dev). 첫 라우트에 필요한 코드만 먼저 보내고 라우트·컴포넌트 단위로 나누는 것이 흔한 방법이다. - 해시 이름과 캐시: 빌드 도구가 파일 이름에 내용 해시를 넣으면 위 HTTP 캐시 절의 1년 캐시 +
immutable패턴과 바로 맞물린다. 자주 바뀌는 앱 코드와 거의 안 바뀌는 라이브러리를 다른 파일로 나누면 배포 뒤에도 라이브러리 파일은 캐시에서 그대로 쓴다. - 모듈을 그대로 보내도 된다: 브라우저는
type="module"로 ES 모듈을 직접 읽는다(MDN 모듈 안내). 그래도 MDN은 번들러가 쓸모없어지지 않는다고 적는다. 코드를 알맞은 크기의 조각으로 나누고, 줄이고(minify), 죽은 코드를 지우는 일은 여전히 번들러가 잘한다.
Core Web Vitals
Google은 사용자가 느끼는 품질을 세 지표로 잰다(web.dev). LCP는 화면에서 가장 큰 이미지나 글 덩어리가 그려진 시각, INP는 페이지에서 일어난 클릭·탭·키 입력이 다음 화면에 반영되기까지 걸린 시간 중 가장 나쁜 쪽에 가까운 값, CLS는 예고 없이 밀려난 레이아웃의 누적 점수다. INP는 2024년 3월 FID를 대신해 들어왔다(web.dev).
| 지표 | 좋음 | 나쁨 | 주로 고치는 곳 |
|---|---|---|---|
| LCP | 2.5초 이하 | 4.0초 초과 | 서버 응답, 렌더를 막는 CSS·JS, 큰 이미지의 늦은 발견 |
| INP | 200ms 이하 | 500ms 초과 | 메인 스레드를 오래 잡는 긴 작업, 큰 DOM 갱신 |
| CLS | 0.1 이하 | 0.25 초과 | 크기 없는 이미지·광고, 늦게 끼어드는 배너, 폰트 교체 |
판정은 평균이 아니라 실제 방문의 75번째 백분위로 한다(기준을 정한 근거). 방문 10번의 LCP가 1.8초, 2.1초, 2.3초, 2.4초, 2.6초, 2.9초, 3.1초, 3.6초, 4.2초, 5.2초라면 평균은 3.0초이지만 75번째 백분위는 3.6초로 '개선 필요'이다. 평균은 빠른 방문이 느린 방문을 가려 주지만, 백분위는 넷 중 하나가 겪는 느림을 그대로 드러낸다. 같은 방식으로 INP 예시의 75번째 백분위는 280ms다.
CLS의 이동 하나는 영향 비율 × 거리 비율이다. 뷰포트의 75%를 덮는 글이 뷰포트 높이의 25%만큼 밀리면 0.1875점이다. 이동들은 1초 안에 이어지고 5초를 넘지 않는 세션 창으로 묶고, 가장 큰 창의 합을 CLS로 쓴다(web.dev CLS). 그림의 이동 네 번은 창 2개로 묶여 CLS 0.20가 된다.
이미지와 폰트
LCP 요소는 대개 이미지다. 핵심은 필요한 만큼만, 일찍 보내는 것이다. srcset에 폭이 다른 후보를 적고 sizes로 칸의 CSS 폭을 알려 주면, 브라우저가 기기의 픽셀 비율에 맞는 후보를 고른다(MDN 반응형 이미지).
- 디코딩 메모리: 화면에 올린 이미지는 픽셀당 4바이트다. 4000×3000 원본은 48.0MB, 800w 후보는 1.9MB다. 저사양 휴대폰에서 스크롤이 끊기는 흔한 까닭이다.
- 크기 예약:
width·height속성이나aspect-ratio를 주면 이미지가 오기 전에 자리를 잡아 CLS가 생기지 않는다. - 지연 로딩과 우선순위: 화면 밖 이미지는
loading="lazy"로 미루고(web.dev), LCP 이미지는 반대로fetchpriority="high"를 주고 lazy를 걸지 않는다(Optimize LCP). - 형식: 사진은 AVIF·WebP가 JPEG보다 같은 화질에서 작다. 아이콘과 선 그림은 SVG가 크기와 선명도 모두 낫다.
- 폰트: 웹 폰트가 오기 전 글자를 숨기면(FOIT) LCP가, 대체 글꼴로 보였다가 바꾸면(FOUT) CLS가 나빠진다.
font-display: swap으로 글을 먼저 보이고(MDN), 쓰는 글자만 남기는 서브셋과 WOFF2, 첫 화면 폰트의 preload로 교체 시간을 줄인다(web.dev 폰트). 한글 폰트는 글자 수가 많아 서브셋의 효과가 특히 크다.
접근성
접근성은 화면 낭독기, 키보드, 확대, 낮은 시력, 떨리는 손으로도 같은 일을 할 수 있게 하는 것이다. 기준은 W3C의 WCAG이고 현재 권고안은 2023년 10월에 나온 2.2다. 원칙은 인식할 수 있고, 운용할 수 있고, 이해할 수 있고, 견고해야 한다는 넷이다(MDN Understanding WCAG).
- 시맨틱 HTML이 먼저:
<button>은 포커스, Enter·Space 동작, '버튼'이라는 역할을 공짜로 준다.<div onclick>은 이 모두를 직접 만들어야 한다. 제목은h1~h6순서로, 목록은ul로, 입력에는<label>을 단다. - ARIA는 빈칸만 채운다: ARIA의 첫 규칙은 '같은 일을 하는 HTML 요소가 있으면 그것을 쓰라'다. ARIA는 의미만 바꿀 뿐 동작을 더하지 않으므로,
role="button"을 달았다면 키보드 처리도 직접 해야 한다. 틀린 ARIA는 없는 것보다 나쁘다. - 키보드와 포커스: Tab만으로 모든 기능에 닿고, 포커스 테두리가 보여야 한다. WCAG 2.2는 포커스된 요소가 고정 헤더 등에 가려지지 않을 것(2.4.11), 누르는 대상이 최소 24×24 CSS px일 것(2.5.8), 끌기 동작에 대체 수단이 있을 것(2.5.7), 기억력 시험 같은 인증을 강요하지 않을 것(3.3.8)을 새로 넣었다.
- 명암비: 본문 글자는 바탕과 4.5:1(AA), 큰 글자는 3:1 이상이어야 한다. 흰 바탕에서 #767676은 4.54:1로 통과하지만 한 단계 밝은 #777777은 4.48:1로 떨어진다.
자동 검사 도구는 명암비·빠진 대체 텍스트 같은 일부만 잡는다. 키보드만으로 한 번, 화면 낭독기로 한 번 직접 써 보는 것이 가장 확실한 검사다(web.dev Learn Accessibility).
상태 관리와 SPA 라우팅
SPA(single-page application)는 페이지를 옮길 때 서버에 새 HTML을 받지 않고 JS가 화면을 바꾼다. 링크 클릭을 가로채 History API의 pushState로 주소만 바꾸고, 주소를 라우트 표와 맞춰 그릴 화면을 고른다. 최근 브라우저는 이 흐름을 한곳에서 다루는 Navigation API도 제공한다.
SPA는 화면 전환이 매끄러운 대신, 서버가 하던 일을 떠맡는다. 뒤로 가기 때 스크롤 위치 복원, 새 화면의 제목과 포커스 옮기기(화면 낭독기 사용자는 바뀐 줄 모른다), 오래 열린 탭에서 커지는 메모리를 직접 챙겨야 한다.
- 상태는 한 곳에: 같은 값을 두 곳에 두면 언젠가 어긋난다. 원본(source of truth)은 하나로 두고 나머지는 거기서 계산한다(파생 상태).
- 서버 상태와 화면 상태를 나눈다: 서버에서 온 데이터는 캐시다. 언제 받았는지, 언제 다시 받을지, 실패하면 어떻게 할지가 핵심이라 캐시 라이브러리에 맡기는 편이 낫다. 열린 메뉴·입력 중인 글 같은 화면 상태만 컴포넌트가 들고 있는다.
- 주소도 상태다: 탭·필터·검색어를 주소(쿼리 문자열)에 두면 공유와 새로 고침, 뒤로 가기가 저절로 맞는다.
웹 API: fetch와 서비스 워커
fetch는 요청을 보내고 Promise로 응답을 받는다. 주의할 점은 404·500도 실패가 아니다는 것이다. 네트워크 자체가 끊길 때만 거부되므로 response.ok를 직접 확인해야 한다. 오래 걸리는 요청은 AbortController로 끊고, 다른 출처로 보내는 요청은 서버의 CORS 허락이 있어야 읽을 수 있다(보안 › 웹 노트).
서비스 워커는 페이지와 네트워크 사이에 끼는 스크립트다. 페이지가 보내는 요청을 fetch 이벤트로 가로채 캐시에서 답하거나 네트워크로 넘길 수 있어, 오프라인에서도 열리는 앱(PWA)과 푸시 알림의 바탕이 된다. HTTPS에서만 동작하고, 새 버전은 열린 탭이 모두 닫힌 뒤에야 활성화되는 수명 주기가 있다.
브라우저에 데이터를 남기는 곳도 여럿이고 성격이 다르다. 모두 출처(스킴·호스트·포트)별로 나뉘어 다른 사이트가 읽지 못한다.
| 저장소 | 수명 | 성격 | 쓰임 |
|---|---|---|---|
| 쿠키 | 만료 속성까지 | 요청마다 서버로 함께 간다. HttpOnly면 스크립트가 못 읽는다. | 로그인 세션. 속성과 CSRF는 보안 › 웹 노트 |
localStorage | 지울 때까지 | 문자열 키-값, 동기 API라 큰 값을 다루면 메인 스레드가 멈춘다. 출처당 5MiB까지다. | 테마 같은 작은 설정 |
sessionStorage | 탭을 닫을 때까지 | localStorage와 같지만 탭마다 따로다. | 입력 중인 양식 임시 저장 |
| IndexedDB | 지울 때까지 | 비동기 트랜잭션 DB. 객체·파일(Blob)을 색인과 함께 많이 담는다. | 오프라인 데이터, 큰 캐시 |
용량과 한도는 MDN 저장 한도 문서를 따랐다. 기본 저장은 '최선 노력'이라 기기 공간이 모자라면 브라우저가 알리지 않고 지울 수 있고, 꼭 남아야 하면 navigator.storage.persist()로 영구 저장을 요청한다. Web Storage는 동기라 큰 데이터에는 IndexedDB가 맞다. 스크립트가 읽을 수 있는 곳에 인증 토큰을 두면 XSS 한 번에 빼앗기므로, 토큰을 어디 둘지는 보안 › 인증 노트를 따른다.
핵심 자료
요지는 직접 정리한 한 줄이다.
| 자료 | 한 줄 요지 |
|---|---|
| Critical rendering path (MDN) | HTML·CSS·JS가 픽셀이 되기까지의 단계와, 무엇이 렌더와 파싱을 막는지 정리한 안내서. |
| HTML Standard — Scripting | async·defer·type=module 스크립트가 언제 내려받고 언제 실행되는지 정한 표준 원문. |
| RFC 9111 HTTP Caching | 신선도, 조건부 요청, Cache-Control 지시어의 뜻을 정한 HTTP 캐시 표준. |
| HTTP caching (MDN) | 브라우저·공유 캐시, max-age, ETag 재검증, 파일 이름에 해시를 넣는 캐시 무효화 패턴을 예제로 설명한다. |
| RFC 9114 HTTP/3 | QUIC 위에서 HTTP를 주고받는 방법. 스트림마다 따로 재전송해 TCP의 줄 막힘을 없앤다. |
| Rendering on the Web (web.dev) | CSR·SSR·정적 렌더링·하이드레이션의 장단점과 지표에 주는 영향을 비교한 글. |
| Islands Architecture (Jason Miller) | 정적 HTML 바다에 상호작용 섬만 따로 하이드레이션한다는 아일랜드 구조를 처음 정리한 글. |
| Web Vitals (web.dev) | LCP·INP·CLS의 정의와 좋음 기준, 75번째 백분위로 판정하는 이유. |
| Understanding WCAG (MDN) | WCAG의 네 원칙(인식·운용·이해·견고)과 성공 기준을 개발자 눈으로 풀어 쓴 안내서. |
| Service Worker API (MDN) | 요청을 가로채 캐시로 응답하는 서비스 워커의 수명 주기와 쓰임. |
- Critical rendering path (MDN)
- HTML Standard — Scripting
- RFC 9111 HTTP Caching
- HTTP caching (MDN)
- RFC 9114 HTTP/3
- Rendering on the Web (web.dev)
- Islands Architecture (Jason Miller)
- Web Vitals (web.dev)
- Understanding WCAG (MDN)
- Service Worker API (MDN)
- <script> 요소 (MDN)
- Cache-Control (MDN)
- ETag (MDN)
- RFC 5861 — stale-while-revalidate
- Keeping things fresh with stale-while-revalidate (web.dev)
- RFC 9113 HTTP/2
- RFC 9000 QUIC
- Islands architecture (Astro 문서)
- Largest Contentful Paint (web.dev)
- Interaction to Next Paint (web.dev)
- Cumulative Layout Shift (web.dev)
- INP가 Core Web Vital이 되다 (web.dev, 2024)
- How the Core Web Vitals thresholds were defined (web.dev)
- Optimize LCP (web.dev)
- Responsive images (MDN)
- Browser-level image lazy loading (web.dev)
- font-display (MDN)
- Best practices for fonts (web.dev)
- ARIA (MDN)
- Learn Accessibility (web.dev)
- History API (MDN)
- Navigation API (MDN)
- Fetch API (MDN)
- Using microtasks with queueMicrotask() (MDN)
- JavaScript execution model (MDN)
- HTML Standard — Event loops
- Optimize long tasks (web.dev)
- Tree shaking (MDN 용어집)
- JavaScript modules (MDN)
- Reduce JavaScript payloads with code splitting (web.dev)
- Web Storage API (MDN)
- IndexedDB API (MDN)
- Storage quotas and eviction criteria (MDN)
- RenderingNG architecture — Chromium
- Avoid large, complex layouts and layout thrashing — web.dev
- Forced reflow — Chrome Performance Insights
- Rendering performance — web.dev
- Compositor-only properties and layer count — web.dev