정적 콘텐츠에 React가 정말 필요할까? Astro로 블로그를 만든 이유
Frontend

정적 콘텐츠에 React가 정말 필요할까? Astro로 블로그를 만든 이유

MDN의 아키텍처 회고에서 시작해 Astro의 Island Architecture, Hydration, Server Islands, View Transitions, MDX까지. 정적 콘텐츠 중심의 블로그를 만들며 Astro를 선택한 이유와 내부 동작을 정리했다.

새로운 기술 블로그를 만들면서 가장 먼저 고민한 것은 어떤 프레임워크를 선택할지였다. React와 Next.js는 익숙한 선택지였지만, 만들고 싶은 것은 대시보드나 웹 애플리케이션이 아닌 콘텐츠 중심의 블로그였다. 그러다 보니 자연스럽게 React가 블로그에 얼마나 필요한지 고민하게 됐다. 비슷한 시기에 읽은 MDN의 프론트엔드 아키텍처 회고 글도 같은 문제를 다루고 있었다. 문서 사이트의 대부분은 정적인 콘텐츠였지만, 페이지 전체는 React 애플리케이션으로 동작하고 있었다. MDN은 이 구조를 다시 검토하기 시작했다. 내가 고민하던 것과 MDN이 마주한 문제는 크게 다르지 않았다.

“정적 콘텐츠를 보여주기 위해 이만큼의 JavaScript가 정말 필요할까?”

왜 MDN은 React를 버렸을까?

먼저 한 가지 짚고 넘어갈 점이 있다. React를 사용했다고 해서 MDN이 SPA였던 것은 아니다. MDN은 서버에서 HTML을 생성해 전달하는 방식을 사용했고, 검색 엔진도 완성된 HTML을 바로 읽을 수 있었다. React는 컴포넌트 재사용성과 개발 생산성을 제공하는 합리적인 선택지였다. 하지만 문서 사이트의 대부분이 정적인 콘텐츠라면 페이지 전체를 React 애플리케이션으로 구성하는 것이 과연 적절한지에 대한 의문도 남는다. 대부분의 페이지는 사용자가 콘텐츠를 읽기 위해 방문하는 문서 페이지였기 때문이다. MDN이 내린 결론은 단순했다. 대부분의 페이지가 정적 콘텐츠라면 페이지 전체를 React 애플리케이션처럼 취급할 필요는 없다는 것이다. 대신 JavaScript가 꼭 필요한 영역에만 동작을 부여하고, 나머지는 HTML 그대로 제공하는 방향으로 아키텍처를 재구성했다.

정적인 콘텐츠는 가능한 한 정적으로 제공한다. MDN은 이러한 철학을 구현하기 위해 Web Components 기반의 자체 아키텍처를 구축했다. Web Components는 브라우저가 제공하는 표준 기능으로, React 없이도 인터랙티브한 UI를 만들 수 있다. 예를 들어 React 컴포넌트 대신 <copy-button> 같은 커스텀 요소를 등록해 코드 복사 버튼을 구현할 수 있다.

<copy-button></copy-button>

<script>
class CopyButton extends HTMLElement {
  connectedCallback() {
    this.innerHTML = '<button>Copy</button>';
  }
}

customElements.define('copy-button', CopyButton);
</script>

문서 페이지 전체를 React로 하이드레이션하는 대신 검색창, 테마 토글, 코드 복사 버튼처럼 상호작용이 필요한 영역에만 JavaScript를 적용하는 방식이다. MDN과 같은 접근을 직접 구현하려면 컴포넌트 모델부터 렌더링 전략, 빌드 파이프라인, 클라이언트 JavaScript 로딩 방식까지 스스로 결정해야 한다. Mozilla 규모에서는 충분히 합리적인 선택일 수 있지만, 개인 블로그를 위해 감당하기에는 다소 무거운 접근이었다. 내가 찾고 있던 것은 MDN과 비슷한 철학을 기본값으로 제공하면서도, 콘텐츠 작성 자체에 집중할 수 있는 도구였다. 그래서 Astro를 검토하게 되었다.


Astro는 무엇이 다를까?

