안녕하세요. J4J입니다.
이번 포스팅은 Data Fetching의 두 번째인 SSG와 Dynamic Routing에 대해 적어보는 시간을 가져보려고 합니다.
이전 글
[Next] Data Fetching에 대해 알아보기 (1) - CSR / SSR
들어가기에 앞서
Next 공식문서에 따르면 Next의 Data Fetching의 종류에는 다음과 같이 있습니다.
- SSR
- SSG
- CSR
- Dynamic Routing
- ISR
그리고 이번 글에서는 SSG와 Dynamic Routing에 대해 알아보도록 하겠습니다.
SSG
SSG는 Static-site generation의 약자로 특정 페이지에서 fetching이 이루어져야 할 데이터들을 build 단계 때 prerendering 하여 props로 사용함으로 써 페이지를 접근할 때마다 data fetching이 이루어지지 않고 캐싱된 데이터를 가져와 사용할 수 있도록 도와줍니다.
즉, build 단계때 가져온 데이터가 항상 유지되어 있기 때문에 build 된 이후에 fetching 되는 데이터가 변경되더라도 해당 페이지에 보이는 데이터 값은 항상 동일하다는 뜻입니다.
SSG는 기본적으로 한번 fetching된 데이터가 다시 build가 이루어지기 전까지 계속 유지되기 때문에 client에 의해 변경되는 데이터들을 활용하여 사용되기에는 적합하지 않습니다.
하지만 한번 fetching된 데이터를 지속적으로 유지해도 되는 페이지들에서는 fetching 되는 시간이 필요 없기 때문에 매우 빠른 속도로 렌더링이 될 수 있도록 도와줍니다.
Next에서 SSG를 활용하기 위해서는 getStaticProps를 사용합니다.
간단한 예시로 다음과 같이 API와 SSG를 구성할 수 있습니다.
import axios from 'axios';
import { GetStaticProps } from 'next';
export const getStaticProps: GetStaticProps = async (context) => {
try {
const res = await axios.get('http://localhost:8080/product/no');
const productNos: number[] = res.data;
return {
props: {
productNos,
},
};
} catch (error) {
return {
notFound: true,
};
}
};
interface Props {
productNos: number[];
}
const Ssg = (props: Props) => {
return (
<div>
{props.productNos.map((productNo) => (
<div>
<h2>데이터 Fetch로 가져온 상품번호 : {productNo}</h2>
</div>
))}
</div>
);
};
export default Ssg;
코드를 위와 같이 작성하고 다음 명령어를 이용하여 build 해줍니다.
$ npm run build
build를 하면 .next → server → pages 경로에 build 된 html 파일이 생성된 것을 확인할 수 있습니다.
그리고 ssg.html 파일을 열어보면 다음과 같은 코드를 확인할 수 있습니다.
<!DOCTYPE html><html><head><meta charSet="utf-8"/><meta name="viewport" content="width=device-width"/><meta name="next-head-count" content="2"/><noscript data-n-css=""></noscript><script defer="" nomodule="" src="/_next/static/chunks/polyfills-c67a75d1b6f99dc8.js"></script><script src="/_next/static/chunks/webpack-69bfa6990bb9e155.js" defer=""></script><script src="/_next/static/chunks/framework-5f4595e5518b5600.js" defer=""></script><script src="/_next/static/chunks/main-ed26494f1722fea8.js" defer=""></script><script src="/_next/static/chunks/pages/_app-fb6e2c4f9cf5fdc3.js" defer=""></script><script src="/_next/static/chunks/pages/ssg-4057add5ee1efead.js" defer=""></script><script src="/_next/static/3P4F3dq6IStvDMIk1Ibau/_buildManifest.js" defer=""></script><script src="/_next/static/3P4F3dq6IStvDMIk1Ibau/_ssgManifest.js" defer=""></script></head><body><div id="__next" data-reactroot=""><div><div><h2>데이터 Fetch로 가져온 상품번호 : <!-- -->1</h2></div><div><h2>데이터 Fetch로 가져온 상품번호 : <!-- -->2</h2></div><div><h2>데이터 Fetch로 가져온 상품번호 : <!-- -->3</h2></div><div><h2>데이터 Fetch로 가져온 상품번호 : <!-- -->4</h2></div><div><h2>데이터 Fetch로 가져온 상품번호 : <!-- -->5</h2></div><div><h2>데이터 Fetch로 가져온 상품번호 : <!-- -->6</h2></div></div></div><script id="__NEXT_DATA__" type="application/json">{"props":{"pageProps":{"productNos":[1,2,3,4,5,6]},"__N_SSG":true},"page":"/ssg","query":{},"buildId":"3P4F3dq6IStvDMIk1Ibau","isFallback":false,"gsp":true,"scriptLoader":[]}</script></body></html>
코드를 자세히 살펴보면 일반적인 상황에서는 페이지를 접근해야 가져오는 API 데이터들이 이미 props로 전달된 것을 확인할 수 있습니다.
또한 props로 전달된 데이터들이 활용되어 html 코드가 이미 작성된 것도 확인할 수 있습니다.
정확하게 확인하기 위해 다음 명령어를 이용하여 서버를 시작해보겠습니다.
$ npm run start
실행된 화면을 확인해 보면 html 파일에 이미 fetching이 이루어진 데이터들이 담겨있었기 때문에 Preview에 fetching 된 데이터가 함께 보이고 있는 것을 확인할 수 있습니다.
즉, fetching된 데이터들을 크롤링 봇들이 수집해갈 수 있다는 의미이며 우리가 원하던 SEO가 정상적으로 적용될 것입니다.
추가적인 테스트를 위해 여기서 API를 수정하여 다음과 같이 데이터를 전달해 주도록 변경해 보겠습니다.
API를 수정한 뒤에 페이지를 새로고침 하면 다음과 같이 이전과 동일한 화면을 확인할 수 있습니다.
화면이 동일하게 보이는 이유는 위에서 얘기했던 것처럼 SSG는 build 단계 때 fetching 될 데이터를 prerendering 하여 캐싱처리를 하고 있기 때문입니다.
만약 데이터가 변경되기를 원한다면 API가 수정되었을 때 build를 다시 진행해 주셔야 정상 적용됩니다.
Dynamic Routing - 기본
Dynamic Routing은 SSG와 유사하게 특정 페이지에서 fetching이 이루어져야 하는 데이터를 build 단계 때 prerendering을 한 뒤 캐싱 처리된 페이지를 보여줄 수 있습니다.
다만 SSG와의 차이점은 Dynamic Routing의 대상은 /dynamicRouting/[productNo].tsx와 같이 변수 값마다 다른 페이지들을 prerendernig 한다는 것입니다.
Dynamic Routing은 상품번호, 리뷰번호 등과 같이 서버로부터 가져오는 데이터들을 가지고 많은 페이지들이 동적으로 구성될 때 사용하면 효과적입니다.
이미 build 하는 단계에서 존재하는 데이터마다 만들어져야 하는 html 파일들이 이미 구성이 돼버리고, 이렇게 만들어진 html 파일들을 모든 사용자에게 캐싱 처리되어 추가 fetching 없이 동일하게 보여주기 때문에 더 빠른 속도로 렌더링 처리가 이루어질 수 있습니다.
Next에서 Dynamic Routing을 활용하기 위해서는 getStaticPaths를 사용합니다.
그리고 SSG에서 사용했던 getStatisProps도 필수적으로 사용되어야 합니다.
간단한 예시로 다음과 같이 API와 Dynamic Routing을 구성해 볼 수 있습니다.
import axios from 'axios';
import { GetStaticProps } from 'next';
interface Product {
name: string;
price: number;
stock: number;
}
export const getStaticPaths = async () => {
try {
const res = await axios.get('http://localhost:8080/product/no');
const productNos: number[] = res.data;
return {
paths: productNos.map((productNo) => {
return {
params: {
// 페이지가 구성되기 위해 사용될 데이터를 params로 저장 (getStaticProps에서 가져와 사용)
// /dynamicRouting/1, /dynamicRouting/2 → params: [{ params: { productNo: '1' }}, { params: { productNo: '2' }}]
productNo: String(productNo),
},
};
}),
fallback: true,
};
} catch (error) {
return {
paths: [],
fallback: false,
};
}
};
export const getStaticProps: GetStaticProps = async (context) => {
if (context.params) {
const productNo = Number(context.params.productNo); // getStaticPaths에서 전달한 params를 이용하여 data fetching
const res = await axios.get('http://localhost:8080/product/detail', {
params: {
productNo,
},
});
const product: Product = res.data;
if (product) {
return {
props: {
product,
},
};
}
}
return {
notFound: true,
};
};
interface Props {
product: Product;
}
const DynamicRouting = (props: Props) => {
return (
<div>
<h2>데이터 Fetch로 가져온 상품</h2>
<p>상품이름: {props.product?.name}</p>
<p>가격: {props.product?.price}</p>
<p>재고: {props.product?.stock}</p>
</div>
);
};
export default DynamicRouting;
코드를 위와 같이 구성하고 다음 명령어를 이용하여 build를 해줍니다.
$ npm run build
build를 한 뒤 .next → server → pages를 확인하면 prerendering이 되어 상품 번호 1~6에 해당되는 html과 json 파일들이 모두 생성되어 있는 것을 확인할 수 있습니다.
이 중 1.html 파일을 확인해 보면 다음과 같이 코드가 작성되어 있는 것을 확인할 수 있습니다.
<!DOCTYPE html><html><head><meta charSet="utf-8"/><meta name="viewport" content="width=device-width"/><meta name="next-head-count" content="2"/><noscript data-n-css=""></noscript><script defer="" nomodule="" src="/_next/static/chunks/polyfills-c67a75d1b6f99dc8.js"></script><script src="/_next/static/chunks/webpack-69bfa6990bb9e155.js" defer=""></script><script src="/_next/static/chunks/framework-5f4595e5518b5600.js" defer=""></script><script src="/_next/static/chunks/main-ed26494f1722fea8.js" defer=""></script><script src="/_next/static/chunks/pages/_app-fb6e2c4f9cf5fdc3.js" defer=""></script><script src="/_next/static/chunks/pages/dynamicRouting/%5BproductNo%5D-bb2353fbc89dee5f.js" defer=""></script><script src="/_next/static/YTvOA5W6FetGgqTX4FoVd/_buildManifest.js" defer=""></script><script src="/_next/static/YTvOA5W6FetGgqTX4FoVd/_ssgManifest.js" defer=""></script></head><body><div id="__next" data-reactroot=""><div><h2>데이터 Fetch로 가져온 상품</h2><p>상품이름: <!-- -->첫번째 상품</p><p>가격: <!-- -->3200</p><p>재고: <!-- -->58</p></div></div><script id="__NEXT_DATA__" type="application/json">{"props":{"pageProps":{"product":{"price":3200,"name":"첫번째 상품","stock":58}},"__N_SSG":true},"page":"/dynamicRouting/[productNo]","query":{"productNo":"1"},"buildId":"YTvOA5W6FetGgqTX4FoVd","isFallback":false,"gsp":true,"scriptLoader":[]}</script></body></html>
SSG와 동일하게 fetching 된 데이터가 props에서 확인할 수 있고, 또한 해당 데이터들이 html 코드에 삽입되어 이미 사용되고 있습니다.
화면에는 정상적으로 나오는지 확인하기 위해 다음 명령어를 이용하여 서버를 실행하겠습니다.
$ npm run start
실행된 화면을 확인해보면 prerendering이 되었기 때문에 Preview에 fetching data들이 정상적으로 보이는 것을 확인할 수 있습니다.
이 또한 SSG처럼 우리가 생각한 것처럼 SEO가 정상적으로 적용될 것입니다.
이번에도 API를 수정하여 다음과 같은 결과가 나오도록 해보겠습니다.
그리고 위의 화면을 새로고침을 해봤지만 변경된 API가 적용되지 않고 이전 화면이 그대로 보이는 것을 확인할 수 있습니다.
결과가 동일한 이유는 SSG와 똑같은 사유로 build 단계 때 prerendering 된 데이터들이 캐싱되어 그대로 사용되기 때문입니다.
변경된 API의 값들이 보이고 싶다면 다시 build를 해주면 결과가 변경되는 것을 확인할 수 있습니다.
Dynamic Routing - fallback
위의 코드를 자세히 확인해 보면 return 값에 fallback이라는 값이 사용되는 것을 확인할 수 있습니다.
fallback은 getStaticProps가 언제 실행될지를 결정해 주는 요소이며 다음과 같이 총 3개의 값들이 사용됩니다.
- true → build 단계 때 fetching 된 데이터로 prerendering 되어 파일 생성, api가 수정되어 prerendering이 되지 않은 경로를 접근했을 때 추가 rendering을 한 뒤 캐싱 처리
- false → build 단계때 fetching 된 데이터로 prerendering 되어 파일 생성, api가 수정되어 prerendering이 되지 않은 경로를 접근했을 때 추가 rendering을 하지 않고 404 리턴
- blocking → true일 때와 상황은 동일하지만 추가 rendering을 할 때 fetching data를 가져온 뒤 초기 렌더링 수행
[ 1. true ]
true에 대한 예시를 간단하게 해 보겠습니다.
먼저 위의 코드와 API를 다시 활용해 보겠습니다.
import axios from 'axios';
import { GetStaticProps } from 'next';
interface Product {
name: string;
price: number;
stock: number;
}
export const getStaticPaths = async () => {
try {
const res = await axios.get('http://localhost:8080/product/no');
const productNos: number[] = res.data;
return {
paths: productNos.map((productNo) => {
return {
params: {
// 페이지가 구성되기 위해 사용될 데이터를 params로 저장 (getStaticProps에서 가져와 사용)
// /dynamicRouting/1, /dynamicRouting/2 → params: [{ params: { productNo: '1' }}, { params: { productNo: '2' }}]
productNo: String(productNo),
},
};
}),
fallback: true,
};
} catch (error) {
return {
paths: [],
fallback: false,
};
}
};
export const getStaticProps: GetStaticProps = async (context) => {
if (context.params) {
const productNo = Number(context.params.productNo); // getStaticPaths에서 전달한 params를 이용하여 data fetching
const res = await axios.get('http://localhost:8080/product/detail', {
params: {
productNo,
},
});
const product: Product = res.data;
if (product) {
return {
props: {
product,
},
};
}
}
return {
notFound: true,
};
};
interface Props {
product: Product;
}
const DynamicRouting = (props: Props) => {
return (
<div>
<h2>데이터 Fetch로 가져온 상품</h2>
<p>상품이름: {props.product?.name}</p>
<p>가격: {props.product?.price}</p>
<p>재고: {props.product?.stock}</p>
</div>
);
};
export default DynamicRouting;
그리고 이 상태에서 build를 하며 다음과 같은 prerendering 된 파일들을 확인할 수 있었습니다.
여기서 API를 수정하여 상품 번호가 7인 것도 함께 리턴해주도록 해보겠습니다.
그 뒤 추가 build를 하지 않고 다음과 같이 상품 번호가 7에 해당되는 페이지를 접근해 보겠습니다.
분명 prerendering이 되지 않아 build 단계 때 만들어지지 않은 경로에 해당되는 파일이지만 위와 같이 정상적으로 화면이 뜨는 것을 확인할 수 있습니다.
또한 prerendering 된 파일 목록들을 확인해 보면 기존에 없었던 상품 번호가 7에 해당되는 파일도 생성된 것을 볼 수 있습니다.
이처럼 fallback을 true로 설정하면 prerendering이 되지 않았더라도 API 결과가 나중에 수정되면 수정된 사항에 맞는 파일들이 자동으로 생성되는 것을 확인할 수 있습니다.
그리고 한번 만들어진 파일은 캐싱처리되는 것을 증명하기 위해 Preview 쪽을 확인해 볼 수 있습니다.
위에서 처음으로 접근했을 때는 다음과 같이 Preview에 fetching 된 데이터가 보이지 않는 것을 확인할 수 있습니다.
왜냐하면 prerendering이 된 페이지가 아니라 처음 접근되면서 페이지가 만들어졌기 때문입니다.
하지만 만약 여기서 새로고침을 하게 되면 다음과 같이 Preview에 fetching된 데이터가 보이는 것을 확인할 수 있습니다.
왜냐하면 이미 위에서 처음 접근하면서 캐싱된 데이터가 만들어졌기 때문에 build 단계 때 prerendering 된 것과 동일한 상황이 만들어졌기 때문입니다.
[ 2. false ]
이번엔 fallback을 false로 변경한 뒤 true일 때와 동일하게 해 보겠습니다.
테스트를 위해 API와 코드를 다음과 같이 변경하겠습니다.
import axios from 'axios';
import { GetStaticProps } from 'next';
interface Product {
name: string;
price: number;
stock: number;
}
export const getStaticPaths = async () => {
try {
const res = await axios.get('http://localhost:8080/product/no');
const productNos: number[] = res.data;
return {
paths: productNos.map((productNo) => {
return {
params: {
// 페이지가 구성되기 위해 사용될 데이터를 params로 저장 (getStaticProps에서 가져와 사용)
// /dynamicRouting/1, /dynamicRouting/2 → params: [{ params: { productNo: '1' }}, { params: { productNo: '2' }}]
productNo: String(productNo),
},
};
}),
fallback: false,
};
} catch (error) {
return {
paths: [],
fallback: false,
};
}
};
export const getStaticProps: GetStaticProps = async (context) => {
if (context.params) {
const productNo = Number(context.params.productNo); // getStaticPaths에서 전달한 params를 이용하여 data fetching
const res = await axios.get('http://localhost:8080/product/detail', {
params: {
productNo,
},
});
const product: Product = res.data;
if (product) {
return {
props: {
product,
},
};
}
}
return {
notFound: true,
};
};
interface Props {
product: Product;
}
const DynamicRouting = (props: Props) => {
return (
<div>
<h2>데이터 Fetch로 가져온 상품</h2>
<p>상품이름: {props.product?.name}</p>
<p>가격: {props.product?.price}</p>
<p>재고: {props.product?.stock}</p>
</div>
);
};
export default DynamicRouting;
다음으로 build를 하면 true일 때와 동일하게 prerendering 된 결과물을 확인할 수 있습니다.
그리고 API를 수정하여 다시 상품 번호 7을 추가하겠습니다.
API가 수정되었으면 상품 번호가 7에 해당되는 페이지를 접근해 보겠습니다.
url을 입력하였더니 true와 달리 변경된 api에 맞게 페이지가 추가 생성되지 않고 404를 리턴했습니다.
또한 상품 번호 7에 해당되는 html파일이 추가 생성되지 않는 것도 확인할 수 있습니다.
[ 3. blocking ]
blocking의 결과는 true와 유사하기 때문에 true와 차이점에 대해서만 소개하겠습니다.
blocking과 true의 차이는 정말 간단하게 fallback 처리 유무입니다.
먼저 위에서 소개한 대로 blocking은 추가 rendering을 할 때 fetching 된 데이터를 가져온 뒤 초기 렌더링이 이루어집니다.
하지만 true 같은 경우는 초기 렌더링이 이루어진 뒤 fetching 된 데이터를 가져오며 데이터를 가져오는 동안 fallback 화면을 보여줍니다.
즉, true 같은 경우는 router를 사용하여 다음과 같이 코드를 작성해 볼 수 있습니다.
import axios from 'axios';
import { GetStaticProps } from 'next';
import { useRouter } from 'next/router';
interface Product {
name: string;
price: number;
stock: number;
}
export const getStaticPaths = async () => {
try {
const res = await axios.get('http://localhost:8080/product/no');
const productNos: number[] = res.data;
return {
paths: productNos.map((productNo) => {
return {
params: {
// 페이지가 구성되기 위해 사용될 데이터를 params로 저장 (getStaticProps에서 가져와 사용)
// /dynamicRouting/1, /dynamicRouting/2 → params: [{ params: { productNo: '1' }}, { params: { productNo: '2' }}]
productNo: String(productNo),
},
};
}),
fallback: true,
};
} catch (error) {
return {
paths: [],
fallback: false,
};
}
};
export const getStaticProps: GetStaticProps = async (context) => {
if (context.params) {
const productNo = Number(context.params.productNo); // getStaticPaths에서 전달한 params를 이용하여 data fetching
const [res] = await Promise.all([
await axios.get('http://localhost:8080/product/detail', {
params: {
productNo,
},
}),
// 강제 timeout
new Promise((resolve) => setTimeout(resolve, 10000)),
]);
const product: Product = res.data;
if (product) {
return {
props: {
product,
},
};
}
}
return {
notFound: true,
};
};
interface Props {
product: Product;
}
const DynamicRouting = (props: Props) => {
/**
* router
*/
const router = useRouter();
return (
<div>
{router.isFallback ? (
<h2>데이터를 가져오는중...</h2>
) : (
<>
<h2>데이터 Fetch로 가져온 상품</h2>
<p>상품이름: {props.product?.name}</p>
<p>가격: {props.product?.price}</p>
<p>재고: {props.product?.stock}</p>
</>
)}
</div>
);
};
export default DynamicRouting;
위와 같이 코드를 작성한 뒤 위에서 계속해오던 build → API에 상품 번호 7 추가 → 상품 번호 7 페이지 접속을 하면 처음에 다음과 같이 fallback 화면이 보이게 됩니다.
그리고 코드를 보면 10초 동안 강제로 timeout을 걸어놨기 때문에 이 상태에서 10초 뒤에는 다음과 같이 fetching 된 데이터를 가져와 렌더링을 해주는 것을 확인할 수 있습니다.
이번엔 blocking을 테스트해 보기 위해 다음과 같이 코드를 수정해 보겠습니다.
import axios from 'axios';
import { GetStaticProps } from 'next';
interface Product {
name: string;
price: number;
stock: number;
}
export const getStaticPaths = async () => {
try {
const res = await axios.get('http://localhost:8080/product/no');
const productNos: number[] = res.data;
return {
paths: productNos.map((productNo) => {
return {
params: {
// 페이지가 구성되기 위해 사용될 데이터를 params로 저장 (getStaticProps에서 가져와 사용)
// /dynamicRouting/1, /dynamicRouting/2 → params: [{ params: { productNo: '1' }}, { params: { productNo: '2' }}]
productNo: String(productNo),
},
};
}),
fallback: 'blocking',
};
} catch (error) {
return {
paths: [],
fallback: false,
};
}
};
export const getStaticProps: GetStaticProps = async (context) => {
if (context.params) {
const productNo = Number(context.params.productNo); // getStaticPaths에서 전달한 params를 이용하여 data fetching
const [res] = await Promise.all([
await axios.get('http://localhost:8080/product/detail', {
params: {
productNo,
},
}),
// 강제 timeout
new Promise((resolve) => setTimeout(resolve, 10000)),
]);
const product: Product = res.data;
if (product) {
return {
props: {
product,
},
};
}
}
return {
notFound: true,
};
};
interface Props {
product: Product;
}
const DynamicRouting = (props: Props) => {
return (
<div>
<h2>데이터 Fetch로 가져온 상품</h2>
<p>상품이름: {props.product?.name}</p>
<p>가격: {props.product?.price}</p>
<p>재고: {props.product?.stock}</p>
</div>
);
};
export default DynamicRouting;
위와 같이 코드를 작성한 뒤 true일 때와 동일하게 build → API에 상품 번호 7 추가 → 상품 번호 7 페이지 접속을 하면 페이지가 렌더링 되지 않고 fetching을 위한 작업이 진행되는 것을 확인할 수 있습니다.
그리고 이 상태에서 10초가 지나면 fetching이 완료된 이후 다음과 같이 화면이 보이는 것을 확인할 수 있습니다.
이상으로 Data Fetching의 두 번째인 SSG와 Dynamic Routing에 대해 간단하게 알아보는 시간이었습니다.
읽어주셔서 감사합니다.
'SPA > Next' 카테고리의 다른 글
[Next] Data Fetching에 대해 알아보기 (4) - useQuery (0) | 2023.03.05 |
---|---|
[Next] Data Fetching에 대해 알아보기 (3) - ISR (0) | 2023.02.20 |
[Next] Data Fetching에 대해 알아보기 (1) - CSR / SSR (0) | 2023.02.12 |
[Next] next-sitemap 사용하기 (0) | 2023.02.08 |
[Next] 하위 경로 사용하기 (0) | 2022.06.27 |
댓글