이 포스팅은 (Next.js) Pre-rendering : Static Generation와 이어진 글입니다.
앞서 살펴보았듯이, 정적인 요소를 담은 페이지를 렌더링하기 위해서 getStaticProps
를 사용한다는 것을 알게 되었다. 여기서 정적인 요소를 담은 페이지로 마케팅 페이지, 블로그 포스트, 커머스 페이지 중 상품 상세 페이지 등을 말한다.
이어 이번 포스팅에서 알아볼 내용은 getServerSideProps
이다.
이전 포스팅을 통해 알아본 getStaticProps
는 빌드 시에 데이터를 가져온다. 때문에 빌드되고 나면 그 데이터는 조작되지 않는다. 따라서, 최신화된 정보를 업데이트해 보여줘야 한다면 getStaticProps
보다는 요청할때마다 데이터를 최신화할 수 있는 getServerSideProps
를 사용해야 한다.
팀 프로젝트에서 상품 상세페이지를 Next.js로 컨버팅해야할 일이 있었는데 일반적으로 상품 상세 페이지는 getStaticProps
를 이용할 것을 권장하고 있었다. 하지만, 해당 페이지에서 고객 리뷰, 세일 적용 가격 등 동적으로 구성해야할 요소들이 존재하고 있어서 getServerSideProps
를 이용하기로 결정했다.
팀 프로젝트는 상태관리를 위해 redux-toolkit
을 사용하고 있다. getServerSideProps
에 다른 상태관리 라이브러리를 시범적으로 적용해볼 수 있을 것 같아, 서버 스테이트를 다루는데 탁월한 기능을 제공하는 React Query를 사용해보기로 했다. (경우에 따라서는, vercel에서 제공하는 useSwr도 적용해볼만 할 것 같다.)
리액트 앱과 마찬가지고 리액트 쿼리도 앱 자체를 Provider로 감싸준 후 context api처럼 context 객체를 앱 전체에서 접근할 수 있도록 사전 작업을 해주어야 한다.
import { Hydrate, QueryClient, QueryClientProvider } from 'react-query';
import { ReactQueryDevtools } from 'react-query/devtools';
const App = (props) => {
const { Component, pageProps } = props;
const [queryClient] = useState(() => new QueryClient());
return (
<QueryClientProvider client={queryClient}>
<Hydrate state={pageProps.dehydratedState}>
<Provider store={store}>
<CssBaseline />
<Component {...pageProps} />
</Provider>
</Hydrate>
<ReactQueryDevtools />
</QueryClientProvider>
);
};
export default App;
context Provider처럼 QueryClientProvider
로 감싸고, 다시 Hydrate
로 감싸주었다.
이를 통해 Hydrate(업데이트되거나 패칭된)된 server state를 앱 전반에서 접근할 수 있게 되었다.
여기서 컴포넌트들이 접급하게 될 상태들은 pageProps 객체의 dehyderatedState에 내장된 값들이다.
import { useDispatch, useSelector } from 'react-redux';
import { useQuery } from 'react-query';
export const useFeedDispatch = () => {
const dispatch = useDispatch();
const fetchFeedList = async (feedOrderType, page) => {
try {
...
const { data } = await axios({
url: `${BASE_URL}/feeds?${queryString}`,
method: 'GET',
});
return data;
} catch (err) {
consoloe.log('err', err);
}
};
const useFeedList = () => {
return useQuery(['feedList'], () => fetchFeedList());
};
export { useFeedList, fetchFeedList };
해당 파일에서는 서버 통신으로 데이터를 받아 useQuery
를 이용해 'feedList'라는 키를 갖는 쿼리 객체 속에 리스폰스로 넘어온 데이터를 담아줄 것이다.
import { dehydrate, QueryClient, useQuery } from 'react-query';
import { fetchFeedList } from 'src/util/hooks/useFeedDispatch';
const FeedDetail = () => {
const { isLoading, error, isData } = useQuery('feedList', () =>
fetchFeedList(),
);
if (isLoading) return <div>Loading</div>;
if (error) return 'error has occured';
return (
...
)
export default FeedDetail;
export async function getServerSideProps() {
const queryClient = new QueryClient();
await queryClient.prefetchQuery('feedList', () =>
fetchFeedList('SELLS_MOST', 1),
);
return {
props: {
dehydrateState: dehydrate(queryClient),
},
};
}
getStaticProps
함수에서는 props에 데이터 객체를 할당해 jsx 컴포넌트의 매개변수로 전달해 사용하고 있지만, getServerSideProps
에서 쿼리 객체를 사용할때는 굳이 props를 매개변수로 전달할 필요가 없다. 왜냐하면 이미 _app.js
에서 Hydrate 프로바이더로 컴포넌트를 감싸주고 있어 접근이 가능하기 때문이다.
따라서 위처럼 getServerSideProps
함수 내에서 데이터를 받아오는 fetchFeedList
함수를 실행 후 props: {dehydrateState: dehydrate(queryClient) }
라고 작성만 해주면 jsx 컴포넌트 내부에서 useQuery로 받아온 값들을 사용할 수 있다.
구동 프로세스 자세히 알아보기 : Mastering data fetching with React Query and Next.js
만일, 리액트 쿼리를 이용하지 않고 상태관리를 redux로 관리할 경우에는 _app.js에 redux Wrapper로 감싸주는 등의 추가 작업이 역시 필요하다. (redux는 역시 보일러 플레이트를 좋아해..)
리액트 쿼리를 이용할 경우, server state와 client state로 state 관리를 위한 관심사를 분리할 수 있고 패칭된 데이터를 알아서 로컬 캐싱해주는 등의 장점이 있기 때문에 프로젝트에 적용하면 장점이 많을 것이라는 생각이 들었다.
프로젝트에 적용하기 위해 참고한 많은 글들이 타입스크립트를 기준으로 작성되어 있어, 내 경우처럼 리액트와 next.js를 결합한 프로젝트를 진행하는 누군가가 있다면 도움이 되길 바라는 마음을 담아 이 글을 맺는다.
리액트 쿼리 공식 가이드
[React Query] Next.js + React Query로 무한 스크롤과 SSR 구현하기