이전에 작성했던 SEO에 대한 글에서 검색 엔진 최적화를 위해 할 수 있는 일 중 서버에서 HTML을 렌더링하여 클라이언트로 보내는 서버사이드렌더링을 사용하는 것이 있었다.
NextJs는 대표적인 서버사이드렌더링을 제공하는 프레임워크이다.
나는 이 글을 작성하며 NextJs가 서버에서 페이지에 필요한 데이터를 가져오고 그 데이터를 활용하여 HTML로 렌더링하는 것을 공부할 예정이다.
NextJs는 서버사이드렌더링만 제공하는 것이 아니다. Data Fetching에 대한 방법이 4가지나 존재한다. 우선 그 4가지 방법들에 대해 알아보자
NextJs 에서 Data Fetching 방법은 SSG, SSR, ISR, CSR로 총 네가지가 있다.
SSG는 빌드시간에만 페이지에 필요한 데이터를 가져오고 해당 데이터를 사용하여 HTML을 렌더링한다.
즉, 우리가 next build 명령어를 작성할 때만 페이지가 렌더링되는 것이다.
📌 하지만 개발모드인 경우에는 모든 페이지 request마다 페이지가 렌더링된다.
페이지 파일에 getStaticProps라는 함수를 사용해 페이지를 빌드 시간(build time)에 사전 렌더링(pre-render)할 수 있다.
export async function getStaticProps(context) {
return {
props: {},
}
}
오직 페이지 컴포넌트에서만 사용할 수 있으며 컴포넌트 내부가 아닌 컴포넌트 외부에 단독으로 사용해야한다.
위에서 언급했듯이 SSG는 페이지를 build 시간에만 렌더링한다. 따라서 getStaticProps 함수 또한 build시에만 실행된다.
getStaticProps는 빌드 시, 렌더링한 HTML 파일 뿐만 아니라 JSON 또한 생성한다.
생성된 JSON 파일은 이미 렌더링된 페이지로 이동할 때 페이지 컴포넌트의 props로 사용할 수 있다.
페이지 접근 시, 미리 생성된 JSON을 props로 사용할 뿐 getStaticProps가 호출되지 않는다.
또한 빌드 시 생성된 HTML 파일과 CDN 파일은 정적인 파일이기 때문에 CDN에 캐시하여 사용할 수 있다.
페이지를 빌드 시간에 미리 렌더링하고 또 이렇게 사전에 렌더링된 페이지를 CDN에 캐시할 수 있기 때문에 페이지 로드가 빠르다.
사전에 렌더링하기 때문에 페이지 소스에서 컨텐츠를 볼 수 있기 때문에 크롤러가 우리 페이지를 크롤링할 때 많은 컨텐츠를 제공할 수 있다.
웹 서버 외부에서 공격할 수 있는 것이 없다.
빌드 시, 가져온 데이터를 계속 사용하기 때문에 상호작용하면서 작동하고 항상 최신의 데이터가 필요한 페이지에서는 알맞지 않다.
그러나 요즘에는 이런 단점이 개선된 방식이 등장했다. ISR로 이 단점을 해결할 수 있는데 이는 뒤에 알아보도록 하자.
SSR은 페이지 요청이 들어왔을 때, 데이터를 가져오고 HTML을 사전 렌더링한다.
SSG와의 주요 차이점은 사전 렌더링을 진행하는 시점이다.
SSG는 빌드 시간에만 사전 렌더링하지만 SSR은 요청이 들어올 때 마다 사전 렌더링을 진행한다.
페이지 파일에 getStaticProps라는 함수를 사용해 페이지를 빌드 시간(build time)에 사전 렌더링(pre-render)할 수 있다.
export async function getServerSideProps(context) {
return {
props: {},
}
}
오직 페이지 컴포넌트에서만 사용할 수 있으며 컴포넌트 내부가 아닌 컴포넌트 외부에 단독으로 사용해야한다.
export async function getServerSideProps(context) {
return {
props: { message: `Next.js is awesome` },
...
}
}
JSON.stringify로 직렬화될 수 있는 객체를 props로 반환하면 해당 객체는 페이지 컴포넌트의 props로 사용할 수 있다.
NextJs는 .next/static 폴더에 있는 정적파일에 자동으로 캐싱 헤더를 추가한다.
그렇다면 동적파일은 캐싱할 수 없을까?
getServerSideProps 함수 내부의 캐시 헤더(Cache-Control)을 사용하면 동적 응답을 캐시할 수 있다.
export async function getServerSideProps({ req, res }) {
res.setHeader(
'Cache-Control',
'public, s-maxage=10, stale-while-revalidate=59'
)
return {
props: {},
}
}
s-maxage=10
10초 동안 fresh 상태로 간주된다.stale-while-revalidate=59
59초 동안 stale 상태의 값이 유지된다.페이지 요청 마다 서버에서 데이터를 가져오고 페이지를 렌더링하기 때문에 항상 최신의 데이터를 유지한다.
사전에 렌더링하기 때문에 페이지 소스에서 컨텐츠를 볼 수 있기 때문에 크롤러가 우리 페이지를 크롤링할 때 많은 컨텐츠를 제공할 수 있다.
페이지 요청 마다 데이터를 가져오고 렌더링하기 때문에 최신의 데이터를 유지할 수 있다.
Vercel, Netlify와 같은 서버리스 플랫폼에 배포할 수 없으며 AWS를 사용중이라면 S3에 빌드된 정적 파일을 올려 CDN으로 캐싱하는 등의 방법을 사용할 수 없다.
또한 데이터를 가져올 때 까지 빈화면만 보여주기 때문에 사용자 입장에서는 페이지가 느리다고 생각할 수 있다.
하지만 React Suspense를 사용하여 fallback UI를 보여줄 수 있다.
위에서 알아본 SSG SSR과는 달리 CSR은 페이지가 렌더링된 후, 데이터를 가져온다. 일반적으로 useEffect()를 사용해 데이터를 가져온다.
useEffect(() => {
setLoading(true)
fetch('/api/profile-data')
.then((res) => res.json())
.then((data) => {
setData(data)
setLoading(false)
})
}, [])
항상 최신의 데이터를 보여준다.
페이지에 필요한 모든 데이터를 가져오지 않았어도 로딩화면을 보여주는 등의 방법으로 사용자가 빈화면을 보고 있지 않아도 된다.
HTML, JS 파일이 로드된 후 데이터를 가져오기 때문에 페이지 소스에 컨텐츠가 없다. 하지만 그렇다고 검색 엔진이 인덱싱할 수 없는 것은 아니다.
그래도 SSG SSR에 비하면 친화적이진 않다.
페이지가 로드된 후 데이터를 요청하기 때문에 바로 모든 컨텐츠를 담은 화면을 볼수는 없다.
위에서 SSG에 관한 내용을 다룰 때, getStaticProps를 사용할 때도 데이터를 최신으로 가져올 수 있다고 했었다.
ISR을 사용하면 페이지를 업데이트하기 위해 다시 빌드할 필요 없이 페이지 별로 SSG를 사용할 수 있다.
export async function getStaticProps() {
const res = await fetch('https://.../posts')
const posts = await res.json()
return {
props: {
posts,
},
revalidate: 10, // In seconds
}
}
정적 페이지를 로드하기 때문에 속도가 빠르다.
revalidate에 시간을 설정하면 설정된 시간이 지난 요청에 대해서는 최신의 컨텐츠를 제공한다.
사전에 렌더링하기 때문에 페이지 소스에서 컨텐츠를 볼 수 있기 때문에 크롤러가 우리 페이지를 크롤링할 때 많은 컨텐츠를 제공할 수 있다.
백그라운드에서 새로운 정적 페이지를 생성하는 것이 늦어지면 첫 로드가 느려질 수 있다.
NextJs 제공하는 Data Fetching 방법 중 어떤것을 선택할 것인지는 개발하는 사람이 장단점을 파악하고 결정하는 것이 중요한 거 같다.
하지만 NextJs 측에서는 SSG를 추천한다.
https://dev.to/matfrana/server-side-rendering-vs-static-site-generation-17nf
https://dev.to/theodorusclarence/should-we-use-csr-ssr-ssg-or-isr-on-nextjs-1f29
엔진이 인덱싱할 수 없는 것은 아니다. Area Code