정적 렌더링
Static Rendering
이전 섹션에서 서버사이드 렌더링에 대해서 알아보았을 때 서버 처리 시간이 늘어나는 만큼 TTFB 에 부정적인 영향을 주는 것을 알 수 있었다. 또 클라이언트 사이드 렌더링에서는 자바크스립트 번들 크기가 증가하면 스크립트를 다운로드 받아 처리하는 시간이 증가하게 되어 FCP, LCP, TTI 에 안 좋은 영향을 주었다.
Static Rendering(이하 SSG)는 사이트를 빌드할 때 미리 렌더링해 둔 HTML자체를 서빙하여 이런 문제를 해결한다.
사용자가 접속할 수 있는 각 라우팅 경로에 대응하는 HTML파일들이 미리 생성된다. 이런 HTML파일들은 클라이언트 측 요청에 대해 서버나 CDN에서 서빙될 수 있다.
이렇게 생성된 정적 HTML파일들은 캐시가 가능하므로 유연성을 제공한다. HTML파일들을 미리 만들어두고 사용하기 때문에 서버가 요청을 받았을 때의 처리 시간은 거의 무시할 수 있는 수준이며 결과적으로 빠른 TTFB와 좋은 성능을 보인다. 이상적으로는 클라이언트 측 자바스크립트가 최소화 되고 페이지를 다운로드 받자 마자 인터렉티브가 가능하게 된다. 따라서 SSG는 빠른 FCP/TTI를 달성할 수 있다.
SSG - 기본 구조
패턴의 이름과 같이 정적 렌더링은 로그인 된 사용자를 위한 UI변경 같은 것이 없는 정적 컨텐츠에 적합하다. 따라서 ‘소개’, ‘연락처’, 블로그 페이지 들이나 온라인 커머스 앱의 상품 페이지들은 정적 페이지가 될 수 있다. Next.js, Gatsby.js, VuePress와 같은 프레임웍들이 정적 컨텐츠 기능을 지원한다. Next.js를 사용하여 데이터가 필요하지 않은 페이지에 대해 정적 컨텐츠를 만드는 예제를 살펴보자.
Next.js:
// pages/about.js
export default function About() {
return (
<div>
<h1>About Us</h1>
{/* ... */}
</div>
)
}
사이트가 next build
명령을 통해 빌드될 때 이 페이지는 about.html
파일로 프리렌더 되어 /about
경로에 서빙된다.
데이터를 포함한 SSG
‘소개’ 페이지나 ‘연락처’ 페이지들은 데이터 스토어로부터 데이터를 불러올 필요가 없다. 하지만 각각의 블로그 포스트나 상품 페이지들은 빌드 시점에 데이터 스토어로부터 불러온 데이터와 HTML로 렌더링 될 템플릿들과 병합 되어야 한다.
만들어야 하는 HTML파일의 갯수는 작성한 블로그 포스트의 갯수나 상품의 갯수에 따라 다르다. 이런 페이지들이 얼마나 있는지 알기 위해 HTML로 만들어진 페이지에 해당하는 리스트 데이터가 필요하다. 이런 상황 역시 Next.js 로 처리가 가능하다. 이런 데이터를 토대로 페이지의 리스트나 개별 페이지를 만들수도 있다 아래 예시를 보자.
리스트 페이지
페이지 리스트를 만들기 위해 필요한 데이터가 서버의 외부에 있는 경우. 이 데이터들은 페이지를 생성하기 전에 미리 받아두어야 한다. Next.js 에서 이것은 페이지 컴포넌트에서 getStaticProps()
함수를 export하는것으로 구현할 수 있다. 이 함수는 서버에서 페이지를 만들기 전에 호출된다. 불러온 데이터는 pre-render될 페이지 컴포넌트의 props
로 제공된다. 아래 예제는 해당 기능을 구현하고 있다.
// This function runs at build time on the build server
export async function getStaticProps() {
return {
props: {
products: await getProductsFromDatabase(),
},
}
}
// The page component receives products prop from getStaticProps at build time
export default function Products({ products }) {
return (
<>
<h1>Products</h1>
<ul>
{products.map(product => (
<li key={product.id}>{product.name}</li>
))}
</ul>
</>
)
}
이 함수는 클라이언트 측 자바스크립트 번들을 포함하지 않고 있고. 그렇기 때문에 데이터베이스로부터 데이터를 직접 조회해 오는 데 사용할 수 있다.
개별 상세 페이지
위의 예시에서 나열된 각 페이지들에 대해 상세 페이지를 만들어야 한다. 이런 상세 페이지들은 리스트에서 각각 대응하는 항목의 링크를 클릭하여 라우팅하게 된다.
상품번호 101, 102, 103에 해당하는 상품이 있다고 가정해 보자. 각각의 상품 상세는 /products/101
, /products/102
, /products/103
으로 접근할 수 있다. 이를 위해 Next.js의 getStaticPath()
를 동적 라우팅과 조합하여 사용할 수 있다.
이를 위해 products/[id].js
페이지 컴포넌트를 만들고 getStaticPaths()
함수를 export 하도록 구현해야 한다. 이 함수는 빌드 타임에 각각의 상품 페이지를 pre-render할 때 필요한 상품번호 배열을 반환한다. 아래 예제는 이를 러프하게 구현한 코드로 완성된 예제는 여기에서 볼 수 있다.
// pages/products/[id].js
// In getStaticPaths(), you need to return the list of
// ids of product pages (/products/[id]) that you’d
// like to pre-render at build time. To do so,
// you can fetch all products from a database.
export async function getStaticPaths() {
const products = await getProductsFromDatabase()
const paths = products.map(product => ({
params: { id: product.id },
}))
// fallback: false means pages that don’t have the correct id will 404.
return { paths, fallback: false }
}
// params will contain the id for each generated page.
export async function getStaticProps({ params }) {
return {
props: {
product: await getProductFromDatabase(params.id),
},
}
}
export default function Product({ product }) {
// Render product
}
각 상품 상세 페이지에 필요한 데이터는 상품 번호를 인자로 전달한 getStaticProps
함수에 의해 빌드 타임에 만들어진다. getStaticPaths()
의 인자로 fallback: false
를 전달한것을 볼 수 있는데. 이는 경로 파라미터로 받은 상품번호에 해당하는 페이지가 없다면 404 에러 페이지를 보여주라는 뜻이다.
이렇게 SSG는 다양한 종류의 페이지를 pre-render할 수 있다.
SSG - 주요 고려사항
위에 언급한 것 처럼 SSG를 사용하면 클라이언트와 서버 양쪽의 처리량을 줄여주어 좋은 성능을 확보할 수 있다. 또 이렇게 만들어진 사이트는 따로 처리해주지 않아도 검색엔진에 최적화되어 있다. 검색 엔진 최적화와 좋은 성능 때문에 SSG는 꽤 괜찮은 렌더링 패턴이지만 SSG를 적용하기 전에 아래 사항들을 한번 확인해볼 필요가 있다.
- HTML파일이 아주 많은 경우: 개별 HTML파일들은 사용자가 요청하기 전에 미리 만들어져 있어야 한다. 블로그를 예로 든다면 모든 포스트에 대한 HTML파일이 데이터 스토어로부터 생성될 것이다. 최초 생성 이후 포스트가 수정되면 해당 포스트와 관련있는 파일들은 모두 다시 빌드되어야 한다. 대량의 HTML파일을 관리하는 게 까다로울 수 있다.
- 호스팅 의존성: SSG 사이트의 응답 속도를 위해 호스팅 플랫폼을 사용하는 것도 좋은 방법이다. 여러 CDN에서 컨텐츠가 서빙되도록 세팅하면 최고의 성능을 이끌어 낼 수 있다.
- 동적 컨텐츠: SSG 사이트는 컨텐츠가 변경될 때 마다 매번 새로 빌드해야 한다. 컨텐츠 변경 이후 배포하지 않으면 이전 컨텐츠가 계속 보여질 수 있다. 따라서 자주 변경되어야 하는 컨텐츠에 대해 SSG를 적용하는것은 적합하지 않다.