Astro를 선택한 가장 큰 이유는 새로운 기능 때문이 아니었다. 오히려 아무것도 하지 않았을 때의 기본값(default)이 내가 원하던 방향과 가장 가까웠다.

---
import CardGrid from '../../components/CardGrid.astro';
---

<CardGrid posts={posts} />

React에 익숙하다면 컴포넌트를 사용하는 순간 JavaScript가 필요할 것이라고 생각할 수 있다. 실제로 React 기반 프레임워크에서는 서버에서 HTML을 생성하더라도 브라우저가 컴포넌트 구조와 이벤트를 복원할 수 있도록 JavaScript가 함께 전송된다.

하지만 Astro에서는 다르다. 위 코드는 빌드 시점에 HTML로 변환되고, 브라우저에는 정적인 결과물만 전달된다. 인터랙션이 필요할 때만 JavaScript를 추가한다.

<CommentBox client:load />

즉, JavaScript는 기본값이 아니라 선택 사항이 된다. 그렇다면 Astro는 이런 구조를 실제로 어떻게 구현할까? 가장 먼저 이해해야 할 개념은 Island Architecture다.

Island Architecture

웹 페이지의 대부분은 정적인 HTML이고, 인터랙션이 필요한 부분은 일부에 불과하다. 블로그를 예로 들면 글 본문은 JavaScript가 필요 없지만, 댓글 입력창이나 좋아요 버튼은 사용자 입력을 처리해야 한다. Island Architecture는 이러한 전제를 바탕으로 페이지 전체를 하나의 애플리케이션으로 취급하는 대신, 인터랙션이 필요한 영역만 독립적인 “섬(Island)“으로 분리한다.

[ 정적 헤더 ]  ← JS 없음
[ 글 본문 ]    ← JS 없음
[ 💬 댓글창 ]  ← Island (JS 있음)
[ 정적 푸터 ]  ← JS 없음

Astro는 빌드 단계에서 client:* 디렉티브가 붙은 컴포넌트만 별도의 JavaScript 청크로 추출하고, 나머지는 순수 HTML로 생성한다.

<CommentBox client:load />
<LikeButton client:visible />

빌드 결과는 다음과 비슷하다.

/_astro/CommentBox.Abc123.js
/_astro/LikeButton.Def456.js

브라우저는 페이지 전체를 위한 거대한 번들을 내려받는 대신, 실제로 필요한 Island의 JavaScript만 요청한다. Astro도 React 컴포넌트를 사용할 수 있는데, 그렇다면 가상 DOM은 어떻게 동작하는 걸까? 기존 React 애플리케이션은 보통 하나의 루트 아래에서 동작한다. React는 이 트리 전체를 hydrate하고 관리한다.

<div id="root"></div>
#root
 ├─ Header
 ├─ Article
 ├─ CommentBox
 └─ Footer

반면 Astro는 페이지 전체를 React 애플리케이션으로 만들지 않는다.

HTML
 ├─ Header      ← 정적
 ├─ Article     ← 정적
 ├─ Footer      ← 정적
 └─ CommentBox  ← React Island

가상 DOM은 Island 내부에만 존재한다. 즉, 페이지 전체에 React 런타임을 올리는 것이 아니라 필요한 영역에만 React를 배치하는 구조다. 흥미로운 점은 이 방식이 React의 useEffect 최적화나 IntersectionObserver보다 한 단계 앞선다는 것이다. React에서는 컴포넌트가 이미 다운로드된 이후에 실행 시점을 제어한다면, Astro는 아예 JavaScript 다운로드 자체를 늦출 수 있다.

예를 들어 IntersectionObserver를 사용하려면 해당 컴포넌트 코드가 먼저 브라우저에 도착해야 한다. 반면 Astro의 client:visible은 컴포넌트가 화면에 들어오기 전까지 JavaScript 파일 자체를 요청하지 않는다. 즉, “언제 실행할 것인가?” 보다 먼저 “언제 내려받을 것인가?” 를 결정한다.

Server Islands

정적인 블로그라 해도 모든 정보가 정적인 것은 아니다. 예를 들어 로그인한 사용자 정보나 개인화된 추천 글처럼 사용자마다 달라지는 데이터가 있을 수 있다.

