본문 바로가기
Frontend2026년 9월 7일17분 읽기

아일랜드 아키텍처와 부분 하이드레이션 — Astro·Qwik으로 자바스크립트 줄이기

YS
김영삼
조회 7
아일랜드 아키텍처와 부분 하이드레이션 — Astro·Qwik으로 자바스크립트 줄이기

아일랜드 아키텍처는 페이지를 정적 HTML로 먼저 보내고, 상호작용이 필요한 "섬"만 골라서 자바스크립트를 붙인다. 그래서 SPA처럼 페이지 전체를 하이드레이션하지 않고도 인터랙션을 얻는다.

핵심은 하이드레이션 비용을 줄이는 것이다. Astro는 필요한 컴포넌트만 client:* 지시자로 활성화하고, Qwik은 아예 하이드레이션을 하지 않고 이벤트가 발생한 순간에 코드를 이어받는(resumability) 방식으로 접근한다. 이 글은 둘의 차이와, 언제 SPA 대신 이걸 써야 하는지를 실무 관점에서 정리한다.

솔직히 말하면, 나는 몇 년간 리액트 SPA를 만들면서 "왜 정적 마케팅 페이지 하나 띄우는데 메인 번들이 200KB를 넘어야 하나"를 계속 의심했다. 사용자는 텍스트를 읽으러 왔는데, 브라우저는 그 텍스트를 이미 다 그려놓고도 자바스크립트를 다시 내려받아 처음부터 컴포넌트 트리를 재구성한다. 이 낭비를 정면으로 겨냥한 게 아일랜드 아키텍처와 resumability다.

하이드레이션이 대체 왜 비싼가

하이드레이션은 서버가 그려 보낸 정적 HTML 위에 자바스크립트가 다시 이벤트 리스너와 상태를 "덧입히는" 과정이다. 화면은 이미 보이는데, 클릭이 먹으려면 프레임워크가 컴포넌트 트리 전체를 브라우저에서 한 번 더 실행해야 한다. 이게 비싼 이유다 — 이미 한 일을 브라우저에서 반복한다.

전통적인 SSR + 하이드레이션(Next.js pages 라우터, Nuxt 등)의 순서는 이렇다. (1) 서버가 HTML을 렌더 → (2) 브라우저가 HTML을 그림(FCP) → (3) 프레임워크 런타임 + 페이지 컴포넌트 JS 다운로드 → (4) 그 JS를 파싱·실행 → (5) 트리 전체를 재실행하며 리스너 부착 → 그제서야 인터랙션 가능(TTI). 문제는 (3)~(5)가 페이지 크기에 비례해 커진다는 점이다. 컴포넌트가 500개면 500개를 다 하이드레이션한다. 화면 밖에 있든, 영원히 클릭 안 하든 상관없이.

특히 저사양 안드로이드에서 이게 체감된다. 파싱·실행은 CPU 바운드라, 회선이 빨라도 기기가 느리면 그대로 얻어맞는다. 나는 예전에 커머스 상세 페이지에서 하이드레이션 때문에 롱 태스크가 400ms씩 튀는 걸 프로파일러로 보고 나서야 이 문제를 진지하게 받아들였다.

참고 흔한 오해: "SSR을 쓰면 빠르다". SSR은 FCP(첫 화면)를 빠르게 할 뿐, TTI(상호작용 가능 시점)는 하이드레이션이 끝나야 온다. FCP와 TTI 사이의 간극 — 화면은 보이는데 버튼이 안 먹는 그 구간 — 이 바로 하이드레이션 세금이다.

아일랜드 아키텍처란 무엇인가

아일랜드 아키텍처는 페이지의 대부분을 상호작용 없는 정적 HTML(바다)로 두고, 인터랙션이 필요한 위젯만 독립적으로 하이드레이션되는 "섬"으로 취급하는 렌더링 모델이다. 용어 자체는 Etsy의 Katie Sylor-Miller가 처음 쓰고 Preact 저자 Jason Miller가 2020년 글로 대중화했다.

