이번에 채용 과제를 하다가 드디어 Next.js의 두 props에 대해 조금은 이해하게 되어 추가 공부를 위해 글을 쓴다. 독일어를 배울 때도 그랬지만 나는 일단 단어가 길어지면 어렵게 느껴지고 머리가 하얘지는(?) 편인데 이 두 props를 처음 마주쳤을 때도 같은 기분이 들었던 것 같다.
우선 공식 문서를 보기 전에 블로그를 이곳저곳 둘러보면서 듬성듬성 배경지식을 쌓고 이 배경지식을 바탕으로 (제일 믿을 만한 출처인) 공식 문서를 탐독하려 한다. 아는 지식이 있는 상태에서 영어(!)를 읽으면 그나마 눈에 좀 잘 들어오기도 하고, 기존에 블로그로 얻은 배경지식과 비교해 보면서 다른 부분이 있다면 더 숙지해야 할 부분인 공식 문서 내용이 머리에 더 잘 들어오는 효과도 있기 때문이다.
Next.js는 사전 렌더링(pre-rendering)이 가능하다는 특징을 갖고 있는 React 프레임워크이다.
pre-rendering을 위한 data fetching을 위해 기존에는 getInitialProps를 사용했지만, Next.js 9.3 버전부터는 getInitialProps가 getStaticProps, getServerSideProps, getStaticPaths로 분리되었다.
모두 pre-render가 필요한 경우에만 사용하는 것이 좋다.
이 중 많이 사용되는 getStaticProps와 getServerSideProps의 차이와 활용에 대해 알아보자.
getStaticProps와 getServerSideProps는 React의 useEffect의 단점을 보완하는 효과가 있다.
useEffect의 의존성 배열에 빈 대괄호를 넣을 경우 최초 렌더링 시에만 1회 실행된다고 알고 있었다. 보통 useEffect 안에는 상태값을 바꿔 렌더링시키기 위한 useState의 setter 함수가 들어간다.
그러나 실제로 이 함수에 바로 값이 저장되지 않고 최초 렌더링 시에는 useState 상태가 초기값으로 렌더링이 되었다가 useEffect가 실행되어 내부 함수가 실행되는 과정 이후 한 번의 렌더링이 더 있은 후에야 해당 useEffect가 적용되는 형태였던 것이다.
이러한 과정은 페이지 깜박임을 유발하여 사용자 경험을 감소시킨다고 한다.
import { useState, useEffect } from 'react';
const SampleComponent = () => {
const DUMMY_DATA = [
{
id: 1,
title: 'first data'
},
{
id: 2,
title: 'second data'
}
]
const [sampleState, setSampleState] = useState([]);
useEffect(() => {
setSampleState(DUMMY_DATA);
}, []);
return (
<>
{sampleState.map((data) => <h1 key={data.id}>{data.title}</h1>)}
</>
)
};
export default SampleComponent;
최초 렌더링 시 useState의 상태값 sampleState는 초기값인 빈 배열의 상태를 가진다.
최초 렌더링 이후 useEffect가 실행되어 setter 함수를 통해 값이 채워지게 되면, 두 번째 렌더링을 통해 채워진 상태값이 보여진다. 이 과정에서 화면 깜박임이 생기게 되는 것이다.
위와 같은 문제를 Next.js에서는 pre-rendering으로 해결했다. 렌더링 이전에 미리 값을 지정해두어 최초 렌더링 때 바로 값이 보이게끔 하는 것이다.
이러한 pre-rendering을 위한 data fetching을 할 수 있는 기능으로 getStaticProps와 getServerSideProps가 있다.
빌드 시에 딱 한 번만 호출되고, 바로 static file로 빌드된다.
이후에는 수정이 불가능하다.
즉, SSG (Static Site Generation) 개념이다.
최초 빌드 시 빌드되는 값이 추후에 수정될 일이 없는 경우에 사용하기 좋다.
원칙적으로는 호출할 때 데이터 fetch를 하지 않으므로 성능면에서는 getServerSideProps보다 더 좋다. (But revalidate로 조절 가능)
예시: 모든 정적인 페이지의 form
앞에 나온 예시에 적용해보자.
// 이번에는 DUMMY_DATA를 컴포넌트 밖으로 뺐다.
const DUMMY_DATA = [
{
id: 1,
title: "first data",
},
{
id: 2,
title: "second data",
},
];
const SampleComponent = (props) => { // 여기 props가 getStaticProps에서 반환한 props!
return (
<>
{props.data.map((data) => (
<h1 key={data.id}>{data.title}</h1>
))}
</>
);
};
export async function getStaticProps() {
return {
props: {
data: DUMMY_DATA,
},
// revalidate: 10,
};
}
export default SampleComponent;
getStaticProps을 사용하면 컴포넌트가 렌더링 되기 전에 getStaticProps에서 반환되는 값을 미리 컴포넌트에 props로 넣어주기 때문에 useEffect, useState와 같은 번거로운 hook 사용이 필요가 없다..!
getStaticProps의 경우에도 요청이 들어올 때마다 주기적으로 업데이트되도록 해주는 장치가 있다. 바로 revalidate이다. 위 코드블럭에서 주석처리한 부분을 적용하면 10초마다 새로 업데이트 된다.
const DUMMY_DATA = [
{
id: 1,
title: "first data",
},
{
id: 2,
title: "second data",
},
];
const SampleComponent = (props) => {
return (
<>
{props.data.map((data) => (
<h1 key={data.id}>{data.title}</h1>
))}
</>
);
};
export async function getServerSideProps(context) {
const req = context.req;
const res = context.res;
return {
props: {
data: DUMMY_DATA,
},
};
}
export default SampleComponent;
getServerSideProps는 서버 쪽에서 렌더링하는 것이므로 파라미터로 context를 받아 서버의 요청 및 응답을 저장하는 req, res 변수를 저장해둬야 한다.
따로 revalitdate로 주기를 정하지 않는 이상 다시 업데이트하지 않는 getStaticProps와 달리, 요청이 들어올 때마다 호출하기 때문에 getServerSideProps는 getStaticProps에 비해 성능상으로는 안 좋지만 내용을 언제든지 수정할 수 있다는 장점이 있다.
상황에 따라 더 적절한 것을 선택하면 될 것 같다.