기존 SSR 방식은 이런 데이터를 위해 페이지 전체를 서버에서 렌더링한다. 하지만 대부분의 콘텐츠가 정적이라면 다소 아까운 선택일 수도 있다. Astro는 이를 위해 Server Islands라는 기능을 제공한다.

<UserProfile server:defer>
  <div slot="fallback">
    로딩 중...
  </div>
</UserProfile>

페이지 전체는 정적으로 생성된다.

제목
본문
이미지
푸터

하지만 server:defer가 붙은 영역만 별도로 서버에 요청된다.

정적 페이지

브라우저 렌더링

Server Island 요청

HTML 조각 교체

페이지 전체를 SSR로 전환하지 않으면서도 필요한 부분만 동적으로 렌더링할 수 있다. 블로그를 예로 들면 로그인 사용자 이름이나 “최근 읽은 글” 같은 개인화 영역을 구현할 때 유용하다. 정적인 페이지는 CDN 캐시를 그대로 활용하고, 동적인 정보만 서버에 맡기는 방식이다.

View Transitions

Astro는 MPA를 유지하면서도 SPA에 가까운 전환 경험을 제공한다. 브라우저는 이미 document.startViewTransition()이라는 네이티브 API를 제공하지만, 이 API는 같은 페이지 안에서 DOM이 변경될 때만 동작한다. Astro처럼 링크 클릭 시 새로운 HTML 문서를 요청하는 MPA 구조에서는 그대로 사용할 수 없다. 그래서 Astro는 <ViewTransitions /> 컴포넌트를 제공한다. 이 컴포넌트는 링크 클릭 순간에만 기본 동작을 가로채고, 다음 페이지 HTML을 미리 받아와 View Transition API와 연결해 준다.

<ViewTransitions />
링크 클릭
→ Astro가 기본 동작 가로챔 (preventDefault)
→ fetch()로 다음 페이지 HTML 요청
→ document.startViewTransition()
→ DOM 교체
→ history.pushState()

평소에는 MPA로 동작하지만 페이지 전환 순간에만 SPA처럼 동작한다. 페이지 전체를 클라이언트 라우터로 운영하지 않으면서도 부드러운 전환 효과를 얻을 수 있는 셈이다. 또한 view-transition-name 속성을 사용하면 서로 다른 페이지의 요소를 자연스럽게 연결할 수 있다.

.post-card img {
  view-transition-name: post-thumbnail;
}

.post-hero img {
  view-transition-name: post-thumbnail;
}

목록에서 상세 페이지로 이동하면 썸네일이 자연스럽게 확대되며 대표 이미지로 이어진다. 단순한 애니메이션 기능처럼 보이지만, MPA의 단점을 줄이면서도 Astro의 정적 중심 아키텍처를 유지할 수 있게 해주는 기능이다.


콘텐츠는 어떻게 관리할까?

여기까지는 Astro가 페이지를 어떻게 렌더링하는지에 대한 이야기였다. 하지만 블로그에서 렌더링 성능만큼 중요한 것이 콘텐츠 관리다. 글이 몇 개 없을 때는 Markdown 파일만 있어도 충분하지만, 글이 늘어나기 시작하면 메타데이터를 일관되게 관리하는 일이 생각보다 번거로워진다. Astro의 Content Collections는 이 문제를 해결하기 위해 등장했다.

const blog = defineCollection({
  schema: z.object({
    title: z.string(),
    pubDate: z.coerce.date(),
    tags: z.array(z.string()),
    featured: z.boolean().default(false),
  }),
});

스키마를 정의하면 Markdown의 Frontmatter도 검증 대상이 된다.

---
title: Astro Deep Dive
pubDate: 2025-06-01
tags:
  - astro
---

예를 들어 title을 빠뜨리거나 pubDate 형식을 잘못 작성하면 빌드 단계에서 바로 오류가 발생한다. 단순히 Markdown 파일을 읽어오는 수준이 아니라, 콘텐츠도 애플리케이션 데이터처럼 관리할 수 있는 셈이다. 실제로 글을 가져올 때도 타입 정보가 그대로 유지된다.

const posts = await getCollection('blog');
posts[0].data.title;
posts[0].data.pubDate;
posts[0].data.tags;