핵심 발상은 단순하다. 블로그 글에서 자바스크립트가 정말 필요한 부분이 어디인가? 본문 텍스트? 아니다. 헤더 로고? 아니다. 검색창, 좋아요 버튼, 이미지 캐러셀 정도다. 그렇다면 그 몇 개만 하이드레이션하고 나머지는 순수 HTML로 두면 된다. 각 섬은 서로 독립적이라, 하나가 늦게 로드돼도 다른 섬을 막지 않는다. 이걸 부분 하이드레이션(partial hydration)이라고도 부른다.

Astro가 이 모델의 대표 주자다. Astro는 기본적으로 컴포넌트를 0 JS로 렌더한다. 리액트 컴포넌트를 써도, 지시자를 명시하지 않으면 브라우저로 나가는 자바스크립트는 그 컴포넌트에 대해 0바이트다. 필요한 곳에만 "여기는 섬"이라고 선언하는 방식이다 — 옵트인이라는 게 중요하다.

접근법별 하이드레이션 비용 비교

각 렌더링 전략이 브라우저에 지우는 부담을 정리하면 이렇다. 숫자는 절대값이 아니라 "무엇이 페이지 크기에 비례해 커지는가"를 보는 게 핵심이다.

전략 초기 JS 하이드레이션 범위 TTI가 페이지 크기에 비례? 대표
CSR(순수 SPA)전체 번들전부(HTML도 JS가 그림)CRA, Vite SPA
SSR + 전체 하이드레이션런타임+전 페이지페이지 전체Next(pages), Nuxt2
스트리밍 SSR + 선택적 하이드레이션런타임+클라 컴포넌트클라이언트 컴포넌트부분적Next App(RSC)
아일랜드(부분 하이드레이션)섬만섬 단위, 지연 가능섬 개수에 비례Astro
Resumability최소 로더(~1KB대)없음(이어받기)아니오(O(1) 지향)Qwik

표에서 봐야 할 건 마지막 열이다. SPA와 전체 하이드레이션은 페이지가 복잡해질수록 TTI가 선형으로 나빠진다. 아일랜드는 "섬 몇 개"에만 비례하고, Qwik은 원칙적으로 페이지 복잡도와 무관하게 상수에 가깝게 가려는 게 목표다. 물론 현실은 데이터 페칭, 스타일 등 다른 변수가 끼어들지만, 설계 철학의 방향이 다르다.

Astro의 client:* 지시자 — 언제 하이드레이션할지 고르기

Astro에서 섬을 만드는 방법은 컴포넌트에 client:* 지시자를 붙이는 것뿐이다. 지시자가 없으면 그 컴포넌트는 서버에서 HTML로만 렌더되고 클라이언트 JS는 0이다. 지시자는 "언제" 하이드레이션할지를 정한다 — 이게 성능 튜닝의 핵심 손잡이다.

---
// src/pages/index.astro
import Counter from '../components/Counter.jsx';
import Search from '../components/Search.jsx';
import Carousel from '../components/Carousel.jsx';
import Newsletter from '../components/Newsletter.jsx';
---

<!-- 지시자 없음 = 0 JS, 순수 HTML로만 렌더 -->
<Counter />

<!-- 즉시 하이드레이션: 페이지 로드 직후 바로 (헤더 검색처럼 최우선) -->
<Search client:load />

<!-- 메인 스레드가 한가해지면 (급하지 않은 위젯) -->
<Carousel client:idle />

<!-- 뷰포트에 들어올 때만 (스크롤 내려야 보이는 폼) -->
<Newsletter client:visible />

