2026년 10월 25일 AI coding agent로 aron.tw를 Next.js에서 SvelteKit으로 옮겼다. 그 버전을 배포한 뒤 다시 Astro로 이전했다. 독자에게 보이는 사이트는 거의 같았다. 검색, 분류, 언어 전환도 유지했다. 검색 페이지의 외부 JavaScript는 155.7 KB에서 15.7 KB로 줄었다.
유지하고 싶던 기능을 없애지 않으면서 브라우저가 내려받고 실행하는 코드를 크게 줄였다. 이번 작업에서 가장 분명한 성과다.
주로 글을 읽는 사이트가 실행 모델을 바꾸면 어떤 일을 덜 수 있고 무엇을 계속 맡아야 하는지 보고 싶었다. 당시 사이트는 5개 언어, 글 언어 버전 35개, 정적 페이지 약 60개였고 Cloudflare Pages에 배포했다. Pagefind 검색, 분류 필터, 복사 버튼, 테마·언어 메뉴, 홈의 인물 사진 슬라이더가 있었다.
각 인터랙션은 독립적으로 다룰 수 있었다. 검색 조건이 바뀐다고 글 페이지의 메뉴를 갱신할 필요가 없고, 다음 글로 넘어갈 때 작성 중인 폼을 유지할 필요도 없다. Astro를 시험하기에 중요한 조건이었다.
기존 동작 검증도 이어 갔다. Astro와 SvelteKit의 정적·인터랙션 화면 210쌍은 픽셀 차이가 없었다. 약 223개의 E2E 인수 기준은 유지하면서 프레임워크 내부 구현에 묶인 일부 사례만 바꿨다. 기능 보존 여부를 성능 수치와 별도로 확인할 수 있었다.
HTML이 도착한 뒤에도 브라우저는 무엇을 할까?
세 버전 모두 정적으로 배포하며 HTML을 보낸다. 차이는 그다음이다.
Next.js는 빌드 때 Server Components를 실행하고 HTML과 클라이언트 이동용 payload를 준비한다. Client Components에는 hydration이 필요하다. SvelteKit도 페이지를 hydrate하고 내부 이동을 client router에 맡긴다. 이 구조는 브라우저에서 컴포넌트 상태와 탐색을 이어 가게 한다. Next.js 정적 내보내기, SvelteKit 페이지 옵션
이 사이트의 Astro 버전은 빌드 때 글을 완성하고 메뉴와 필터를 일반 TypeScript 스크립트로 처리한다. 브라우저에서 React나 Svelte가 페이지를 맡지 않는다. 이동하면 다음 HTML 문서를 불러온다.
검색 페이지는 세 버전 모두 같은 Pagefind를 포함하므로 기능을 빼서 수치를 낮춘 비교가 아니다.
검색 페이지의 외부 JS 전송량
같은 검색 기능에서 Astro의 외부 JS가 더 적습니다. HTML 안의 스크립트는 별도입니다.
- Next.js155.7
- SvelteKit60.6
- Astro15.7
데이터 표 펼치기
| 프레임워크 | KB (brotli) |
|---|---|
| Next.js | 155.7 |
| SvelteKit | 60.6 |
| Astro | 15.7 |
로컬 Apple M3 Max, 390 px 모바일 viewport, CPU 4배 감속. Pagefind 포함. 첫 샘플의 외부 JS response body 크기이며 시간 중앙값이 아닙니다. HTML 인라인 스크립트 제외. KB = 1,024 bytes.
측정 발췌와 방법이번 측정에서 Astro의 일반 읽기 페이지는 외부 JS가 약 0.9–3.2 KB였다. 검색 페이지는 별도다. 다만 작은 스크립트 일부는 HTML 안에 들어가므로 외부 파일이 작다는 사실을 JavaScript가 없다는 뜻으로 읽으면 안 된다.
브라우저가 실행하는 작업도 줄었다. Chromium에서 390 px 모바일 화면, CPU 4배 감속으로 페이지마다 5회 측정한 중앙값은 다음 범위였다.
| 페이지별 중앙값의 범위 | Next.js | SvelteKit | Astro |
|---|---|---|---|
| Script 실행 시간 | 47–88 ms | 14–35 ms | 4–14 ms |
| JS heap | 3.1–4.2 MB | 1.6–2.6 MB | 0.8–0.9 MB |
이 범위는 서로 다른 페이지 사이의 차이이며 한 페이지의 오차 구간이 아니다. heap은 JavaScript 메모리만 가리킨다. 이 블로그에서는 전송량, 실행량, JS 메모리를 줄인 모두가 유용한 개선이다.
그런데 Lighthouse 점수는 거의 그대로였다. 별도의 Lighthouse 13.5.0 모바일 시뮬레이션에서 페이지마다 3회 중앙값을 구했을 때 FCP는 여전히 약 3–12초였다. 세 버전이 함께 불러오는 동기 글꼴 stylesheet 11개가 첫 렌더링 경로에 남아 있었다.
글자가 더 일찍 나타나게 하려면 다음에는 글꼴을 손보겠다. 프레임워크가 줄여 준 작업과 남은 병목이 분명해졌다. 같은 문제를 풀려고 다시 이전할 이유는 없다.
인터랙션은 다른 코드로 남는다
SvelteKit으로 옮기면서 두 가지 작업이 생겼다.
먼저 글 본문이 HTML에 있으면서 hydration payload에도 중복됐다. transport hook과 별도 출력 처리를 조합해 첫 로드의 중복을 제거하고 클라이언트 이동에 필요한 데이터는 남겼다. 기본 옵션 하나로 끝난 일이 아니라 이 프로젝트에서 추가로 구현한 처리다.
또 head의 우선순위가 높은 modulepreload 약 18개가 글꼴과 대역폭을 나눠 썼다. 당시 느린 4G 시뮬레이션에서는 FCP가 0.6–0.9초 늘었고 fetchpriority="low"로 조정한 뒤 이전 수준으로 돌아왔다. bundle이 작아져도 로딩 순서는 따로 살펴야 한다.
Astro는 그 hydration 상태를 복원하지 않으므로 본문 payload 중복을 처리할 필요가 없어졌다. 검색과 분류 필터는 여전히 작성해야 했다.
글 카드를 <template>에 미리 렌더링하고 조건이 바뀌면 스크립트가 해당 카드를 복제해 목록, 페이지 번호, URL을 갱신했다. 대화상자, 메뉴, 인물 사진 슬라이더도 각각 스크립트를 쓴다. 이전 완료 당시 인터랙션 코드는 TypeScript 모듈 8개, 860줄이었다.
이전 기록에는 컴포넌트 스크립트가 <template> 안에 출력돼 자동 실행되지 않았던 문제도 있다. 당시 메뉴 스크립트를 바깥에서 불러오도록 옮겨 이벤트를 연결했다. hydration이 없어도 스크립트가 어디서 실행되는지는 설계해야 한다.
지금은 각 인터랙션을 따로 이해할 수 있어서 이 코드를 유지할 만하다. 검색에 편집 기능이나 큰 공유 상태가 생겨 여러 영역이 함께 바뀐다면 순수 TypeScript를 고집하며 DOM을 수동 동기화하지는 않겠다. Astro에서도 필요한 곳에 Svelte 또는 React island를 둘 수 있다.
콜드 빌드는 빨라졌고, 웜 빌드에서는 차이가 줄었다
브라우저 밖에서는 콜드 빌드 시간이 5.81초에서 2.55초로 줄었다.
로컬 cold build 시간
로컬에서 번갈아 측정한 결과 Astro의 cold build는 기존 Next.js 구현보다 약 56% 짧았습니다.
- Next.js5.81
- SvelteKit3.88
- Astro2.55
데이터 표 펼치기
| 프레임워크 | 초 |
|---|---|
| Next.js | 5.81 |
| SvelteKit | 3.88 |
| Astro | 2.55 |
Apple M3 Max, 번갈아 실행한 각 3회 중앙값. 프레임워크 및 출력 캐시는 비우고 공통 OG 이미지 캐시는 유지했습니다. 콘텐츠 처리와 Pagefind를 포함합니다. 웜 빌드 결과는 이어지는 본문에서 비교합니다.
측정 발췌와 방법콘텐츠 처리와 Pagefind를 포함한 시간이며 프레임워크 컴파일만 측정한 수치는 아니다. 이어서 다시 빌드하면 Next.js의 영구 캐시가 차이를 줄인다. 웜 빌드는 Next.js 2.98초, SvelteKit 4.07초, Astro 2.63초였다. 콜드 빌드의 개선을 모든 조건에서 두 배 빠르다는 주장으로 넓힐 수는 없다.
패키지 수나 코드 줄 수로도 순위를 매기지 않겠다. 프레임워크와 직접 작성한 코드에 배분한 일이 다르므로 줄 수를 작업 시간으로 환산할 수 없다. 860줄을 계속 관리할 가치가 있는지는 후속 변경에서 드러날 것이다.
페이지 전환 수치의 해석을 바로잡기
로컬에서 블로그 목록의 글을 클릭했을 때 걸린 시간은 다음과 같다.
별도의 운영 사이트 기록은 SvelteKit 373 ms, Astro 768 ms로 각각 3회 중앙값이었다. 하지만 배포 시점, CDN과 네트워크 상태, 기다린 생명주기가 달랐다. Playwright의 locator.click()은 자신이 일으킨 탐색을 기다린다. 전체 문서 로드와 client router의 화면 갱신을 동일한 종점으로 볼 수 없다.
원래 기록은 리소스 이벤트 약 50건을 많은 304 재검증으로 해석하기도 했다. 이 해석은 수정해야 한다. requestfinished에는 browser cache에서 가져온 리소스도 잡힌다. 개별 응답과 캐시 출처 없이 이벤트 수만으로 네트워크 왕복 횟수를 계산할 수 없다.
2026년 10월 25일 저장한 운영 사이트 확인 결과, HTML은 max-age=0, must-revalidate, 글꼴과 표본 이미지는 4시간 freshness, hash가 붙은 /_astro/ CSS·JS는 1년 immutable이었다. 4시간 안에 여전히 fresh한 글꼴이 must-revalidate 때문에 매번 네트워크 검증을 받는 것은 아니다. 그 지시어는 만료 후 재사용을 제한한다. HTTP 캐시 명세
전체 문서 탐색은 HTML을 다시 파싱하고 DOM을 만든다. 그래도 CSS, 글꼴, 이미지는 브라우저 캐시에서 가져올 수 있다. CDN hit는 또 다르다. 응답이 사용자 가까이에 있을 뿐 브라우저의 요청은 발생한다. 이들을 나눠 봐야 지연 원인을 찾을 수 있다.
사이트에는 hover prefetch도 켜 두었다. Astro 7.4.2는 지원 환경에서 <link rel="prefetch">로 다음 HTML을 미리 가져온다. 다음 페이지의 모든 리소스와 인터랙션까지 준비된다는 뜻은 아니다. 당시 300 ms hover 테스트에서 시간이 개선되지 않았다는 결과만으로 prefetch가 쓸모없다고 판단할 수도 없다.
다음 비교에서는 차갑고 따뜻한 browser cache 조건을 고정하고, 미리 가져오기가 끝났는지 확인한 뒤, 동일한 콘텐츠 표시 시점까지 측정하겠다. 기존 자료로 JS와 빌드 개선은 확인됐다. 페이지 전환은 조건을 맞춘 별도 실험이 필요하다.
이 블로그에서는 Astro를 유지하겠다
지금 사이트는 읽기가 중심이며 인터랙션을 부분별로 나눌 수 있다. 빌드 때 페이지를 완성하고 필요한 컨트롤만 브라우저에 맡기는 방식이 유지하려는 사이트와 잘 맞는다.
페이지 사이에 많은 작업 상태를 이어 가거나 여러 영역이 자주 함께 바뀌는 프로젝트라면 Svelte의 반응형 컴포넌트와 클라이언트 이동에 구체적인 용도가 있다. SvelteKit도 csr=false로 hydration과 router를 끌 수 있지만 기존 Svelte 인터랙션은 다른 방식으로 준비해야 한다.
Next.js 프로젝트라면 먼저 client boundary와 라우트 분할을 살펴볼 수 있다. 이번에는 Server Actions나 streaming을 측정하지 않았고, 프레임워크 교체가 기존 구현 개선보다 경제적이라고 증명하지도 않았다. AI와 기존 테스트 덕분에 나는 실험할 수 있었지만 모든 팀에 같은 투자가 맞는 것은 아니다.
aron.tw의 선택은 충분히 분명해졌다. Astro를 남기고 작은 인터랙션들을 관리하며, 여전히 글자 표시를 막는 글꼴을 개선한다. 프레임워크 선택은 여기서 멈추고 사이트의 다음 일을 할 생각이다.
