function HomePage() {
const [loadedMeetups, setLoadedMeetups] = useState([]);
useEffect(() => {
// send a http request and fetch data
const res = await fetch('https://some-url.com');
const data = await res.json();
setLoadedMeetups(data);
}
return <MeetupList data={loadedMeetups} />
}
export default HomePage;
위와 같이 useEffect를 사용해서 fetch된 data를 표시할 수 있다. useEffect는 최초에 랜더링이 된 다음에 실행된다. 따라서 처음 진입시에는 데이터가 없는 상태로 랜더링이 된다. (View page source로 확인해보면 데이터가 없음을 확인할 수 있습니다.)
next build를 하면 ./next/server/pages에 파일들이 생성됩니다.
Request(요청)
-> /some-route (어떤 라우트)
-> Return pre-rendered page (Good for SEO)
-> Hydrate with React code once loaded (React코드도 같이 받게 됩니다.)
-> Page/App is interactiv
페이지로드시에 완성된 html 파일과 리액트코드를 함께 받음으로써, Server-side-rendering과 SPA의 기능을 다 갖추게 됩니다.
import path from 'path';
import { promises } from 'fs';
function HomePage(props) {
return (
<div>Test</div>
);
}
export async function getStaticProps() {
const filePath = path.join(process.cwd(), 'data', 'dummy-data.json');
return {
props: {
products: {}
}
}
}
export default HomePage;
위와 같이 client환경에서는 사용할 수 없는 fs와 path를 사용하는 경우, 반드시 server코드(getStaticProps)내에서 사용을 해줘야 합니다.
server코드에서 사용시에 import된 node 코드들은 자동으로 삭제가 됩니다. 하지만 아래와 같이 import만 해두고 사용하지 않으면 client에서 import를 시도하게 되므로 아래와 같은 에러가 발생합니다.
getStaticProps가 실행된 다음에 component function이 실행됩니다.
export async function getStaticProps(context) { ... }
export async function getStaticProps() {
// fetch data from an API
return {
props: {
},
revalidate: 10 //
}
}
context의 params에는 file name에 대한 정보가 key value pair로 들어 있습니다.
// pages/[pid].js
export async function getStaticProps(context) {
const { params } = context;
const productId = params.pid;
// (...)
}
필수 property로 props는 pages component의 props으로 들어갑니다.
기본적으로 false입니다. 즉 next build를 다시 하지 않는 이상 데이터가 그대로입니다.
revalidate를 사용하면 Incremental Static Generation이 가능합니다. Pre-generate된 Page를 request가 올떄마다 ~초 후에 다시 Re-generate 하는 것입니다.
즉, revalidate에 숫자(second를 의미)를 넣으면, ~초 후에 generation이 다시 된다는 것을 의미합니다. (demo 페이지 참고)
만약 revalidate: 10
이라면
step (second) | data | regeneration |
---|---|---|
페이지 접속 (0s) | (always) old data | regeneration start |
2번째 접속(7s) | old data | wait for timeout |
3번째 접속(11s) | new data | regeneration end |
참고로 revalidate는 npm run dev에서는 유효하지 않습니다. production인 상황에서만 유효합니다.
npm run build를 하면?
revalidate가 추가된 상황에서 npm run build를 하면 위 이미지와 같이 ISR(incremental static regeneration)이 루트 페이지(/
)에 ISR: 10 Seconds로 추가 된 것을 볼 수 있습니다.
notFound를 true로 하면 정상적인 페이지 대신에, 404페이지로 표시됩니다.
예를들어 data가 없는 경우, 404로 표시하도록 할 수 있습니다.
export async function getStaticProps() {
// (...)
if (data.products.length === 0) {
return {
notFound: true
}
}
// (...)
}
data fetch를 실패한경우 redirect를 할 수 있습니다.
export async function getStaticProps() {
// (...)
if (!data) {
return {
redirect : {
destination: '/no-data'
}
}
}
// (...)
}
dynamic page(
[id].js
)에서 getStaticProps를 쓴다면 반드시 필요합니다.export async function getStaticPaths(context) { ... }
Server Error
Error: getStaticPaths is required for dynamic SSG pages and is missing for '/[pid]'.
Read more: https://err.sh/next.js/invalid-getstaticpaths-value
// pages/[meetupId]/index.js
export async function getStaticProps(context) {
const meetupId = context.params.meetupId;
return {
props: {
meetup: {
title: "Our Meetup!",
id: meetupId
}
}
}
}
위와같이 쓰면, context.params는 [meetupId]
의 값을 가져오기 때문에, 문제가 없는 것처럼 보입니다. 하지만 미리 페이지를 만들어야 하는 상황에서 meetupId가 어떤 값일지는 알 수가 없으므로 getStaticPaths가 필요합니다.
export async function getStaticPaths() {
return {
fallback: false,
paths: [
{ params: { meetupId: 'm1' } }
]
}
}
미리 생성되야할 meetupId(dynamic segment value)에 대해서 정의합니다.
export async function getStaticPaths() {
return {
fallback: false,
paths: [
{ params: { meetupId: 'm1' } },
{ params: { meetupId: 'm2' } },
{ params: { meetupId: 'm3' } }
]
}
}
meetupId가 여러개가 있다면, 위와같이 paths array안에 넣어주면 됩니다.
보통은 api로 받아서 params들을 만듭니다.
모든 path에 대해서 다 만들기 보다는 popular page 일부만 (예를 들어 100개정도) 생성할 수도 있습니다.
fallback은 기본값이 false입니다.
fallback: true
로 하면, 생성하지 않는 path에 대해서도 컴포넌트를 생성할 수 있게 됩니다. (다만 just-in-time으로 생성되므로, pre-generated된 page는 아닙니다.)
true인 경우에는 nextjs가 path에 대한 blocking을 하지 않습니다. false인 경우에는 유효하지 않은 path로 들어간다면 404를 보여주지만, ture는 유효하지 않은 path에 대해서 막지 않고 랜더링을 하려고 하기 때문에 따로 조건문 처리가 필요합니다. (아래 코드에서 주석확인)
// pages/[pid].js
import path from 'path';
import { promises } from 'fs';
function ProductDetailPage(props) {
const { loadedProduct } = props;
if (!loadedProduct) { // props가 없는 경우에 대한 처리 (모든 path에 대해서 랜더링을 시도하기 때문에 필요합니다.)
return <p>Loading...</p>
}
return (
<div>
<h1>{loadedProduct.title}</h1>
<p>{loadedProduct.description}</p>
</div>
)
}
export async function getStaticProps(context) {
const { params } = context;
const productId = params.pid;
const filePath = path.join(process.cwd(), 'data', 'dummy-data.json');
const json = await promises.readFile(filePath);
const data = JSON.parse(json);
const product = data.products.find(product => product.id === productId)
if (!data) {
return {
notFound: true,
}
}
if (!product) {
return {
redirect: {
destination: '/'
}
}
}
return {
props: {
loadedProduct: product
}
}
}
export async function getStaticPaths() {
return {
paths: [
{ params: { pid: 'p1' } },
],
fallback: true
}
}
export default ProductDetailPage
getStaticPaths
에서 p1
에 대해서만 페이지를 생성했지만, fallback: true
이기 때문에 https://my-domain/p2
페이지도 정상적으로 진입할 수 있습니다. 다만 미리 생성된 페이지가 아니므로, props
가 없을때에 대한 처리를 해줘야합니다.(코드에서 주석 확인)
fallback: true
를 사용하는 것은 useEffect내에서 데이터를 fetch하는 것과 동일합니다. (useEffect 코드가 필요 없다는 약간의 장점이 있습니다.)
fallback: 'blocking'
은 if (!loadedProduct)
이라는 조건도 필요하지 않습니다. 데이터를 가져온 후에 랜더링이 되게 합니다. Loading...이라는 화면을 보여주지 않아도 되는 장점이 있지만, 데이터를 가져오는데 시간이 오래걸린다면, fallback: true
를 사용하는것이 나을 것 입니다.
fallback: true
인 경우에는 아래와 같은 방식으로 처리가 됩니다.
// pages/[pid].js (예외처리 예시)
export async function getStaticProps(context) {
const { params } = context;
const productId = params.pid;
const data = await getData();
const product = data.products.find(product => product.id === productId)
if (!data) {
return {
notFound: true,
}
}
if (!product) {
return {
redirect: {
destination: '/'
}
}
}
return {
props: {
loadedProduct: product
}
}
}
getStaticPaths에서 정상적으로 params가 생성되므로, 정상적인 순서로 실행되고 client에 component가 랜더링 됩니다.
build process에서 실행되지 않고, deploy가 된 후에 (항상)server에서 실행됩니다. (development server에서도 실행됩니다.)
export async function getServerSideProps(context) {...}
export async function getServerSideProps(context) {
const {params, req, res} = context; // req object는 authentication에서 활용할 수 있음
// fetch data from an API
return {
props:{}
}
}
npm run build
build 성공시 파일이 어떻게 생성되었는지 결과가 나옵니다.
useEffect()내에서 fetch()를 하는 방식
some data는 항상 pre-rendered될 필요가 없습니다.
sales.json 데이터는 아래와 같습니다. (아래예시는 firebase에서 Test db를 생성하여 진행했습니다.)
{
"s1": {
"username": "cho",
"volumn": 100
},
"s2": {
"username": "heo",
"volumn": 50
},
"s3": {
"username": "julie",
"volumn": 80
}
}
useEffect와 fetch를 사용한 방식으로 일반적인 React에서 사용하는 방식입니다.
pre-rendering되는 부분이 없고, View Page Source를 보면 아래코드에서Loading...
만 있습니다.
import { useEffect, useState } from 'react';
function LastSalesPage() {
const [sales, setSales] = useState();
const [isLoading, setIsLoading] = useState(false);
useEffect(() => {
setIsLoading(true);
fetch('https://nextjs-course-bcd7e-default-rtdb.firebaseio.com/sales.json')
.then(response => response.json())
.then(data => {
const transformedSales = [];
for (const key in data) {
transformedSales.push({ id: key, username: data[key].username, volumn: data[key].volumn })
}
setSales(transformedSales)
setIsLoading(false)
})
}, [])
if (isLoading) {
return <p>Loading...</p>
}
if (!sales) {
return <p>No data...</p>
}
return (
<ul>
{sales?.map(sale => <li key={sale.id}>name: {sale.username}, volumn: ${sale.volumn}</li>)}
</ul>
)
}
export default LastSalesPage
next team에서 만든 swr 라이브러리를 사용하는 방식입니다.
결과는 1.과 동일합니다. useSWR을 사용하면 좀 더 많은 기능을 쉽게 사용할 수 있습니다. (예를 들면, db 내용이 바뀌면 알아서 fetch를 해줍니다.)
import { useEffect, useState } from 'react';
import useSWR from 'swr';
const fetcher = (url) => fetch(url).then(res => res.json())
function LastSalesPage() {
const [sales, setSales] = useState();
const { data, error } = useSWR('https://nextjs-course-bcd7e-default-rtdb.firebaseio.com/sales.json', fetcher);
useEffect(() => {
if (data) {
const transformedSales = [];
for (const key in data) {
transformedSales.push({ id: key, username: data[key].username, volumn: data[key].volumn })
}
setSales(transformedSales);
}
}, [data])
if (!data || !sales) {
return <p>Loading...</p>
}
if (error) {
return <p>Failed to Load.</p>
}
return (
<ul>
{sales?.map(sale => <li key={sale.id}>name: {sale.username}, volumn: ${sale.volumn}</li>)}
</ul>
)
}
export default LastSalesPage
pre-fetching & client-side fetching 조합입니다.
현재 db 데이터로 static page를 만들고, 페이지 요청시에 client-side에서 다시 fetching하는 방식입니다.
View Page Source로 보면, props.sales로 초기값을 받은 부분은 pre-rendering되게 됩니다.
import { useEffect, useState } from 'react';
import useSWR from 'swr';
const fetcher = (url) => fetch(url).then(res => res.json())
function LastSalesPage(props) {
const [sales, setSales] = useState(props.sales); // 초기값
const { data, error } = useSWR('https://nextjs-course-bcd7e-default-rtdb.firebaseio.com/sales.json', fetcher);
useEffect(() => {
if (data) {
const transformedSales = [];
for (const key in data) {
transformedSales.push({ id: key, username: data[key].username, volumn: data[key].volumn })
}
setSales(transformedSales);
}
}, [data])
if (!data && !sales) {
return <p>Loading...</p>
}
if (error) {
return <p>Failed to Load.</p>
}
return (
<ul>
{sales?.map(sale => <li key={sale.id}>name: {sale.username}, volumn: ${sale.volumn}</li>)}
</ul>
)
}
export async function getStaticProps() {
const response = await fetch('https://nextjs-course-bcd7e-default-rtdb.firebaseio.com/sales.json');
const data = await response.json();
const transformedSales = [];
for (const key in data) {
transformedSales.push({ id: key, username: data[key].username, volumn: data[key].volumn })
}
return { props: { sales: transformedSales } }
}
export default LastSalesPage
Client-side fetch + getStaticProps = Okay
Client-side fetch + getServerSideProps = Nope
Client-side fetch는 getServerSideProps와 함께 쓰지 않습니다. getServerSideProps는 매 request마다 실행되므로, client-side fetch와 함께 쓸 이유가 없습니다.