지시자별 의미를 정리하면 이렇다.

  • client:load — 페이지 로드 즉시. 가장 비싸다. 진짜 즉시 반응이 필요한 것만.
  • client:idlerequestIdleCallback으로 메인 스레드가 한가할 때. 우선순위 낮은 인터랙션에 좋다.
  • client:visibleIntersectionObserver로 뷰포트 진입 시. 스크롤해야 보이는 무거운 컴포넌트(차트, 지도)에 최적. 개인적으로 제일 많이 쓴다.
  • client:media={"{"}"(max-width: 767px)"{"}"} — 특정 미디어쿼리일 때만. 모바일 전용 메뉴 같은 것.
  • client:only="react" — SSR을 건너뛰고 클라이언트에서만 렌더. 서버에서 못 도는 브라우저 전용 라이브러리(예: window에 의존)를 쓸 때. 대신 그 자리에 HTML이 안 나가니 SEO/CLS 주의.
주의 client:only는 서버 렌더 HTML이 아예 없다. 즉 JS가 로드되기 전까지 그 영역은 빈칸이라 레이아웃 시프트(CLS)가 생기기 쉽고, 검색엔진이 그 콘텐츠를 못 볼 수 있다. 나는 초보 때 상세 페이지 핵심 정보를 client:only 컴포넌트에 넣었다가 색인이 안 잡혀서 데인 적이 있다. 콘텐츠성 요소엔 절대 쓰지 말 것.

Qwik의 resumability — 하이드레이션을 아예 안 한다

Qwik은 접근이 근본적으로 다르다. 하이드레이션을 "더 잘"하는 게 아니라 아예 안 한다. 대신 resumability(재개 가능성)라는 개념을 쓴다 — 서버가 애플리케이션의 상태와 이벤트 핸들러의 위치까지 HTML에 직렬화해 넣고, 브라우저는 그걸 "이어받기"만 한다. 처음부터 다시 실행하는 재구성 단계가 없다.

차이를 비유하면 이렇다. 하이드레이션은 영화를 처음부터 다시 재생해 현재 장면까지 따라잡는 것이고, resumability는 일시정지된 지점에서 그냥 재생 버튼을 누르는 것이다. Qwik은 초기에 극소량의 로더(qwikloader, gzip 기준 1KB 대)만 내려보내고, 실제 컴포넌트 코드는 사용자가 그 요소와 상호작용하는 바로 그 순간에 조각(chunk) 단위로 가져온다.

// src/components/counter.tsx
import { component$, useSignal } from '@builder.io/qwik';

export const Counter = component$(() => {
  const count = useSignal(0);

  // onClick$ 의 $ 가 핵심.
  // 이 핸들러는 별도 청크로 분리되어,
  // 실제로 클릭이 일어나기 전까지 다운로드되지 않는다.
  return (
    <button onClick$={() => count.value++}>
      좋아요 {count.value}
    </button>
  );
});

여기서 $ 접미사가 Qwik의 마법이다. 옵티마이저가 $가 붙은 경계를 만나면 그 함수를 별도 파일(청크)로 뽑아낸다. 그래서 페이지를 처음 열 때는 그 핸들러 코드가 아예 네트워크를 타지 않는다. 클릭하는 순간에야 해당 청크를 가져온다. 이걸 "지연 실행(lazy execution)"이라 부르고, 이 세밀한 코드 쪼개기 덕에 초기 JS가 페이지 복잡도와 거의 무관해진다.

현실적인 우려도 있다. "그럼 클릭할 때마다 네트워크 지연 아니냐?" 맞는 지적이라 Qwik은 유휴 시간에 상호작용 가능성이 높은 청크를 서비스워커로 미리 프리페치한다. 이론은 근사한데, 나는 이게 캐시가 차가운 첫 상호작용에서 가끔 미세한 딜레이로 느껴질 때가 있었다. 대부분은 프리페치로 가려지지만, 완전 공짜는 아니라는 것.

Astro vs Qwik — 무엇을 언제

둘 다 "JS를 줄인다"는 목표는 같지만 겨냥하는 지점이 다르다. Astro는 콘텐츠 중심 사이트에서 인터랙션이 드물 때 강하고, Qwik은 인터랙션이 많은데도 초기 로드를 극단적으로 가볍게 하고 싶을 때 강하다.

