Astro와 아일랜드 아키텍처
Astro
Astro는 빠른 웹 사이트를 쉽게 만들어주는 웹 프레임워크입니다. 빠른 웹 사이트를 만드는 간단한 비결은 서버에서 다운받는 전송량을 줄이면 됩니다.
Astro는 React, Vue 와 같은 자바스크립트 라이브러리를 사용하는 기존의 웹 사이트 구축 방법과는 다른 접근 방식을 취합니다. Astro는 전체 페이지를 정적 HTML로 렌더링하여 최종 빌드에서는 모든 자바스크립트가 기본적으로 제거됩니다.
물론 인터랙티브한 컴포넌트가 필요하다면 React, Svelte, Solid.JS, 웹 컴포넌트와 같은 원하는 라이브러리를 사용할 수 있습니다.
이 글에서는 2023년 1월에 릴리즈된 Astro v2 를 기준으로 Astro 의 여러가지 특징 중 하나인 아일랜드 아키텍처Island Architecture에 대해서 알아보도록 하겠습니다.
아일랜드 아키텍처
페이지 렌더링에 많은 자바스크립트를 로드하고 처리하면 성능이 저하될 수 밖에 없습니다. 대부분의 정적인 웹 사이트에서도 인터랙티브한 컴포넌트가 필요하기 때문에 어느 정도의 자바스크립트는 필요한 경우가 대부분입니다.
SSR
SSR의 핵심 원칙은 HTML이 서버에서 렌더링되며, 하이드레이션을 위해 필요한 자바스크립트가 같이 클라이언트에 같이 제공됩니다. 이러한 하이드레이션에는 비용이 들기 때문에 SSR 기반의 방법들은 하이드레이션 프로세스를 최적화하는데 중점을 두고 있습니다.
프로그레시브 하이드레이션
프로그레시브 하이드레이션은 중요한 컴포넌트를 먼저 하이드레이션하며, 나머지 컴포넌트는 점진적으로 스트리밍을 통해 하이드레이션됩니다. 하지만, 클라이언트에 제공되는 자바스크립트는 여전히 동일하게 유지됩니다.
아일랜드 아키텍처
아일랜드 아키텍처라는 용어는 Katie Sylor-Miller 와 Jason Miller 에 의해 널리 알려지기 시작했습니다.
이는 정적 HTML 위에 독립적으로 전달할 수 있는 인터랙티브한 “아일랜드”를 통해 클라이언트에 전달되는 자바스크립트의 양을 줄이는 것을 목표로 합니다. 아일랜드는 컴포넌트 기반 아키텍처로, 페이지를 여러 개의 아일랜드로 영역으로 구분합니다. 페이지의 정적 영역은 순수 HTML로 구성되며 하이드레이션이 필요하지 않습니다. 동적 영역은 렌더링 후 자체적으로 하이드레이션이 가능한 HTML와 자바스크립의 조합이 됩니다.
동적 컴포넌트 아일랜드
대부분의 페이지는 정적 컨텐츠와 동적 컨텐츠로 이루어집니다. 일반적으로 페이지에는 정적 컨텐츠와, 분리된 영역의 인터랙티브한 동적 컴포넌트로 구성됩니다. 예를 들면:
- 블로그 게시물, 뉴스 기사, 회사 홈페이지 등에는 텍스트, 이미지 뿐만 아니라 소셜 미디어 공유 버튼, 채팅창 같은 인터랙티브 컴포넌트가 포함되어 있습니다.
- 쇼핑 사이트에서는 정적인 제품 설명들과 함께 이미지 캐러셀과 같은 인터랙티브한 컴포넌트가 여러 페이지에서 사용될 수 있습니다.
- 계좌의 거래 내역 조회 페이지에서 인터랙티브한 필터링을 제공하는 컴포넌트를 제공할 수 있습니다.
정적 컨텐츠는 상태를 가지지 않고, 이벤트를 실행하지 않기 때문에 하이드레이션이 필요없습니다. 렌더링 후에는 동적 컨텐츠에 이벤트 핸들러를 연결하고, 가상 DOM을 사용해 DOM 을 생성해야 합니다. 이러한 작업들은 클라이언트로 전송되는 자바스크립트에 의해 이루어집니다.
아일랜드 아키텍처에서는 서버에서 정적 컨텐츠를 렌더링하며, 렌더링된 HTML에는 동적 컨텐츠를 위한 플레이스홀더가 포함됩니다. 동적 컨텐츠 플레이스홀더에는 독립된 컴포넌트들이 포함되며, 서버에서 렌더링된 출력물을 클라이언트에서 하이드레이션하는데 필요한 최소한의 자바스크립트를 포함합니다.
프로그레시브 하이드레이션에서 페이지는 개별 컴포넌트의 스케줄링과 하이드레이션을 제어합니다. 각 컴포넌트는 페이지 내의 다른 컴포넌트와 독립적으로 비동기 실행되는 하이드레이션 스크립트가 있으며, 한 컴포넌트에 성능 문제가 발생하더라도 다른 컴포넌트에는 영향을 미치지 않아야 합니다.
아일랜드의 장점
- 성능: 대부분의 웹 사이트가 정적 HTML로 변환되고, 자바스크립트는 필요한 개별 컴포넌트에서만 로드되기 때문에 클라이언트에 전송되는 자바스크립트 코드의 양이 줄어듭니다. 전송되는 코드는 인터랙티브 컴포넌트에 필요한 스크립트로만 구성되며, 이는 전체 페이지의 가상 DOM을 다시 생서앟고 페이지의 모든 컴포넌트들을 하이드레이션하기 위해 필요한 스크립트보다 훨씬 적습니다.
- 병렬 로딩: 아래와 같은 페이지에서 우선 순위가 낮은 이미지 캐러셀 아일랜드는 우선 순위가 높은 헤더 아일랜드의 로딩을 차단할 필요없이 병렬로 로딩해서 동시에 하이드레이션되므로, 페이지 아래에 있는 이미지 캐러셀을 기다릴 필요없이 헤더가 바로 인터랙티브하게 동작할 수 있습니다. Core Web Vitals 의 TTI (Time to Interactive) 는 사용자가 페이지와 상호 작용이 가능한 시점까지 걸리는 시간을 측정하는 지표이며, 앞의 이유들로 인해 자동으로 높은 TTI 점수를 얻을 수 있습니다.
- SEO: 모든 정적 컨텐츠는 서버에서 렌더링되기 때문에, SEO 친화적입니다.
- 컴포넌트 우선순위: Astro에서는 각 컴포넌트를 렌더링하는 방법과 시기를 정확히 알려줄 수 있습니다. 이미지 캐러셀을 로드하는데 비용이 많이 드는 경우, 캐러셀이 페이지에 표시될 때만 캐러셀을 로드하도록 지시하는 특별한 클라이언트 디렉티브를 추가할 수 있습니다. 캐러셀이 화면 표시 영역에 없다면 로드되지 않습니다.
- 컴포넌트 기반: 재사용성, 유지보수성 등과 같은 컴포넌트 기반 아키텍처의 모든 장점을 제공합니다.
Astro 아일랜드 예제
여기에서는 solid.js 를 연동해서 astro 아일랜드 예제를 만들어 보겠습니다.
solid.js 연동
solid.js 를 연동하기 위해서는 내장된 astro add
CLI 도구를 사용합니다.
$ pnpm astro add solid
실행하게 되면 다음과 같은 파일들이 업데이트 됩니다.
astro.config.mjs
// ...
import solidJs from "@astrojs/solid-js";
export default defineConfig({
// ...
integrations: [solidJs(), /** ... */],
});
package.json
@astrojs/solid-js
추가solid-js
추가
tsconfig.json
{
"compilerOptions": {
"jsx": "preserve",
"jsxImportSource": "solid-js"
}
}
solid.js 컴포넌트 임포트
InputCounter
라는 solid.js 컴포넌트를 다음과 같이 작성합니다.
import { createSignal } from "solid-js";
export default function InputCounter() {
const [count, setCount] = createSignal(0);
const onDecrease = () => setCount(count() - 1);
const onIncrease = () => setCount(count() + 1);
const onInputChange = (s: string) => setCount(Number.parseInt(s));
return (
<div>
<button onClick={onDecrease}>-</button>
<input
type="text"
value={count()}
onChange={(e) => onInputChange(e.target.value)}
/>
<button onClick={onIncrease}>+</button>
</div>
);
}
다음과 같이 작성하게 되면 solid.js 컴포넌트를 임포트했더라도, 결과물은 자바스크립트를 포함하지 않는 순수 HTML 만 생성됩니다.
---
// 정적 solid.js 컴포넌트
import InputCounter from "../../components/solid/InputCounter";
---
<!-- 자바스크립트를 사용하지 않는 100% 순수 HTML 생성 -->
<InputCounter />
하지만 클라이언트 사이드에서 인터랙티브한 UI를 필요로 하는 경우, client:load
지시자를 사용해 SPA 자바스크립트 애플리케이션처럼 동적 아일랜드를 생성하도록 지시할 수 있습니다.
---
// 동적 solid.js 컴포넌트
import InputCounter from "../../components/solid/InputCounter";
---
<!-- 이 컴포넌트는 인터랙티브하게 동작하며,
페이지의 나머지 부분은 자바스크립트를 사용하지 않는 정적 웹사이트가 됩니다. -->
<InputCounter client:load />