Astro 아일랜드 아키텍처 — 필요한 곳에만 자바스크립트를 보내기

블로그·웹 만들기 2026-07-19 00:01:16 astroislandperformancessg

글 목록 →

블로그 글 한 편을 띄우는 데 얼마나 많은 자바스크립트가 필요할까? 정직하게 답하면 0바이트다. 제목, 본문, 링크 — 전부 HTML이 원래 할 줄 아는 일이다. 그런데도 흔한 SPA 프레임워크로 만든 블로그는 수백 KB의 번들을 내려받고, 그걸 파싱하고, 서버가 이미 그려준 HTML 위에 컴포넌트 트리를 통째로 다시 세운다. 이 과정을 하이드레이션(hydration)이라 부른다. 정적인 글 위에서 하이드레이션은 순수한 낭비다.

Astro의 아일랜드 아키텍처(Islands Architecture) 는 이 낭비를 정면으로 겨냥한다. 페이지 전체를 되살리는 대신, 상호작용이 실제로 필요한 조각만 골라서 되살린다. 정적인 HTML 바다 위에 떠 있는 몇 개의 섬 — 이름 그대로다.

기본값이 “자바스크립트 없음”

Astro의 출발점은 다른 프레임워크와 반대다. 대부분의 프레임워크는 “전부 인터랙티브”에서 시작해 필요 없는 걸 덜어내려 애쓴다. Astro는 “전부 정적”에서 시작해 필요한 것만 더한다.

.astro 컴포넌트는 빌드 타임에만 실행된다. 아래 코드의 posts를 가져오는 로직은 서버(빌드 머신)에서 한 번 돌고, 결과 HTML만 남는다. 클라이언트로 가는 자바스크립트는 없다.

---
import { getCollection } from "astro:content";
const posts = await getCollection("blog");
---

<ul>
  {posts.map((post) => <li>{post.data.title}</li>)}
</ul>

브라우저는 완성된 <ul>을 받는다. 이 목록을 “되살릴” 이유가 없다. 클릭할 것도, 상태도 없기 때문이다.

섬을 띄우는 순간 — client:* 지시어

그런데 페이지 어딘가에는 진짜 상호작용이 필요한 조각이 있다. 다크모드 토글, 검색창, 이미지 캐러셀 같은 것들. 이때 React·Vue·Svelte 컴포넌트를 가져와 client:* 지시어를 붙이면, 그 컴포넌트만 섬이 된다.

---
import ThemeToggle from "../components/ThemeToggle.tsx";
import Search from "../components/Search.tsx";
---

<header>
  <h1>내 블로그</h1>
  <ThemeToggle client:load />
</header>

<main>
  <Search client:visible />
  <slot />
</main>

여기서 벌어지는 일이 아일랜드 아키텍처의 핵심이다. 페이지의 나머지는 여전히 자바스크립트가 0인 HTML이고, ThemeToggleSearch만 각자 자기 몫의 번들을 갖는다. 두 섬은 독립적으로 하이드레이션된다. 검색창이 아직 로드되지 않아도 토글은 이미 동작한다. 하나가 에러로 죽어도 다른 하나는 멀쩡하다.

지시어를 안 붙이면 어떻게 될까? 그 React 컴포넌트는 빌드 타임에 HTML로 렌더되고 끝난다. 화면에는 보이지만 클릭해도 아무 일도 일어나지 않는다. 지시어가 곧 “이건 살아 있어야 한다”는 선언이다.

언제 되살릴지 고르기

client:*에는 여러 변종이 있고, 각각 하이드레이션 시점이 다르다. 이게 성능 튜닝의 실질적인 손잡이다.

  • client:load — 페이지 로드 즉시. 첫 화면에 보이고 바로 반응해야 하는 것에 쓴다. 헤더의 테마 토글처럼.
  • client:idle — 브라우저가 한가해지면(requestIdleCallback). 중요하지만 급하지 않은 것. 페이지 하단의 구독 폼 같은 것.
  • client:visible — 뷰포트에 들어올 때(IntersectionObserver). 스크롤해야 보이는 것들의 기본값으로 삼을 만하다. 사용자가 끝까지 안 내리면 그 번들은 영영 안 받는다.
  • client:media={"(max-width: 768px)"} — 미디어 쿼리가 맞을 때만. 모바일 전용 햄버거 메뉴를 데스크톱 사용자에게 보낼 이유가 없다.
  • client:only="react" — 서버 렌더를 아예 건너뛰고 클라이언트에서만 그린다. windowlocalStorage에 의존해 서버에서 렌더가 불가능한 컴포넌트용 탈출구다.