항목 Astro Qwik
기본 철학아일랜드 / 부분 하이드레이션resumability / 하이드레이션 없음
기본 JS0(옵트인)~1KB대 로더
UI 프레임워크React/Vue/Svelte/Solid 혼용 가능Qwik 자체(qwikify$로 React 일부)
섬 간 상태 공유어렵다(섬은 격리됨, nanostores 등 필요)앱 전체가 하나라 자연스럽다
이상적 용도블로그·문서·마케팅·커머스 목록인터랙션 많은 앱형 사이트
생태계 성숙도넓음, 채택 많음상대적으로 젊음

개인적으로는 이렇게 나눈다. 콘텐츠가 주인공이고 인터랙션이 양념이면 Astro. 인터랙션이 페이지 전반에 깔려 있는데 그래도 첫 로드를 작게 유지해야 하면 Qwik. 그리고 솔직히, 팀에 리액트 사람만 있고 "앱"을 만든다면 굳이 이 둘로 안 가고 Next App Router로 클라이언트 컴포넌트를 최소화하는 게 현실적인 절충인 경우도 많다.

참고 Astro의 섬은 서로 격리돼 있어서, 헤더 섬과 사이드바 섬이 같은 상태를 공유하려면 별도 스토어(nanostores 같은 프레임워크 무관 스토어)가 필요하다. 이걸 모르고 리액트 Context로 두 섬을 묶으려다 "왜 상태가 안 공유되지" 하고 한참 헤매는 사람이 많다. 섬은 각각 독립 앱이라고 생각하면 된다.

언제 SPA를 그냥 쓰는 게 맞나

아일랜드나 resumability가 항상 정답은 아니다. 로그인 뒤에서만 도는 대시보드, 실시간 협업 툴, 캔버스 에디터처럼 화면 전체가 상호작용 덩어리이고 SEO가 무의미한 앱이라면 전통적 SPA(또는 SSR+하이드레이션)가 더 단순하고 생산적이다. 하이드레이션 세금이 문제 되는 건 "첫 방문·공개·저사양·콘텐츠 위주" 조합일 때다.

판단 기준을 몇 개로 압축하면 이렇다.

  1. 공개 페이지이고 SEO·첫 로드 속도가 매출/유입에 직결되는가 → 아일랜드/Qwik 유리.
  2. 페이지의 90%가 읽기용 콘텐츠이고 인터랙션이 국소적인가 → Astro.
  3. 로그인 뒤 앱이고 SEO가 필요 없으며 상태가 화면 전체에 얽혀 있는가 → SPA로 충분.
  4. 인터랙션은 많지만 초기 로드도 포기 못 하는가 → Qwik 또는 RSC(클라 컴포넌트 최소화).

한 가지 더. 도구를 바꾸는 비용도 성능이다. Lighthouse 점수 몇 점 올리자고 팀 전체가 익숙한 스택을 버리면, 개발 속도라는 다른 곳에서 세금을 낸다. 나는 "번들이 실제로 사용자 이탈을 만들고 있다"는 데이터가 있을 때만 아키텍처를 갈아엎으라고 권한다. 감이 아니라 필드 데이터(CrUX, RUM)로 결정할 문제다.

실무 도입 체크리스트

막상 도입할 때 걸리는 것들이 있다. 내가 겪은 순서대로.

  • 섬을 과하게 쪼개지 마라. 섬마다 프레임워크 런타임이 실릴 수 있어서, 리액트 섬 10개를 뿌리면 오히려 중복 비용이 난다. 인접한 인터랙션은 한 섬으로 묶는 게 낫다.
  • client:load를 남발하지 마라. 기본값을 client:visible이나 client:idle로 두고, 진짜 즉시 필요한 것만 load로 승격하라.
  • Qwik은 서드파티 React 라이브러리를 그대로 못 쓰는 경우가 있다. qwikify$로 감쌀 수 있지만 그 컴포넌트는 결국 하이드레이션되니, 무거운 걸 감싸면 이점이 반감된다.
  • 측정을 붙여라. WebPageTest나 Lighthouse의 TBT(Total Blocking Time), 그리고 실사용자 INP를 보라. "느낌"이 아니라 숫자로 개선을 확인해야 한다.

