안녕하세요. J4J입니다.
이번 포스팅은 Data Fetching의 세 번째인 ISR에 대해 적어보는 시간을 가져보려고 합니다.
이전 글
[Next] Data Fetching에 대해 알아보기 (1) - CSR / SSR
[Next] Data Fetching에 대해 알아보기 (2) - SSG / Dynamic Routing
들어가기에 앞서
Next 공식문서에 따르면 Next의 Data Fetching의 종류에는 다음과 같이 있습니다.
- SSR
- SSG
- CSR
- Dynamic Routing
- ISR
그리고 이번 글에서는 ISR에 대해 알아보도록 하겠습니다.
ISR - 기본
ISR은 Incremental Static Regeneration의 약자로 Dynamic Routing의 이점을 가져가며 단점을 보완하기 위해 사용됩니다.
위에 링크를 걸어둔 Dynamic Routing에 대한 이전 글을 확인해 보면 아시겠지만 Dynamic Routing의 특징은 다음과 같습니다.
- build 단계 때 fetching data를 가져와 페이지를 prerendering
- build 이후 호출되는 데이터가 변경되더라도 prerendering된 페이지는 값이 변경되지 않음
즉, Dynamic Routing을 사용하면 prerendering을 하기 때문에 많은 수의 페이지들을 미리 구성하여 사용자들이 더 빠른 속도로 화면을 사용할 수 있게 도와주지만 build 이후로 변경된 데이터를 확인할 수 없다는 단점이 존재합니다.
ISR은 이런 단점을 보완하여 build 이후에도 rebuild를 하지 않고 변경된 데이터들로 구성된 페이지를 다시 생성하여 사용자에게 노출시킬 수 있도록 도와줍니다.
ISR을 사용하기 위해서는 2가지 속성값을 필수적으로 사용해야 합니다.
- getStaticPaths의 fallback
- getStaticProps의 revalidate
먼저 fallback은 이전 글에도 나와 있지만 getStaticProps가 언제 실행될지를 정해주는 역할을 수행합니다.
그리고 revalidate는 getStaticProps로 인해 fetching된 데이터가 stale 되는 시간을 의미하며 단위는 초(seconds)입니다.
fallback(= blocking)과 revalidate(= 30s)를 사용하여 ISR 방식으로 페이지가 만들어지는 과정을 예시를 들면 다음과 같습니다.
- 처음 build를 통해 prerendering 페이지를 생성하고 캐싱 처리
- client가 페이지에 접속하면 revalidate 타이머가 동작하고, 30초 동안 모든 사용자에게 캐싱된 페이지를 노출
- 30초가 지난 뒤 페이지에 접속한 첫 번째 client는 캐싱되어 있던 이전 페이지를 동일하게 확인하게 되고, 해당 client가 페이지에 접근한 순간 데이터를 다시 fetching 하여 prerendering을 진행
- 데이터가 fetching 된 이후에 접속한 client는 초기에 만들어진 stale 된 페이지 대신 새롭게 prerendering 된 페이지를 확인하게 되고 다시 revalidate 타이머가 동작하며 반복 (두 번째 이후의 client가 접속하더라도 fetching이 이루어지지 않으면 기존에 prerendering된 페이지를 확인)
- build 단계 때 prerendering 되지 않은 페이지에 접근하는 경우 fallback의 특징에 따라 동작
ISR의 간단한 예시로 다음과 같이 API와 코드를 작성해 볼 수 있습니다.
기본적인 틀은 Dynamic Routing과 동일하지만 위와 얘기한 것처럼 fallback과 revalidate가 필수적으로 사용된 것을 볼 수 있습니다.
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에서 가져와 사용)
// /isr/1, /isr/2 → params: [{ params: { productNo: '1' }}, { params: { productNo: '2' }}]
productNo: String(productNo),
},
};
}),
fallback: 'blocking', // prerendering되지 않은 페이지가 호출되면 초기 렌더링이 되기전에 데이터를 fetching한 뒤 렌더링 진행
};
} 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,
},
}),
]);
const product: Product = res.data;
if (product) {
return {
props: {
product,
},
revalidate: 30, // getStaticPaths에서 전달받은 params를 이용하여 fetching된 데이터의 유효시간 설정 (30초)
};
}
}
return {
notFound: true,
revalidate: 30,
};
};
interface Props {
product: Product;
}
const Isr = (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 Isr;
코드를 위와 같이 구성하고 다음 명령어를 이용하여 build를 해줍니다.
$ npm run build
build를 한 뒤 .next → server → pages를 확인하면 prerendering이 된 html과 json 파일들을 확인할 수 있습니다.
그리고 만들어진 1.json 파일을 확인해 보면 다음과 같이 fetching 된 데이터가 props로 사용되는 것을 확인할 수 있습니다.
{"pageProps":{"product":{"price":3200,"name":"첫번째 상품","stock":58}},"__N_SSG":true}
여기까지는 Dynamic Routing과 동일합니다.
이번 글에서는 ISR을 구현한 것이고 revalidate 타이머가 동작되지 않게 하기 위해 url에 접근하기 전에 API를 다음과 같이 수정해 보도록 하겠습니다.
이후로 다음 명령어를 이용하여 서버를 실행한 뒤 url에 접근해 보면 build 할 때 prerendering 된 데이터가 그대로 유지되어 있는 것을 확인할 수 있으며 Preview에도 fetching 되었던 데이터가 정상적으로 보이는 것을 볼 수 있습니다.
$ npm run start
그리고 30초 뒤에 새로고침을 해보면 다음과 같이 변경된 API가 적용되지 않고 기존 페이지가 그대로 보이는 것을 확인할 수 있습니다.
하지만 해당 시간대에 API로그를 확인해 보면 Next에서 API를 호출했던 로그가 남아있는 것을 확인할 수 있습니다.
2023-02-20 16:26:29.010 INFO 19340 --- [nio-8080-exec-1] o.a.c.c.C.[Tomcat].[localhost].[/] : Initializing Spring DispatcherServlet 'dispatcherServlet'
2023-02-20 16:26:29.018 INFO 19340 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet : Initializing Servlet 'dispatcherServlet'
2023-02-20 16:26:29.019 INFO 19340 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet : Completed initialization in 1 ms
2023-02-20 16:26:29.171 INFO 19340 --- [nio-8080-exec-1] c.q.o.controller.ProductController : servletPath: /product/detail
2023-02-20 16:26:29.171 INFO 19340 --- [nio-8080-exec-1] c.q.o.controller.ProductController : returnData: {price=3200, name=변경된 첫번째 상품, stock=58}
이 상태에서 url을 다시 새로고침해보면 변경된 API에 맞게 페이지가 재 구성된 것을 볼 수 있습니다.
또한 위에서 봤었던 1.json 파일을 다시 확인해 보면 props가 변경된 API에 맞게 수정된 것도 확인해 볼 수 있습니다.
{"pageProps":{"product":{"price":3200,"name":"변경된 첫번째 상품","stock":58}},"__N_SSG":true}
ISR - On-Demand Revalidation
ISR에서는 On-Demand Revalidation을 추가적으로 적용할 수 있습니다.
여기서 말하는 On-Demand Revalidation은 ISR의 revalidation을 더 효율적으로 사용할 수 있게 도와줍니다.
위에서 확인한 바와 같이 ISR은 Dynamic Routnig과 달리 rebuild를 하지 않고 revalidate에서 설정한 시간이 지났을 때 자동으로 변경된 데이터를 이용하여 prerendering을 진행해 줍니다.
하지만 여기서도 발생될 수 있는 문제점은 데이터가 변경되었더라도 사용자가 페이지를 방문해야만 revalidate 타이머가 동작하고 심지어 설정한 시간이 지나더라도 적어도 한 번 이상 사용자가 더 접근을 해야 변경된 데이터를 확인할 수 있습니다.
이런 문제점을 해결할 수 있게 도와주는 것이 On-Demand Revalidation이고 On-Demand Revalidation을 사용할 경우 데이터가 갱신되는 시점이 사용자의 페이지 방문이 아니라 Next에서 revalidation을 위한 trigger가 발생될 때로 변경됩니다.
먼저 On-demand Revalidation을 적용하기 위해서 Next 버전 확인이 필요합니다.
12.2.0 버전 이후로 정상적으로 사용 가능하다고 하니 만약 버전이 이보다 낮은 경우에는 적용할 수 없습니다.
그리고 적용하기 위해서는 Next의 API Routes를 사용해줘야 합니다.
간단한 예시로 다음과 같이 코드를 작성해 볼 수 있습니다.
먼저 API Routes를 위한 코드를 /pages/api 하위 경로에 다음과 같은 코드를 작성해 줍니다.
// /pages/api/product/detail/[productNo].ts
import { NextApiRequest, NextApiResponse } from 'next';
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
try {
await res.revalidate(`/isr/${req.query.productNo}`); // /isr/{productNo}에 해당되는 페이지에 revalidate 트리거 적용
return res.json({ revalidated: true });
} catch (err) {
return res.status(500).send('Error revalidating');
}
};
export default handler;
그리고 ISR이 적용된 코드는 다음과 같이 revalidation 트리거가 동작되는 API Routes를 호출할 수 있게 이벤트로 추가해 주며, revalidate는 따로 적용하지 않게 하여 트리거 없이는 revalidate가 되지 않도록 해보겠습니다.
// /pages/isr/[productNo].tsx
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에서 가져와 사용)
// /isr/1, /isr/2 → params: [{ params: { productNo: '1' }}, { params: { productNo: '2' }}]
productNo: String(productNo),
},
};
}),
fallback: 'blocking', // prerendering되지 않은 페이지가 호출되면 초기 렌더링이 되기전에 데이터를 fetching한 뒤 렌더링 진행
};
} 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,
},
}),
]);
const product: Product = res.data;
if (product) {
return {
props: {
productNo,
product,
},
// revalidate: 30, // On-demand Revalidation 처리를 위해 설정 제거
};
}
}
return {
notFound: true,
};
};
interface Props {
productNo: number;
product: Product;
}
const Isr = (props: Props) => {
/**
* handle
*/
const handle = {
clickRevalidation: async () => {
await axios.get(`/api/product/detail/${props.productNo}`);
},
};
return (
<div>
<h2>데이터 Fetch로 가져온 상품</h2>
<p>상품이름: {props.product?.name}</p>
<p>가격: {props.product?.price}</p>
<p>재고: {props.product?.stock}</p>
<button onClick={handle.clickRevalidation}>revalidation 트리거</button>
</div>
);
};
export default Isr;
코드를 작성하고 build를 한 뒤 서버를 실행하면 다음과 같은 결과를 확인해 볼 수 있습니다.
추가적으로 Next에서는 다음과 같이 revalidation이 무분별하게 처리되는 것을 방지하기 위해 secretToken을 사용하는 것을 가이드하고 있습니다.
불필요한 API 호출을 방지하기 위해 인증 과정을 추가한다면 On-Demand Validation이 더 효과적으로 사용될 것이라고 생각합니다.
https://<your-site.com>/api/revalidate?secret=<token>
// pages/api/revalidate.js
export default async function handler(req, res) {
// Check for secret to confirm this is a valid request
if (req.query.secret !== process.env.MY_SECRET_TOKEN) {
return res.status(401).json({ message: 'Invalid token' })
}
try {
// this should be the actual path not a rewritten path
// e.g. for "/blog/[slug]" this should be "/blog/post-1"
await res.revalidate('/path-to-revalidate')
return res.json({ revalidated: true })
} catch (err) {
// If there was an error, Next.js will continue
// to show the last successfully generated page
return res.status(500).send('Error revalidating')
}
}
이상으로 Data Fetching의 세 번째인 ISR에 대해 간단하게 알아보는 시간이었습니다.
읽어주셔서 감사합니다.
'SPA > Next' 카테고리의 다른 글
[Next] Apple 로그인 Spring을 활용하여 구현하기 (0) | 2023.03.20 |
---|---|
[Next] Data Fetching에 대해 알아보기 (4) - useQuery (0) | 2023.03.05 |
[Next] Data Fetching에 대해 알아보기 (2) - SSG / Dynamic Routing (0) | 2023.02.17 |
[Next] Data Fetching에 대해 알아보기 (1) - CSR / SSR (0) | 2023.02.12 |
[Next] next-sitemap 사용하기 (0) | 2023.02.08 |
댓글