
Astro 프로젝트의 콘텐츠를 관리하고 작성하는 방법.
전용 쿼리가 존재하는, 내부 콘텐츠 전용 DB같은 느낌으로 생각하면 될 듯!
src/content 디렉터리에 존재하는 특정 컨텐츠 전용 디렉터리들.
해당 용도로 예약되어 다른 파일을 넣을 수 없다! (하위 디렉터리 생성은 가능)

콘텐츠 컬렉션 디렉터리에 저장된 콘텐츠.
확장자로 md, json, yaml를 가질 수 있다.
-> Astro의 내장 콘텐츠 API를 사용하여 콘텐츠 쿼리를 사용할 수 있다!
git ignore에 해당 디렉터리를 추가할 것
콘텐츠 컬렉션을 위한 메타데이터가 저장됨.
자동 유지관리 및 업데이트되므로 수동으로 건드리지 말 것.
import { defineCollection } from 'astro:content';
const blogCollection = defineCollection({ /* ... */ }); //컬렉션 정의
export const collections = { // 컬렉션 등록
'blog': blogCollection,
};
{
"extends": "astro/tsconfigs/base",
"compilerOptions": {
"strictNullChecks": true
}
}
컬렉션에 대한 규칙 같은 것!
import { z, defineCollection } from 'astro:content';
const blogCollection = defineCollection({ //컬렉션 정의
type: 'content',
schema: z.object({ //컬렉션 스키마 정의
title: z.string(),
tags: z.array(z.string()),
image: z.string().optional(),
}),
});
export const collections = { //컬렉션 등록
'blog': blogCollection,
};
reference 함수를 사용하여 컬렉션 스키마의 속성을 다른 컬렉션의 항목으로 정의할 수 있다!
import { defineCollection, reference, z } from 'astro:content';
const authors = defineCollection({
type: 'data',
schema: z.object({
name: z.string(),
portfolio: z.string().url(),
})
});
const blog = defineCollection({
type: 'content',
schema: z.object({
title: z.string(),
author: reference('authors'), // `authors` 컬렉션에서 참조
relatedPosts: z.array(reference('blog')), // `blog` 컬렉션에서 참조
})
});
export const collections = { blog, authors };
컬렉션에서 항목을 쿼리하는 데 사용되는 속성
-> velog같은 곳에서 url맨 뒤에 붙는 경로명 같은 거!
-> slug를 활용해 동적 라우팅을 할 수 있다! = url친화적인 속성!
type: 'content'를 사용하면 모든 콘텐츠 항목은 해당 파일 id에서 slug 속성을 생성한다.
아래처럼 slug를 재정의할수도 있음!
---
title: 내 블로그 게시물
slug: my-custom-slug/supports/slashes
---
첫인상은 일단 firebase같은 느낌.
import { getCollection, getEntry } from 'astro:content';
const allBlogPosts = await getCollection('blog'); //해당 컬렉션의 모든 항목을 가져옴
const graceHopperProfile = await getEntry('authors', 'grace-hopper'); //해당 컬렉션의 해당 항목을 가져옴
object처럼 접근하여 특정 값 참조 가능
---
import { getEntry, getEntries } from 'astro:content';
const blogPost = await getEntry('blog', 'welcome');
const author = await getEntry(blogPost.data.author); // 단일 참조
const relatedPosts = await getEntries(blogPost.data.relatedPosts); // 배열 참조
---
array.filter처럼 쓸 수 있다.
import { getCollection } from 'astro:content';
const publishedBlogEntries = await getCollection('blog', ({ data }) => {
return data.draft !== true;
return data.startsWith('en/'); // 컬렉션 내 중첩된 디렉터리를 기준으로 한 필터링
return import.meta.env.PROD && data //이렇게 하면 개발 서버에서만 보이는 부분 생성 가능
콘텐츠 컬렉션은 src/pages/ 디렉터리 외부에 저장 = 경로생성x
-> 컬렉션 항목에서 HTML 페이지를 생성하려면 새 동적 경로 수동 생성 필요
동적 경로는 들어오는 요청 매개변수(예: src/pages/blog/[...slug].astro 파일의 Astro.params.slug)를 매핑하여 컬렉션의 올바른 항목을 가져옵니다.
getStaticPaths()함수를 사용하여 빌드하는 동안 src/pages에서 페이지 생성 가능
---
import { getCollection } from 'astro:content';
export async function getStaticPaths() {
const blogEntries = await getCollection('blog');
//blogEntries의 모든 목록에 대한 새 페이지가 생성된다.
return blogEntries.map(entry => ({
params: { slug: entry.slug },
props: { entry },
}));
}
const { entry } = Astro.props;
const { Content } = await entry.render();
---
<h1>{entry.data.title}</h1>
<Content />
경로 사전생성이 불가능하므로 필요할 때마다 해당 슬러그를 컬렉션에서 찾아 생성해야 함
---
import { getEntry } from "astro:content";
const { slug } = Astro.params; //요청을 우선 받아와야 함
if (slug === undefined) throw new Error("슬러그가 필요합니다");
const entry = await getEntry("blog", slug);
if (entry === undefined) return Astro.redirect("/404");
const { Content } = await entry.render();
---
<Layout>
{Content}
</Layout>
새로고침 없이 페이지 콘텐츠 업데이트가 가능한 탐색 방법
-> 기본적으로 모든 페이지는 일반 전체 페이지 브라우저 탐색으로 세팅되어 있음!
-> 필요한 곳에만 View Transitions사용 가능
head에 추가되는 경량 클라이언트 라우팅 컴포넌트.
탐색을 가로채 페이지 간 전환을 사용자 정의할 수 있다!
-> 모든 페이지에 적용을 원하면 레이아웃과 같은 공통 재사용 컴포넌트에 추가하면 됨.
---
import { ViewTransitions } from 'astro:transitions';
---
<html lang="ko">
<head>
<title>내 홈페이지</title>
<ViewTransitions />
</head>
<body>
<h1>나의 웹사이트에 오신 것을 환영합니다!</h1>
</body>
</html>
Astro는 페이지 이동시 직전 페이지와 현재 페이지에 같은 view-transition-name을 자동 할당
-> 이 이름을 통해 DOM에서의 해당 위치를 추론할 수 있다!
Astro가 자동 할당하는 전환 이름을 재설정할 수 있다.
//src/pages/old-page.astro
<aside transition:name="hero">
//src/pages/new-page.astro
<aside transition:name="hero">
페이지 이동시 컴포넌트 및 HTML 요소를 유지한다.
-> 동일한 비디오 요소가 포함된 다른 페이지로 이동하면 다음 video 태그가 계속 재생된다!
-> 뒤로가기, 앞으로가기에도 적용됨!
<video controls="" autoplay="" transition:persist> //태그에 직접 명시하면 된다
<source src="https://ia804502.us.archive.org/33/items/GoldenGa1939_3/GoldenGa1939_3_512kb.mp4" type="video/mp4">
</video>
<Counter client:load transition:persist initialCount={5} /> //컴포넌트에 명시할 수도 있음.
다른 컴포넌트에 있는 경우 수동으로 식별자 지정 가능
//src/pages/old-page.astro
<Video controls="" autoplay="" transition:persist="media-player"/>
//src/pages/new-page.astro
<MyVideo controls="" autoplay="" transition:persist="media-player"/>
props유지 여부. 기존 컴포넌트의 props를 유지한다.
---
import CommonHead from '../components/CommonHead.astro';
---
const anim = {
old: {
name: 'fadeIn',
duration: '0.2s',
easing: 'linear',
fillMode: 'forwards',
},
new: {
name: 'fadeOut',
duration: '0.3s',
easing: 'linear',
fillMode: 'backwards',
}
};
const myFade = {
forwards: anim,
backwards: anim,
};
---
<header transition:animate={myFade}> ... </header>
---
<html transition:animate="none"> //기본 설정을 비활성화하고
<head>
<CommonHead />
</head>
<body>
<Header/>
<Main1 transition:animate="slide"/> //바뀌는 부분만 애니메이션을 넣는다
<Main2 transition:animate={myFade}/> //컴포넌트화해서 넣을 수도 있음
<footer transition:animate={fade({ duration: '0.4s' })}/> //인라인 커스텀 가능
</body>
</html>
아래 사항들을 수신하여 ViewTransition으로 제어 가능!
data-astro-reload 속성을 추가하면 해당 링크로 이동할 때 새로고침이 보장된다!
-> 해당 새로고침은 라우터에서 무시되어 히스토리에 쌓이지 않음!
<a href="/articles/emperor-penguins" data-astro-reload/>
navigate를 사용하여 클라이언트측에서 라우팅 가능
-> history.back(), history.forward(), history.go() 등 사용 가능
//form.astro
<script>
import { navigate } from 'astro:transitions/client';
document.querySelector('select').onchange = (ev) => {
let href = ev.target.value;
navigate(href);
};
</script>
<select>
<option value="/play">시작</option>
<option value="/blog">블로그</option>
</select>
//index.astro
---
import Form from "../components/Form.astro";
import { ViewTransitions } from "astro:transitions";
---
<html>
<head>
<ViewTransitions />
</head>
<body>
<Form />
</body>
</html>
//form.jsx
import { navigate } from 'astro:transitions/client';
export default function Form() {
return (
<select onChange={(e) => navigate(e.target.value)}>
<option value="/play">시작</option>
<option value="/blog">블로그</option>
</select>
);
}
//index.astro
---
import Form from "../components/Form.jsx";
import { ViewTransitions } from "astro:transitions";
---
<html>
<head>
<ViewTransitions />
</head>
<body>
<Form client:load /> //해당 옵션을 추가하면 됨!
</body>
</html>
SSR에서 navigator에 사용하는 옵션 사용 가능
<a href="/main" data-astro-history="replace"> //push, replace, auto 사용 가능
크로스 브라우징을 위한 대체 옵션
animate (기본값, 권장) : 애니메이션o
swap: 애니메이션x. 이전 페이지를 즉시 새 페이지로 대체
none: 애니메이션x. 지원되지 않는 브라우저에서는 전체 페이지 탐색으로 대체
---
import { ViewTransitions } from 'astro:transitions';
---
<title>내 사이트</title>
<ViewTransitions fallback="swap" />
탐색 트리거
라우터가 다음 페이지를 가져옴
라우터가 'forward' or 'back' 값을 사용하여 HTML 요소에 data-astro-transition 속성 추가
document.startViewTransition 함수 호출
4번 콜백에서 라우터가 swap 수행
swap
- head태그 일부 교체
- 새 페이지에 스타일시트 DOM 노드 존재시 유지 -> FOUC 방지
- 새 페이지에 스크립트 존재시 유지
- transition:persist가 있는 헤드 요소 유지
- body태그 완전 교체
- transition:persist -> 새 DOM으로 이동 및 스크롤 위치 복원
- astro:after-swap 이벤트는 document에서 트리거
새 스타일시트, 새 스크립트 대기 및 실행
astro:page-load 이벤트 발생
FOUC(Flash of Unstyled Content)
페이지가 로드되는 동안 스타일 시트가 로드되지 않아 콘텐츠가 일시적으로 스타일이 없는 상태로 표시되는 현상
이해가 안 되는데...
새 페이지에 스타일시트가 존재시 유지한다는게 무슨 말이지?
헤더 dom노드가 사라질 일이 뭐가 있어서 유지를 해..?
그럼 해당 과정이 없으면 스타일시트 dom 노드가 사라진다는 거?? 그럼 fouc는 고사하고 아예 스타일도 못 적용하잖아
번들 모듈 스크립트(astro default)는 한 번만 실행된다!
-> 초기 실행 후에는 뒤로가기, 새로고침 등으로 돌아갔을 때 실행되지 않음..
인라인 스크립트는 같은 상황에서 해당 속성을 추가하여 재실행 가능
-> Astro에 의해 처리되지 않는 스크립트에만 사용 가능
<script is:inline data-astro-rerun>
if (!window.SomeGlobal) {
//수명주기가 없으므로, 한번만 체크되어야 하는 경우 window 전역객체에 플래그를 추가해서 확인
window.SomeGlobal = {} // ....
}
</script>
ViewTransition 라우터가 탐색 중 document에 여러 이벤트를 발생시킴
-> 이벤트가 탐색 수명 주기에 후크 제공
-> 해당 후크로 현재 수명주기단계 파악 가능
수명 주기 단계
- 준비 단계
- DOM swap 단계: 이전 페이지 콘텐츠를 새 페이지 콘텐츠로 대체
- 완료 단계: 로딩 완료후 스크립트 실행 및 정리 작업 수행
탐색 시작 후 ~ 새 콘텐츠 로드 전
<script is:inline>
document.addEventListener('astro:before-preparation', ev => {
const originalLoader = ev.loader; //기존 콘텐츠 로더
ev.loader = async () => { //이렇게 하면 비동기 함수 적용 가능
const { startSpinner } = await import('./spinner.js'); //수행할 작업 가져오기
await originalLoader(); //기존 로더를 실행하고..
startSpinner(); //하려고 했던 작업을 수행.
};
});
</script>
새 콘텐츠 로드 후 ~ 준비 단계 끝
<script is:inline>
document.addEventListener('astro:before-preparation', () => {
document.querySelector('#loading').classList.add('show');
});
document.addEventListener('astro:after-preparation', () => {
document.querySelector('#loading').classList.remove('show');
});
</script>
콘텐츠 swap 전
-> 현재 화면은 이전 페이지의 스냅샷.
<script is:inline>
function setDarkMode(document) {
let theme = localStorage.darkMode ? 'dark' : 'light';
document.documentElement.dataset.theme = theme;
}
//서버의 느낌이 낭낭하게 나는군요....
setDarkMode(document);
document.addEventListener('astro:before-swap', ev => {
//기본 swap. 헤드 콘텐츠 비교 후 이전 문서에서 유지되는 내용들을 newDocument로 옮기고 body와 치환
setDarkMode(ev.newDocument);
});
document.addEventListener('astro:before-swap', ev => {
//커스텀 swap. 스왑시 비교 구문을 직접 만들 수 있다.
ev.swap = () => {
diff(document, ev.newDocument);
};
});
</script>
콘텐츠 swap 후 ~ 새 페이지 렌더링 및 스크립트가 실행 전
document.addEventListener('astro:after-swap',
//스크롤 복원을 하자
() => window.scrollTo({ left: 0, top: 0, behavior: 'instant' });
)
새 페이지 로딩 완료 ~ 페이지 탐색 종료
<script>
document.addEventListener('astro:page-load', () => {
// 한번만 실행됨. useEffect(()=>{},[]) 같은 느낌..
setupStuff();
}, { once: true });
</script>
링크를 누르기 전에 스크립트가 백그라운드에서 페이지를 가져와둔다.
-> MPA의 페이지 탐색에서 즉각적으로 반응한다!
-> 단, 사이트 내 링크에 대해서만 작동하며 외부 링크에는 작동하지 않음.
//astro.cofig.mjs
import { defineConfig } from 'astro/config';
export default defineConfig({
prefetch: true
});
-> 단, 링크 위에 빠르게 마우스를 올리거나 스크롤할때는 적용되지 않음!
<a href="/about" data-astro-prefetch="tap">소개</a>
프리페칭 성능
각 전략은 필요할 때만 미리 가져오고 사용자의 대역폭을 절약하도록 미세 조정된다!
- 데이터 절약 모드/느린 연결속도에는 tap으로 대체됨
- 옵션으로 해당 기능 무시 가능: prefetch('/about', { ignoreSlowConnection: true })
- viewport, load일 때는 네트워크 막힘을 방지하기 위해 프리페칭의 우선순위를 낮게 유지
브라우저 API이므로 클라이언트 측 스크립트에서만 사용 가능!
<button id="btn">클릭</button>
<script>
import { prefetch } from 'astro:prefetch';
const btn = document.getElementById('btn');
btn.addEventListener('click', () => {prefetch('/about');});
</script>
prefetch('/about', { with: 'fetch' }); //언제나 최신이므로 우선순위 높음
prefetch('/about', { with: 'link' }); //캐싱되어있을 수 있으니 우선순위 낮음
기본적으로 View Transitions 사용시 프리패치도 함께 활성화
astro.config.mjs에서 설정 가능. 설정값은 필요할 때 찾아보기..