자주 묻는 질문

아일랜드 아키텍처와 부분 하이드레이션은 같은 말인가요?

거의 같은 맥락에서 쓰이지만 뉘앙스가 다릅니다. 부분 하이드레이션은 "페이지 일부만 하이드레이션한다"는 기법 전반을 가리키고, 아일랜드 아키텍처는 그 기법을 "독립적으로 하이드레이션되는 섬들"이라는 구체적 모델로 구조화한 것입니다. Astro의 구현이 아일랜드 모델의 대표 예입니다.

Qwik의 resumability는 하이드레이션과 뭐가 다른가요?

하이드레이션은 브라우저에서 컴포넌트 트리를 다시 실행해 리스너와 상태를 재구성하는 반면, resumability는 서버가 그 상태와 핸들러 위치를 HTML에 직렬화해 두고 브라우저가 재실행 없이 이어받습니다. 즉 하이드레이션의 "재구성" 단계 자체가 없어서, 초기 JS 실행량이 페이지 복잡도와 거의 무관해집니다.

client:load와 client:idle, client:visible 중 뭘 기본으로 쓰나요?

대개 client:visible을 기본으로 두는 걸 권합니다. 뷰포트에 들어올 때만 하이드레이션하므로 첫 화면 밖 컴포넌트가 초기 로드를 막지 않습니다. 헤더 검색처럼 처음부터 반드시 반응해야 하는 요소만 client:load로 올리고, 급하지 않은 것은 client:idle을 씁니다.

Astro에서 리액트 컴포넌트를 써도 자바스크립트가 안 나가나요?

네. client:* 지시자를 붙이지 않으면 리액트 컴포넌트라도 서버에서 HTML로만 렌더되고 클라이언트로 나가는 JS는 0바이트입니다. 지시자를 붙인 그 컴포넌트만 해당 프레임워크 런타임과 함께 번들에 포함됩니다.

SEO 관점에서 아일랜드 아키텍처가 유리한가요?

유리합니다. 콘텐츠가 서버에서 완전한 HTML로 나가기 때문에 크롤러가 JS 실행 없이 본문을 읽을 수 있고, 초기 로드가 가벼워 Core Web Vitals(LCP, INP)에도 좋습니다. 단 client:only로 렌더한 영역은 서버 HTML이 없으니 검색엔진이 못 볼 수 있어, 콘텐츠성 요소에는 쓰지 말아야 합니다.

인터랙션이 아주 많은 앱에도 Qwik이 맞나요?

Qwik은 오히려 인터랙션이 많으면서도 초기 로드를 작게 유지해야 할 때 강점이 큽니다. 다만 화면 전체가 상호작용 덩어리이고 SEO가 무의미한 로그인 뒤 대시보드라면, 팀 친숙도와 생태계를 고려해 전통적 SPA나 Next App Router가 더 실용적인 선택일 수 있습니다.

기존 Next.js 프로젝트를 Astro나 Qwik으로 꼭 옮겨야 하나요?

아니요. 마이그레이션 자체가 비용입니다. 실제 필드 데이터(CrUX, RUM)로 하이드레이션 비용이 이탈을 만든다는 근거가 있을 때만 검토하세요. 많은 경우 Next App Router에서 클라이언트 컴포넌트를 줄이고 서버 컴포넌트 비중을 높이는 것만으로도 상당한 개선이 가능합니다.

댓글 0

아직 댓글이 없습니다.
Ctrl+Enter로 등록