Next.js 애플리케이션을 개발하다 보면 사전 렌더링을 할 필요가 없거나 사전 렌더링을 할 수 없는 데이터를 다루게 되는 경우도 있다
아래와 같은 경우에는 클라이언트 사이드 데이터 페칭
을 고려해 볼 만한 예시이다
- 갱신 주기가 잦은 데이터
- 특정 유저에만 한정되는 데이터
- 데이터의 일부분만 표시하는 경우
갱신 주기가 잦은 데이터를 사전 렌더링을 한다면 사용자가 페이지를 방문했을 때 그 페이지의 데이터는 이미 과거의 것이 되어있을 수도 있다.
계정에 접속해서 프로필 페이지에서 특정한 데이터를 열람하는 경우에는 페이지를 사전 렌더링할 필요가 없다.
검색 엔진에서도 개인 프로필을 확인하지 않기도 하고 사용자 경험에서도 프로필 페이지를 방문할 때 조금 기다려도 그리 나쁘지 않다
또, 모든 데이터를 한 번에 불러오도록 하면 서버에서 요청을 처리하는 데 시간이 많이 소요되므로 개발 단계에서 이 페이지를 사전 렌더링할 이유가 없다
위 같은 경우들에서는 사전 렌더링이나 프리페칭 보다는 React의 useEffect
나 fetch (또는 axios)
같은 함수를 이용해서 클라이언트 측에서 데이터를 가져오는 것
이 좋다
즉 getStaticProps
나 getServerSideProps
대신 서버가 아니라 클라이언트
에서 코드가 실행될 때 데이터를 가져오도록 구축하는 것이다.
우선 데이터를 fetch
해와야 하므로 데이터베이스가 필요하다
firebase
의 realtime database
를 사용해서 더미데이터를 생성해보자
해당 데이터베이스로 axios
를 사용해서 데이터를 받아와보자
import axios from "axios";
import React, { useEffect, useState } from "react";
interface ISale {
id: string;
username: string;
volume: string;
}
const LastSalesPage = () => {
const [sales, setSales] = useState<ISale[] | undefined>();
const [isLoading, setIsLoading] = useState(false);
useEffect(() => {
setIsLoading(true);
axios
.get("https://nextjs-course-29d4b-default-rtdb.firebaseio.com/sales.json")
.then((res) => {
const formatSales: ISale[] = [];
for (const key in res.data) {
formatSales.push({
id: key,
username: res.data[key].username,
volume: res.data[key].volume,
});
}
setSales(formatSales);
setIsLoading(false);
});
}, []);
if (isLoading) return <p>Loading...</p>;
if (!sales) return <p>No data</p>;
return (
<ul>
{sales?.map((item) => (
<li key={item.id}>
{item.username} - ${item.volume}
</li>
))}
</ul>
);
};
export default LastSalesPage;
useEffect
훅을 사용해서 페이지가 로드됐을때 파이어베이스 서버에서 데이터를 가져오는 코드를 작성했다.
페이지 소스보기를 확인해보면 body태그에 <p>No data</p>
가 남아있는 것을 확인할 수 있다
Next.js에서는 getServerSideProps
를 사용하지 않으면 기본 페이지를 사전 렌더링
하기 때문이다
이 페이지에서 사용된 데이터는 getStaticProps
같은 함수로 준비한 것이 아니기 때문에 Next.js에서 페이지를 사전 렌더링할 때 useEffect
를 거치지 않는다
따라서 sales에 아무런 값이 없기 때문에 이러한 초기 상태로 페이지가 사전 렌더링이 이루어진 것이다
즉, 사전 렌더링이 이루어지지만 데이터가 없고 클라이언트 사이드에서 데이터를 페칭하고 있는 것이다.
useSWR을 사용하기 위해 패키지를 설치한다
$ npm install swr
기존의 코드를 useSWR을 사용해서 리팩토링 해보자
import axios from "axios";
import useSWR from "swr";
import React, { useEffect, useState } from "react";
interface ISale {
id: string;
username: string;
volume: string;
}
const LastSalesPage = () => {
// const [sales, setSales] = useState<ISale[] | undefined>();
// const [isLoading, setIsLoading] = useState(false);
const getSales = async () => {
const response = await axios.get(
"https://nextjs-course-29d4b-default-rtdb.firebaseio.com/sales.json"
);
const formatSales: ISale[] = [];
for (const key in response.data) {
formatSales.push({
id: key,
username: response.data[key].username,
volume: response.data[key].volume,
});
}
return formatSales;
};
const { data, error } = useSWR("sales", getSales);
// useEffect(() => {
// setIsLoading(true);
// axios
// .get("https://nextjs-course-29d4b-default-rtdb.firebaseio.com/sales.json")
// .then((res) => {
// const formatSales: ISale[] = [];
// for (const key in res.data) {
// formatSales.push({
// id: key,
// username: res.data[key].username,
// volume: res.data[key].volume,
// });
// }
// setSales(formatSales);
// setIsLoading(false);
// });
// }, []);
if (error) return <p>Failed to load.</p>;
if (!data) return <p>Loading...</p>;
return (
<ul>
{data?.map((item) => (
<li key={item.id}>
{item.username} - ${item.volume}
</li>
))}
</ul>
);
};
export default LastSalesPage;
fetcher 함수로 getSales를 선언하고 데이터 포맷팅까지 해서 리턴한다
마찬가지로 페이지 소스보기를 확인해보면 Next.js에서 사전 렌더링할 때 useSWR코드는 실행하지 않으므로 <p>Loading...</p>
가 남아있는 것을 확인할 수 있다.
이번에는 클라이언트 사이드 페칭
과 사전 렌더링
을 같이 사용해보자
사전 렌더링을 하기 위해 getStaticProps
를 사용해서 데이터를 받아오고 props로 전달해주겠다
import axios from "axios";
import useSWR from "swr";
import React, { useEffect, useState } from "react";
import { GetStaticProps, NextPage } from "next";
interface ISale {
id: string;
username: string;
volume: string;
}
interface ILastSalesPageProps {
salesData: ISale[];
}
const LastSalesPage: NextPage<ILastSalesPageProps> = ({ salesData }) => {
const [sales, setSales] = useState<ISale[] | undefined>(salesData);
// const [isLoading, setIsLoading] = useState(false);
const { data, error } = useSWR("salesList", async () => {
const response = await axios.get(
"https://nextjs-course-29d4b-default-rtdb.firebaseio.com/sales.json"
);
return response.data;
});
useEffect(() => {
if (data) {
const formatSales: ISale[] = [];
for (const key in data) {
formatSales.push({
id: key,
username: data[key].username,
volume: data[key].volume,
});
}
setSales(formatSales);
}
}, [data]);
// useEffect(() => {
// setIsLoading(true);
// axios
// .get("https://nextjs-course-29d4b-default-rtdb.firebaseio.com/sales.json")
// .then((res) => {
// const formatSales: ISale[] = [];
// for (const key in res.data) {
// formatSales.push({
// id: key,
// username: res.data[key].username,
// volume: res.data[key].volume,
// });
// }
// setSales(formatSales);
// setIsLoading(false);
// });
// }, []);
if (error) return <p>Failed to load.</p>;
if (!sales) return <p>Loading...</p>;
return (
<ul>
{sales?.map((item) => (
<li key={item.id}>
{item.username} - ${item.volume}
</li>
))}
</ul>
);
};
export default LastSalesPage;
export const getStaticProps: GetStaticProps = async (context) => {
const response = await axios.get(
"https://nextjs-course-29d4b-default-rtdb.firebaseio.com/sales.json"
);
const formatSales: ISale[] = [];
for (const key in response.data) {
formatSales.push({
id: key,
username: response.data[key].username,
volume: response.data[key].volume,
});
}
return {
props: {
salesData: formatSales,
},
};
};
getStaticProps
는 개발모드 에서는 페이지에 방문할때 마다 실행되므로 빌드해서 프로덕션용으로 구축
해서 실행해보자
빌드타임에서 getStaticProps
가 실행되어 데이터를 받아오고 이를 정적 생성한다
그리고 그 이후 데이터베이스가 변경되어도 useSWR
을 통해서 데이터가 새로 업데이트가 될 것이다
$ npm run build
$ npm start
페이지를 새로 로드할 때 로딩스피너도 보이지 않을 것이다.
파이어베이스의 데이터베이스에서 데이터를 새로 추가해보자
데이터베이스의 데이터가 변하면 브라우저에 바로 업데이트가 되는 것을 확인할 수 있다
하지만 페이지 소스보기
를 통해서 페이지소스를 확인해보면 새로 추가된 Julie데이터는 찾아볼 수 없다
이는 페이지 사전 렌더링을 위한 getStaticProps
를 실행할 때 그런 정보는 없었기 때문이다
하지만 클라이언트 측에서 데이터 페칭
이 일어나기 때문에 브라우저에 보이는 것이다
클라이언트 사이드 데이터 페칭과 사전 렌더링을 결합함으로써 최적의 사용자 경험을 만들어낼 수 있다.
시작부터 일부 데이터를 갖게 하고 브라우저 내부에서 업데이트를 하는 것이다
참고: https://www.udemy.com/course/nextjs-react-incl-two-paths/