선택 기준은 단순하다. “사용자가 이걸 만지기 전에 준비되어 있으면 되는가?” 스크롤 한참 아래에 있는 위젯에 client:load를 붙이는 건, 아무도 안 볼 수도 있는 코드를 첫 화면 로딩과 경쟁시키는 일이다.

섬은 서로 격리되어 있다

여기서 초심자가 가장 자주 걸려 넘어진다. 각 섬은 자기만의 독립된 앱이다. React 섬 두 개를 나란히 놓아도 그 둘은 같은 React 트리에 있지 않다. 그래서 평소 쓰던 방법이 통하지 않는다.

  • React Context를 부모 .astro 파일에서 감쌀 수 없다. .astro는 React 트리가 아니기 때문이다.
  • 한 섬의 useState는 다른 섬이 볼 수 없다.
  • props는 부모 .astro에서 섬으로 내려보낼 수만 있고, 그 값은 직렬화 가능해야 한다. 함수는 넘길 수 없다.

섬 사이에 상태를 공유해야 한다면 프레임워크 바깥의 공유 저장소를 쓴다. Astro 문서가 권하는 방식은 nanostores 같은 프레임워크 중립 스토어다.

// stores/cart.ts
import { atom } from "nanostores";
export const itemCount = atom(0);
// 어느 섬에서든
import { useStore } from "@nanostores/react";
import { itemCount } from "../stores/cart";

export default function CartBadge() {
  const count = useStore(itemCount);
  return <span>{count}</span>;
}

스토어가 섬 바깥에 있으니 헤더의 배지 섬과 본문의 담기 버튼 섬이 같은 값을 본다. 정 간단한 경우라면 CustomEvent로 신호를 주고받아도 된다.

이 제약은 불편이 아니라 설계가 드러난 것이다. 섬이 격리되어 있다는 건 곧 각 섬의 번들이 독립적이라는 뜻이고, 그게 애초에 이 아키텍처가 가벼운 이유다.

그래서 무엇이 좋아지는가

효과는 지표로 바로 나타난다. 자바스크립트를 덜 보내면 파싱·컴파일 시간이 줄고, 메인 스레드가 덜 막히고, 상호작용 준비 시점(TTI)이 당겨진다. 특히 저사양 기기와 느린 회선에서 차이가 크다 — 번들 200KB는 최신 노트북에선 대수롭지 않지만 오래된 안드로이드 폰에선 체감되는 지연이다.

그리고 이 이득은 점진적으로 무너지지 않는다. 자바스크립트가 실패해도 콘텐츠는 이미 HTML로 거기 있다. 섬 하나가 죽어도 페이지가 백지가 되지 않는다.

어디에 맞고 어디에 안 맞나

만능은 아니다. 아일랜드 아키텍처는 콘텐츠가 중심이고 상호작용이 군데군데 있는 사이트에서 빛난다. 블로그, 문서 사이트, 마케팅 페이지, 커머스 상품 페이지. 정적인 부분이 넓을수록 이득이 크다.

반대로 화면 전체가 하나의 살아 있는 상태 기계인 앱 — 대시보드, 에디터, 실시간 협업 도구 — 은 결이 다르다. 그런 화면은 사실상 전부가 섬이라서, 섬을 나누는 오버헤드만 남고 얻는 게 없다. 그 경우엔 처음부터 SPA 프레임워크를 쓰는 게 맞다.

판단 기준을 한 문장으로 줄이면 이렇다. 페이지를 열었을 때 자바스크립트를 전부 꺼도 대부분이 쓸모 있게 남는가? 그렇다면 아일랜드가 맞는 그림이다.

정리

아일랜드 아키텍처는 새로운 기술이라기보다 기본값의 전환이다. “전부 인터랙티브”를 가정하고 덜어내는 대신, “전부 정적”을 가정하고 필요한 곳에만 client:*로 생명을 불어넣는다. 각 섬은 자기 번들과 자기 하이드레이션 시점을 갖고 독립적으로 살아나며, 그 사이의 넓은 바다는 끝까지 순수한 HTML로 남는다.

이 블로그도 같은 원리로 서 있다. 글 목록도 본문도 자바스크립트를 필요로 하지 않고, 실제로 한 줄도 보내지 않는다. 나중에 검색이나 댓글이 필요해지면 그때 섬 하나를 띄우면 된다 — 나머지 페이지는 지금과 똑같이 가벼운 채로.

댓글

이 글에 대한 의견은 아래 댓글로 남겨주세요 (GitHub 계정 필요). 로그인 없이 남기고 싶다면