콘텐츠를 관리하다 보니 자연스럽게 MDX도 사용하게 됐다. MDX는 Markdown과 컴포넌트를 함께 사용할 수 있는 포맷이다. 흥미로운 점은 Markdown을 곧바로 HTML로 변환하는 것이 아니라, 중간에 AST(Abstract Syntax Tree)를 거친다는 것이다. 덕분에 변환 과정에 플러그인을 끼워 넣어 링크, 이미지, 코드 블록 같은 요소를 원하는 형태로 가공할 수 있다.

[구글](https://google.com)에서 검색을 한다.

먼저 remark가 Markdown을 mdast(Markdown AST)로 변환한다.

{
  "type": "paragraph",
  "children": [
    {
      "type": "link",
      "url": "https://google.com",
      "children": [
        {
          "type": "text",
          "value": "구글"
        }
      ]
    },
    {
      "type": "text",
      "value": "에서 검색을 한다."
    }
  ]
}

이 단계에서는 아직 <a><p> 같은 HTML 태그가 존재하지 않는다. link, text, paragraph 같은 Markdown의 의미만 표현된다. 그 다음 rehype가 이를 HTML AST(hast)로 변환한다.

{
  "type": "element",
  "tagName": "p",
  "children": [
    {
      "type": "element",
      "tagName": "a",
      "properties": {
        "href": "https://google.com"
      },
      "children": [
        {
          "type": "text",
          "value": "구글"
        }
      ]
    },
    {
      "type": "text",
      "value": "에서 검색을 한다."
    }
  ]
}

이 단계에서는 Markdown의 의미가 실제 HTML 구조로 변환된다.

Markdown

mdast (remark)

hast (rehype)

HTML

AST를 거치는 구조 덕분에 Markdown은 단순한 문서 포맷이 아니라 변형 가능한 데이터가 된다. 실제로 카카오페이 기술 블로그는 이 과정에 커스텀 플러그인을 추가해 Markdown 안의 비디오 문법을 <video> 태그로 변환한다. 문서를 작성하는 방식은 그대로 유지하면서도, 최종 결과물은 팀의 요구사항에 맞게 확장할 수 있는 것이다.

이미지는 어떻게 최적화할까?

Astro를 사용하면서 느낀 또 다른 특징은 가능한 많은 결정을 빌드 시점에 미리 내린다는 점이었다. 이미지 최적화도 같은 철학 위에 있다. Astro의 <Image /> 컴포넌트는 빌드 시점에 여러 크기의 이미지를 생성하고 srcset을 자동으로 구성한다.

<Image
  src={thumbnail}
  alt="thumbnail"
/>
<img
  srcset="
    thumbnail-400w.webp 400w,
    thumbnail-800w.webp 800w,
    thumbnail-1200w.webp 1200w
  "
>

과정은 대략 다음과 같다. 서버는 사용자의 화면 크기를 알 수 없다. 대신 빌드 단계에서 여러 크기의 이미지를 미리 만들어 두고, 브라우저가 자신의 뷰포트에 맞는 파일을 선택하게 한다. Astro는 여기서도 가능한 작업을 런타임이 아니라 빌드 타임에 처리한다. 사용자가 페이지에 접속한 뒤 최적화하는 것이 아니라, 배포 전에 대부분의 준비를 끝내 두는 방식이다.

원본 이미지

빌드 시점

400w
800w
1200w

브라우저가 선택

마치며

블로그를 만들기 전까지는 React와 Next.js가 사실상 기본 선택지라고 생각했다. 하지만 MDN의 아키텍처 회고를 읽고 Astro의 구조를 살펴보면서, 콘텐츠 중심의 사이트에서는 다른 접근도 충분히 가능하다는 것을 알게 됐다.

Astro가 특별한 기능을 제공해서 선택한 것은 아니다. 정적인 콘텐츠는 정적으로 제공하고, 필요한 곳에만 JavaScript를 사용한다. 내가 만들고 싶었던 블로그와 가장 잘 맞는 기본값이 Astro였다. 결국 블로그를 만드는 데 필요한 것들은 단순하다. HTML 몇 장과, 정말 필요한 만큼의 JavaScript.