Astro 5에서 7.1로 올리기 전 점검할 것: Content Layer, Node 22, Tailwind
Astro 5 기반 GitHub Pages 블로그를 7.1로 올릴 때 실제로 빌드를 막는 Content Collections, Node 버전, slug·render API와 Tailwind 전환 순서를 정리합니다.
Astro 7.1이 나왔다고 npx @astrojs/upgrade부터 실행하면, Restato 같은 오래된 Astro 5 블로그는 패키지보다 콘텐츠 API와 배포 환경에서 먼저 막힐 가능성이 높습니다. 현재 저장소를 기준으로 확인하면 핵심 작업은 네 가지입니다. GitHub Actions의 Node를 22.12 이상으로 올리고, 레거시 Content Collections를 Content Layer로 옮기고, post.slug와 post.render()를 새 API로 바꾸고, Tailwind 4 전환은 별도 작업으로 분리해야 합니다.
Astro는 2026년 6월 22일 Astro 7을 공개했고, 7월 16일에는 Astro 7.1을 발표했습니다. Rust 기반 컴파일러와 MDX 파이프라인, Vite 8, 대형 콘텐츠 컬렉션의 메모리 사용량을 낮추는 옵션이 추가됐습니다. 하지만 현재 Restato 저장소는 astro: ^5.0.0, Node 20, 레거시 콘텐츠 설정을 사용합니다. 업그레이드 효과보다 먼저 호환성 경계를 정리해야 합니다.
현재 저장소에서 바로 걸리는 항목
| 점검 대상 | 현재 상태 | Astro 6·7에서 필요한 변화 | 위험도 |
|---|---|---|---|
| Node.js | GitHub Actions에서 Node 20 | Node 22.12 이상 | 높음 |
| 콘텐츠 설정 | src/content/config.ts, type: 'content' | src/content.config.ts와 glob() loader | 높음 |
| 콘텐츠 API | post.slug, post.render() | post.id, render(post) | 높음 |
| Tailwind | Tailwind 3 + @astrojs/tailwind | 당장은 유지 가능, Tailwind 4는 별도 전환 | 중간 |
| Vite 설정 | 별도 Vite 플러그인 없음 | Vite 8 영향이 비교적 작음 | 낮음 |
Astro 6 업그레이드 가이드에서 가장 중요한 변경은 Node 22 요구사항과 레거시 Content Collections 제거입니다. Astro 7은 다시 Vite 8로 올라가지만, 일반적인 Astro 프로젝트는 커스텀 Vite 플러그인을 사용하지 않는 한 변경량이 비교적 작습니다.
1단계: 패키지보다 Node 버전을 먼저 맞춥니다
현재 배포 워크플로는 다음과 같이 Node 20을 사용합니다.
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: "20"
cache: npm
Astro 6부터는 Node 22.12.0 이상이 필요합니다. 로컬과 GitHub Actions 버전을 동시에 고정하는 편이 안전합니다.
# .nvmrc
22.12.0
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: "22.12.0"
cache: npm
이 변경은 Astro 버전을 올리기 전에 먼저 적용할 수 있습니다. 기존 Astro 5 빌드가 Node 22에서 통과하는지 확인하면, 이후 오류가 런타임 변경 때문인지 Astro 변경 때문인지 구분하기 쉬워집니다.
2단계: 레거시 Content Collections부터 제거합니다
현재 설정은 src/content/config.ts에 있고 type: 'content'를 사용합니다.
import { defineCollection, z } from 'astro:content';
const blog = defineCollection({
type: 'content',
schema: z.object({
title: z.string(),
description: z.string(),
date: z.date(),
tags: z.array(z.string()).default([]),
}),
});
Astro 6은 이 레거시 방식의 자동 호환을 제거했습니다. 파일을 프로젝트 루트의 src/content.config.ts로 옮기고, loader와 Zod import를 명시해야 합니다.
import { defineCollection } from 'astro:content';
import { glob } from 'astro/loaders';
import { z } from 'astro/zod';
const blog = defineCollection({
loader: glob({
pattern: '**/*.{md,mdx}',
base: './src/content/blog',
}),
schema: z.object({
title: z.string(),
description: z.string(),
date: z.coerce.date(),
tags: z.array(z.string()).default([]),
image: z.string().optional(),
draft: z.boolean().default(false),
knowledgeId: z.string().optional(),
verifiedAt: z.coerce.date().optional(),
updated: z.coerce.date().optional(),
}),
});
const projects = defineCollection({
loader: glob({
pattern: '**/*.{md,mdx}',
base: './src/content/projects',
}),
schema: z.object({
title: z.string(),
description: z.string(),
image: z.string().optional(),
url: z.string().optional(),
github: z.string().optional(),
tags: z.array(z.string()).default([]),
featured: z.boolean().default(false),
order: z.number().default(0),
}),
});
export const collections = { blog, projects };
여기서 z.date() 대신 z.coerce.date()를 사용한 이유는 MDX frontmatter의 날짜가 문자열로 읽힌 뒤 Date 객체로 변환되기 때문입니다. 실제 스키마를 기준으로 작성해야 한다는 원칙은 MDX frontmatter와 라이브 스키마를 맞춘 과정에서도 다뤘습니다.
3단계: slug와 render() 사용 위치를 모두 찾습니다
Content Layer로 옮기면 콘텐츠 엔트리의 식별자는 slug가 아니라 id가 됩니다. 또한 엔트리 메서드였던 post.render() 대신 astro:content의 render() 함수를 사용해야 합니다.
현재 블로그 상세 페이지의 핵심 코드는 다음 형태입니다.
const posts = await getCollection('blog');
return posts.map((post) => ({
params: { slug: post.slug },
props: { post },
}));
const { Content } = await post.render();
마이그레이션 후에는 다음처럼 바뀝니다.
import { getCollection, render } from 'astro:content';
const posts = await getCollection('blog');
return posts.map((post) => ({
params: { slug: post.id },
props: { post },
}));
const { Content } = await render(post);
RSS도 같은 영향을 받습니다.
items: posts.map((post) => ({
title: post.data.title,
pubDate: post.data.date,
description: post.data.description,
link: `/blog/${post.id}/`,
})),
Restato 저장소에서는 홈, 블로그 목록, 태그 목록, 상세 페이지, RSS가 post.slug를 사용합니다. 한 파일만 고친 뒤 빌드가 성공하길 기대하면 안 됩니다.
rg 'post\.slug|\.render\(\)' src
이 검색 결과를 0건으로 만든 뒤 빌드하는 것이 안전합니다.
4단계: Tailwind 4는 Astro 업그레이드와 분리합니다
현재 프로젝트는 Tailwind 3과 @astrojs/tailwind를 사용합니다. 이 통합은 deprecated 상태지만, Astro 공식 문서는 여전히 Tailwind 3의 레거시 지원 방법으로 안내합니다. 따라서 첫 번째 목표가 Astro 7.1 빌드 복구라면 Tailwind 3을 그대로 유지하는 편이 낫습니다.
한 번에 다음을 모두 바꾸면 오류 원인을 분리하기 어려워집니다.
- Astro 5 → 7
- Vite 5 계열 → Vite 8
- Content Collections → Content Layer
- Tailwind 3 → 4
- Astro Tailwind integration →
@tailwindcss/vite
권장 순서는 다음과 같습니다.
- Node 22와 Content Layer로 먼저 이동
- Astro 7.1 빌드와 화면 회귀 테스트
- 배포 확인
- 별도 브랜치에서 Tailwind 4로 전환
Tailwind 4 전환 단계에서는 @astrojs/tailwind를 제거하고 @tailwindcss/vite를 사용해야 합니다. Astro의 Styles and CSS 가이드가 현재 권장 구성을 설명합니다.
가장 안전한 실제 작업 순서
# 1. 별도 브랜치
git switch -c chore/astro-7-upgrade
# 2. Node 22에서 현재 상태 확인
node -v
npm ci
npm run build
# 3. Content Layer와 slug/render API를 먼저 수정
npm run build
# 4. Astro와 공식 integration 업그레이드
npx @astrojs/upgrade
# 5. 다시 검증
npm run build
npm test
업그레이드 CLI가 패키지 버전을 바꿔주더라도 애플리케이션 코드의 slug, render(), 콘텐츠 loader까지 자동으로 정확히 고쳐준다고 가정하면 안 됩니다. Git diff를 기능 단위로 나눠 검토해야 합니다.
Astro 7.1 기능은 빌드가 안정된 뒤 적용합니다
Astro 7.1의 deferRender는 콘텐츠가 많아져 빌드 중 메모리 사용량이 커질 때 유용합니다.
const blog = defineCollection({
loader: glob({
pattern: '**/*.{md,mdx}',
base: './src/content/blog',
deferRender: true,
}),
schema: blogSchema,
});
다만 MDX는 원래 지연 렌더링 방식으로 처리되는 부분이 있고, 작은 컬렉션에서는 빌드 캐시를 포기하는 비용이 더 클 수 있습니다. 메모리 문제가 실제로 확인되지 않았다면 바로 켜지 않는 편이 좋습니다.
experimental.collectionStorage: 'chunked' 역시 데이터 저장 파일이 커지는 대형 사이트를 위한 옵션입니다. 현재 단계에서는 안정적인 Content Layer 전환이 먼저입니다.
발행 전에 확인할 체크리스트
- 로컬과 GitHub Actions가 Node 22.12 이상을 사용한다.
-
src/content/config.ts가 제거되고src/content.config.ts가 존재한다. - 모든 컬렉션에 loader가 있다.
- Zod를
astro/zod에서 가져온다. -
post.slug를post.id로 바꿨다. -
post.render()를render(post)로 바꿨다. - RSS와 sitemap URL이 이전과 동일하다.
- 태그 페이지와 블로그 목록의 링크가 깨지지 않는다.
- Tailwind 3 화면이 그대로 렌더링된다.
-
npm run build와 테스트가 통과한다.
Astro 7.1의 성능 개선은 매력적이지만, 이 저장소에서 진짜 마이그레이션 작업은 패키지 업데이트가 아닙니다. Node, Content Layer, 엔트리 식별자와 렌더링 API를 먼저 바꾸는 것이 핵심입니다. 이 네 경계를 분리해서 처리하면 업그레이드는 큰 재작성보다 검증 가능한 여러 개의 작은 변경으로 바뀝니다.