
- #Optimization
- #Project
- #ModuReview
Next.js의 캐싱 기능 활용하기
Prolip
2025-08-16
Next.js에서 제공하는 캐싱 기능 활용하기, 주기적으로 HTML 파일을 생성하는 ISR, On-Demand 방식으로 fetch 캐시 무효화하기
시작..
이번에 진행한 게시판 프로젝트에서 선택했던 캐싱 전략에 대해 작성해보려고 합니다. Next.js를 사용하면서 생각보다 다양하게 캐싱이 가능하구나 싶었는데 까먹기 전에 얼른 기록해야겠습니다.
1. 메인페이지 - ISR + CSR
먼저 메인페이지에 대한 배경 설명이 필요합니다.
우리 프로젝트는 사용자들이 게시글(후기)을 작성해 다른 사용자들과 공유할 수 있는 커뮤니티형 프로젝트였는데요. 메인페이지는 사용자들의 게시글들 중에서 댓글, 북마크, 조회수 등을 점수로 치환해 선정된 베스트 후기들을 표시하고 있었습니다.
Next.js는 별도로 다이나믹 함수를 사용하거나 지정해주지 않는 이상 빌드 타임에 자동으로 SSG(Static-Site-Generation) 방식으로 페이지를 정적 캐싱하는데요.
메인페이지는 1시간마다 집계되는 베스트 후기를 사용자에게 보여주는 것이 핵심 기능인 페이지였기 때문에 별도로 SSR 렌더링 방식을 채택하게 되었습니다.
여기서 SSR은 렌더링 주체자가 서버이며, 렌더링 시점이 사용자가 페이지를 요청하는 시점입니다. 당연히 SSR은 사용자가 요청할 때마다 서버에서 그 시점에 데이터를 요청하고, HTML을 생성해 보여주니 데이터의 신선함을 보장하기 가장 적합하겠다고 판단했습니다.
단일 CPU의 한계
우리 프로젝트는 현재 AWS EC2 인스턴스에 배포한 상태입니다. 프리티어로 단일 코어 cpu를 이용하고 있는데요.
여러 지인들과 커뮤니티에 url을 뿌려 테스트하던 도중 여러 사람이 메인 페이지에 몰릴 때 페이지 로딩이 간헐적으로 느려지는 문제가 발생했습니다.
네트워크 탭에서 살펴본 결과 페이지 요청 후 pending 상태가 오래 지속되고 있었는데요. 새벽 시간에 혼자 접속했을 때는 곧바로 응답이 오고 있어 여러 요청이 모이면 단일 코어 cpu가 처리하지 못하겠다는 가설을 세우게 됩니다.
이 시점에 메인페이지는 SSR로 동작하고 있었습니다. SSR은 렌더링 주체자가 서버로 클라이언트에서 메인페이지를 요청하면 아래와 같이 동작할 거라고 생각합니다.
1. 자바스크립트 코드 실행
먼저 자바스크립트 코드를 실행합니다. Next.js 서버는 메인페이지에 필요한 모든 리액트 컴포넌트, 자바스크립트 코드를 실행합니다. (클라이언트 코드도 서버에서 한 번 실행됩니다. 콘솔 로그 하나 찍어보시면 서버 콘솔에 남아요.)
서버는 브라우저가 없는 환경에서 Node.js를 사용해 코드를 실행할테니 가상돔도 서버 메모리에 만들어질 겁니다.
2. 데이터 패칭, 전달
코드가 실행되며 베스트 후기 데이터를 백엔드 서버에 요청하고 이 데이터를 기반으로 베스트 후기 컴포넌트로 전달합니다.
렌더링 시작 전에 필요한 모든 데이터가 준비될 때까지 기다릴 거예요.
3. 직렬화
서버 메모리에 생성된 가상돔 트리를 클라이언트로 전달하기 위해 직렬화해야 합니다. 네트워크를 통해 전송할 수 있는 텍스트 형태로 변환해야 하니까요.
그럼 컴포넌트의 각 로직, 상태, 적용된 스타일 등을 모두 계산해 문저열로 만들어야하는데 이 작업이 가장 오래 걸릴 거 같다고 생각합니다.
이렇게 가설만 세우고 대응해 작업하면 이후 문제가 해결되지 않았을 때 다시 시간을 허비해야만 합니다. 그래서 가설을 검증하기 위해 부하 테스트 툴을 사용해 부하 테스트를 하게 되었는데요.
이 부하 테스트에 대한 내용은 다른 게시글로 정리하고 결과부터 보여드리자면.
초당 20명씩 60초간 요청했을 때, 무려 절반의 요청이 실패했으며 대부분의 사용자(p99)가 체감하는 응답 시간은 8.1초로 동시에 여러 요청이 들어왔을 때 서버가 감당하지 못한다는 사실을 알게 되었습니다.
이건 Next.js가 싱글 스레드 기반으로 동작하기 때문인데요. 한 번에 하나의 요청만 처리할 수 있다는 의미입니다. 먼저 들어온 요청을 처리하는 동안 그 뒤의 모든 요청은 처리되지 못하고 기다리게 됩니다.
가장 처음 사용자는 빠르게(2ms) 받지만 점점 서버 cpu에 부하가 걸려 요청이 지연된다면, 뒤에 들어오는 요청이 점점 쌓이고 결국 맨 뒤의 요청은 timedout으로 요청이 실패하거나 혹은 요청이 성공하더라도 오랜 시간(8.1s)을 대기하게 됩니다.
이 문제를 해결하기 위한 방법은 여러가지가 있는데, 먼저 pm2의 클러스터 모드가 있습니다. cpu 코어 수에 맞게 프로세스를 생성해 요청을 분산시킬 수 있습니다. 마치 로드밸런서와 같은데요. 프리티어 환경으로 단일 cpu 코어를 가지고 있기 때문에 불가능했습니다.
그럼 그냥 CSR 쓰면 됐지 않느냐
저도 처음에 의문을 가졌고 여기까지 읽어주신 여러분들도 의문을 가지실 수도 있습니다. CSR(Client-Side-Rendering)을 사용하면 사용자의 브라우저에서 항상 최신 데이터를 요청하니 신선도가 당연히 보장되지 않을까? 왜 굳이 서버에 부하가 가는 SSR을 선택했지?
가장 큰 이유는 SEO(검색 엔진 최적화) 때문이었는데요. 진행 중인 프로젝트의 메인 페이지에 노출되는 베스트 후기들은 댓글, 북마크, 조회수 등 여러 점수가 합산되어 선정된 후기들로 사용자들이 생성한 귀중한 컨텐츠입니다. 동시에 잠재적인 신규 사용자들이 검색으로 우리 서비스로 유입될 수 있는 경로라고 생각했습니다.
만약 베스트 후기 영역을 클라이언트 측에서 요청했다면 검색 엔진 크롤러는 HTML 내에서 해당 영역을 찾지 못했을 것이고, 연결된 경로를 인덱싱하지 못했을 것입니다. 이런 이유로 베스트 후기만큼은 서버에서 렌더링해 검색 엔진 최적화에 대응해야겠다고 판단했습니다.
ISR로 주기적으로 정적 캐싱하기
그래서 이 문제를 해결하기 위해 ISR(Incremental Static Regeneration) 방식을 사용하게 됐습니다. ISR은 빌드 타임에 HTML 파일을 한 번 생성하는 SSG와 다르게 지정된 시간 간격으로 서버에서 페이지를 정적으로 캐싱하는 방식입니다.
백엔드 서버에서 베스트 후기를 집계하는 시간에 맞게 메인페이지도 일정 시간 간격으로 페이지를 새로 생성해 캐싱하도록 적용했습니다.
export const revalidate = 3600; // 이러면 1시간 간격으로 재생성! export async function MainPage() { // ... }
이렇게 적용하면 서버는 1시간에 단 한 번만 HTML 파일을 생성하고, 그 외의 요청에는 모두 생성한 HTML 파일을 단순히 서빙하는 구조로 작동합니다.
그냥 반찬가게에 1시간 마다 반찬 만들어두고 손님들에게 만들어둔 반찬을 제공한다고 생각하시면 됩니다.
그래서 동일하게 20명씩 60초간 요청했을 때 얼마나 개선되었을까요?
1200건에 해당하는 요청이 모두 성공했으며 대부분의 사용자가 체감하는(p99) 응답 시간이 10ms로 약 800배 이상 개선되었습니다.
이후 초당 100명씩 30초간 총 3000개에 해당하는 부하 테스트도 진행했는데요.
여전히 무리 없이 안정적으로 요청을 처리할 수 있게 되었습니다.
그런데 실시간 후기는 클라이언트에서 요청하는데..?
실시간 후기 데이터는 클라이언트에서 데이터를 요청하고 있습니다. 그럼 제가 위의 제 관점과 상충되는 것처럼 보일 수 있는데요. 이 결정의 배경은 컨텐츠의 중요도와 역할에 대한 판단이 있었습니다.
- 베스트 후기: 먼저 베스트 후기 영역은 메인 페이지의 정체성이라고 볼 수 있었고, 검색 엔진에 반드시 노출되어야만 하는 핵심 정보였습니다. 그렇기에 SEO를 취우선으로 고려해 초기에 SSR을 채택했으며 이후에 성능을 위해 ISR로 변경했습니다.
- 실시간 후기: 이 영역은 사실 프로젝트 초기에 고려하지 않았던 영역입니다. “우리 서비스가 이렇게 활발하고 여러 컨텐츠가 있어요!”를 보여주기 위해 이후에 추가된 영역으로 SEO보다 사용자 경험에 포커스를 두고 개발했습니다.
그래서 실시간 후기에 해당하는 영역은 Next.js에서 제공하는 dynamic 함수를 사용해 불러오도록 구현했는데요.
export default async function MainPage() { const data = await getBestReviews(); return ( <section> <Hero /> <BestReviews reviews={data} /> <RecentReviews /> <ContactUs /> </section> ); } export default function RecentReviews() { return ( <article> <h2>최근 등록된 후기</h2> <RecentReviewsClient /> <Link href="/search" aria-label="더 많은 후기 보러가기"> 더 많은 후기 보기{` >`} </Link> </article> ); }
이렇게 메인페이지 컴포넌트와 실시간 후기 영역에 해당하는 RecentReviews
컴포넌트가 구현되어있습니다. 여기서 최대한 정적으로 캐싱할 수 있는 영역을 제외하고 클라이언트 측으로 보낼 코드를 RecentReviewsClient로 분리했어요.
import dynamic from 'next/dynamic'; const RecentReviewsCarousel = dynamic(() => import('./RecentReviewsCarousel'), { ssr: false, loading: () => <RecentReviewsCarouselLoading />, }); export default function RecentReviewsClient() { return ( <section> <RQProvider LoadingFallback={<RecentReviewsCarouselLoading />}> <RecentReviewsCarousel /> </RQProvider> </section> ); }
이제 클라이언트에서만 실행되도록 RecentReviewCarousel
컴포넌트는 Next.js에서 제공하는 dynamic 함수를 사용해서 불러오는데요.
- 첫 번째 인자로 import 문을 사용해 사용할 컴포넌트를 불러와요.
- 두 번째 인자로 옵션 객체를 전달하는데, 우리는 클라이언트에서만 이 코드를 실행하길 바라니 ssr 옵션을 false로 설정해요. 여기서 ssr 옵션을 비활성화하면 캐싱된 HTML이 전송될 때 이 컴포넌트 영역이 빈 공간으로 전송되는데요. 여기에 대체 UI를 loading 옵션으로 전달할 수 있어요.
이렇게 ISR과 CSR을 적절히 조합한 하이브리드 형식으로 메인페이지를 구현하게 되었습니다.
메인페이지를 최적화해야만 했던 이유
메인페이지는 대부분의 사용자가 서비스를 처음 마주하는 대문이라고 생각합니다.
어떤 맛집에 찾아갔는데 문이 고장나서 열리지 않거나 혹은 극단적이지만 문이 열리는데 10초씩 걸리면 화가 나서 다른 음식점을 찾을 수 있다고 생각합니다.
그정도로 사용자들은 내부에 좋은 컨텐츠가 얼마나 많던, 결국 경험할 기회를 제공 받아야 그 값어치를 제대로 느낄 수 있을 거예요.
그리고 현실적으로 서비스 트래픽이 대부분 메인페이지에 집중된다고 생각했습니다. 검색 페이지, 마이페이지, 작성페이지 등 다른 페이지들은 사용자의 행동에 따라 트래픽이 분산되는데 결국 그 모든 흐름의 시작이 메인페이지라고 생각했습니다.
따라서 부하가 가장 많이 발생하는 지점은 꼭 최적화해야만 한다고 생각했습니다. 우리 프로젝트의 핵심 가치가 결국 좋은 후기를 공유하는 것인데, 메인페이지가 그 가치를 베스트 후기라는 형태로 보여주는 공간이니 프로젝트의 정체성을 보여주는 페이지만큼은 무조건 빠르고 쾌적하게 보여줘야한다고 생각했습니다.
2. 게시글 상세보기 페이지 - Next.js의 on-demand 방식 활용하기
게시글 상세보기 페이지는 여러 사용자들이 공유한 경험이 담긴 핵심 컨텐츠를 제공하는 페이지입니다.
Next.js의 on-demand 방식을 사용함에 있어 배경을 설명해보자면, 게시글 본문은 작성자가 수정하거나 삭제하지 않는 한 모든 사용자에게 항상 동일한 내용을 보여줍니다.
이건 캐싱을 적용하기에 아주 이상적인 조건이었는데요. Next.js의 확장된 fetch에는 options 객체에서 캐시 옵션을 컨트롤할 수 있습니다.
const res = await fetch(url, { method: 'GET', next: { revalidate: false, tags: ['특정 태그'], }, });
- revalidate: 해당 속성을 false로 설정하게 되면 직접 무효화하기 전까지 해당 데이터를 무제한으로 캐싱할 수 있습니다. 공식 문서상으로 이 방식을 특정 이벤트에 따라 갱신하는 on-demand 방식으로 부르고 있습니다. 시간을 지정하면 지정한 시간 동안 캐싱하는 것도 가능합니다.
- tags: revalidate를 false로 설정하게 되면 revalidate 혹은 revalidateTag를 호출해 캐시를 무효화할 수 있는데요. 데이터에 ‘review-13’이라는 태그를 설정했다면, revalidateTag(’review-13’)을 호출해 캐시를 무효화할 수 있습니다.
이런 방법으로 최초 요청 시에만 백엔드 측으로 데이터를 요청하고 이후 요청에 대해서는 캐싱 중인 데이터를 반환하도록 구현했습니다.
데이터는 .next 폴더의 cache/fetch-cache 폴더에 저장되는데요. 아래처럼 응답 본문을 인코딩해 짧게 압축시켜 저장합니다.
{ "kind": "FETCH", "data": { "headers": { "access-control-allow-credentials": "true", "access-control-allow-headers": "Authorization, Content-Type, Accept", "access-control-allow-methods": "GET, POST, PUT, DELETE, OPTIONS", "cache-control": "no-cache, no-store, max-age=0, must-revalidate", "connection": "keep-alive", "content-type": "application/json", "date": "Fri, 29 Aug 2025 12:42:58 GMT", "expires": "0", "pragma": "no-cache", "server": "nginx/1.27.4", "strict-transport-security": "max-age=31536000 ; includeSubDomains", "transfer-encoding": "chunked", "x-content-type-options": "nosniff", "x-frame-options": "DENY", "x-xss-protection": "0" }, "body": "tuYW1lIjoi66as67ew7Ja0XzExZTk4ZThmIiwiY3JlYXRlZF9hdCI6IjIwMjUtMDgt", "status": 200, "url": "백엔드 주소" }, "revalidate": 31536000, "tags": [ "review-6" ] }
revalidateTag
위에서 설명한대로 캐시된 데이터를 직접 무효화 시켜줘야하는데요. 그렇다면 무효화를 언제, 어떻게, 어디서 해야 할까요?
당연히 작성자가 게시글을 수정하거나 삭제했을 때라고 생각합니다.
Next.js에서 태그 기반의 캐시를 무효화하는 기능을 revalidateTag
함수로 제공하고 있는데요. 공식 문서에서 말하길 Server Actions 혹은 Route Handler 내에서만 사용이 가능하다고 합니다.
이런 이유로 여기서 한 가지 제약이 생겨버렸는데요. 먼저 이 제약이 생긴 배경에 대해 설명해보자면, 우리 프로젝트는 현재 모든 요청을 tanstack/react-query를 통하도록 설계해놓은 상태입니다.
이유는 이전에 에러 처리 중앙화 게시글에서 작성했듯, queryCache, mutationCache의 onError 콜백과 throwOnError 속성을 통해 API 요청 중 발생한 에러를 한 지점으로 모아주기 위해서였습니다.
때문에 클라이언트에서 useMutation을 통해 게시글 수정, 삭제 요청을 보내는 구조에서 이 제약사항을 해결하고자 Next.js의 라우트 핸들러를 미들웨어처럼 활용하도록 구성하게 되었는데요.
이렇게 구현된 요청 흐름은 아래와 같아요.
┌ (PATCH,DELETE /reviews/[reviewId]) 클라이언트단 브라우저 => Next.js 서버 => 백엔드 서버 └ (PATCH,DELETE /api/reviews/[reviewId])
- 클라이언트 ⇒ Next.js 서버: 사용자가 저장 혹은 삭제 버튼을 클릭하면 요청은 백엔드 서버가 아닌 Next.js 서버의 API 라우트로 전송됩니다. (PATCH, DELETE /api/reviews/[reviewId])
- Next.js 서버 ⇒ 백엔드 서버: Next.js 서버는 클라이언트로부터 요청을 받고 백엔드 서버로 그대로 전달해요.
- 캐시 무효화: 백엔드 서버로부터 요청이 성공했음을 응답 받으면 Next.js 서버는
revalidateTag
를 사용해 수정 혹은 삭제한 게시글의 캐시를 무효화해요.
이런 방법으로 캐시 관리의 모든 제어권을 프론트 서버가 갖게 되는데요. 백엔드 서버는 따로 우리가 이런 게시글을 캐시하고 있다는 내용을 알 필요 없이 그냥 단순히 데이터 처리만 수행하면 되는 구조입니다.
무조건 옳았을까?
사실 이 구조가 이상적이라고만 생각하진 않습니다. 단순히 브라우저에서 백엔드 서버로 바로 요청을 보내는 것과 다르게 지금 구조에선 모든 수정 및 삭제 요청이 Next.js 서버를 거치게 되는데요.
그럼 모든 수정 및 삭제 요청이 Next.js 서버를 한 번 더 거치기 때문에 네트워크 홉이 한 단계 늘어나 직접 백엔드 서버에 요청하는 것보단 약간의 지연이 발생할 수 밖에 없습니다. 여기서 네트워크 홉은 출발지(브라우저)에서 목적지(백엔드 서버) 사이의 경로를 의미합니다.
하지만 읽기와 쓰기 요청의 빈도를 비교했을 때 이 트레이드오프가 충분히 가치 있을 거라고 판단했는데요. 제가 커뮤니티 등을 이용하면서 느낀 점은 게시글을 수정하는 행위(쓰기)보다 열람하는 행위(읽기)가 압도적으로 많다고 느꼈습니다.
가끔 발생하는 쓰기 요청의 작은 비용을 감수하는 대신 월등히 자주 발생하는 읽기 요청의 응답 속도를 선택하고 백엔드 서버의 부하를 줄이는 것이 큰 그림으로 보았을 때 서비스 품질 향상에 더 이득이라고 결론을 내리게 되었습니다.
어떻게 구현했는지 기록하기에 앞서 수정과 삭제 모두 동일한 흐름을 가지고 있기 때문에 삭제 요청만 코드로 첨부해볼게요.
useDeleteReview
export default function useDeleteReview() { const queryClient = useQueryClient(); const router = useRouter(); const {mutate, ...rest} = useMutation({ mutationFn: ({reviewId}: MutationVariables) => deleteReview(reviewId), onSuccess: (_data, {category}) => { toast.success({ title: '리뷰를 성공적으로 삭제했어요.', }); const invalidateKeys = [ reviewsQueryKeys.category.category('all'), reviewsQueryKeys.category.category(category), reviewsQueryKeys.keyword.all(), ]; invalidateKeys.forEach(key => { queryClient.invalidateQueries({queryKey: key}); }); router.push('/search'); }, }); return { deleteReview: mutate, ...rest, }; }
서버 상태 변경을 위해 useMutation
훅을 사용하고 있습니다. 외부로 노출되는 mutate 함수를 호출하면 deleteReview
라는 API 요청 함수로 게시글 아이디를 전달합니다.
요청이 성공된 경우 실행되는 onSuccess
핸들러를 보면, 클라이언트 측에서 캐시하는 데이터를 무효화할뿐 서버에 캐시 중인 게시글 캐시 무효화에는 일절 관여하고 있지 않습니다. 단순히 요청을 보내고 클라이언트 캐시만을 무효화하는 훅입니다.
deleteReview
export async function deleteReview(reviewId: number) { await requestDelete({ baseUrl: process.env.NEXT_PUBLIC_CLIENT_URL, endpoint: `/api/reviews/${reviewId}`, }); }
deleteReview
는 인자로 받은 reviewId를 조합해 DELETE 요청을 보내는데요. 여기서 중요한 점은 백엔드 서버가 아닌 Next.js 서버의 API 라우트(/api/reivews/[reviewId])로 요청을 보낸다는 점입니다.
/api/reviews/[reviewId]/route.ts
export async function DELETE(_: NextRequest, {params}: {params: Promise<{reviewId: string}>}) { const {reviewId} = await params; if (!reviewId) { return NextResponse.json( { title: 'REVIEW_ID_MISSING', detail: '리뷰 ID가 제공되지 않았습니다. 다시 시도해주세요', status: 400, }, { status: 400, }, ); } const cookieStore = await cookies(); const accessToken = cookieStore.get('accessToken'); const userNickname = cookieStore.get('userNickname'); if (!accessToken || !userNickname) { return NextResponse.json( { title: 'UNAUTHORIZED', detail: '로그인이 필요한 서비스입니다. 다시 로그인해주세요.', status: 401, }, { status: 401, }, ); } const res = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/reviews/${reviewId}`, { method: 'DELETE', headers: { Cookie: `accessToken=${accessToken.value}; userNickname=${encodeURIComponent(userNickname.value)}`, }, }); if (!res.ok) { const errorResponse = await res.json(); return NextResponse.json( { title: errorResponse.title || 'INTERNAL_SERVER_ERROR', detail: errorResponse.detail || '예상치 못한 서버 오류가 발생했습니다.', status: errorResponse.status || 500, }, { status: errorResponse.status || 500, }, ); } revalidateTag(`review-${reviewId}`); return NextResponse.json( { message: '리뷰가 성공적으로 삭제되었습니다.', }, {status: 200}, ); }
이제 미들웨어로 동작하는 API 라우트 핸들러를 확인해볼게요.
- 전달된 reviewId가 있는지 확인하고 예외처리합니다.
- 에러 구조는 우리 프로젝트에서 사용하는 구조로 title(에러를 나타낼 코드), detail(상세 내용), status(상태 코드)를 가지고 있어요.
- accessToken, userNickname 쿠키가 있는지 확인해요.
- 백엔드 측에서 사용자를 검증할 때 필요한 쿠키들로 모두 httpOnly, sameSite=Lax, Domain 등 여러가지 디렉티브를 사용하고 있는데요.
- 브라우저에서 Next.js 서버로 요청할 때는 브라우저가 알아서 쿠키를 전송하지만 Next.js 서버에서 백엔드 서버로 요청할 때는 쿠키를 직접 실어줘야 하기 때문에 쿠키를 조회하고 예외처리합니다.
- 위에서 모든 예외처리를 통과한 경우 실제 백엔드 서버로 요청을 보내게 되는데요. 이 때 쿠키 헤더에 accessToken, userNickname 쿠키를 실어 보냅니다.
- 요청이 실패한 경우 예외처리하고, 요청이 성공한 경우
revalidateTag
를 호출해 캐시를 무효화합니다.
한 가지 더 좋았던 점
이렇게 서버 측에서 데이터를 캐시하기 때문에 속도도 물론 빠르지만, SEO 측면에서도 큰 이점을 가져다줬는데요.
만약 게시글 본문 데이터를 브라우저에서 useEffect나 리액트 쿼리 등을 통해 가져왔다면, 구글과 같은 검색 엔진 크롤러는 내용이 텅 빈 HTML 페이지를 보니 페이지 인덱싱이 어려웠을 거라고 생각합니다.
하지만 위 방식은 서버 컴포넌트 내에서 fetch를 통해 데이터를 가져오고, Next.js 서버는 캐시된 데이터(첫 요청이라면 새로 가져오겠죠?)로 본문 내용이 모두 채워진 HTML을 클라이언트로 전송하니 크롤러는 게시글 페이지의 모든 텍스트, 구조를 제대로 분석할 수 있게 됩니다.
덕분에 우리 서비스에 작성된 소중한 후기들이 검색 결과에 잘 노출될 수 있는 기반이 마련되었고 아래 사진처럼 구글에 제대로 노출되는 것을 확인했습니다.
마치며
이렇게 Next.js에서 제공하는 여러 캐싱 기능을 활용해봤는데요. 잘 만들어진 프레임워크를 제대로 활용할 수 있다면 좋은 서비스를 만드는 데 금방 가까워질 수 있지 않을까 생각합니다..
전체적인 코드는 이 곳에 모두 공개되어있습니다. 혹시 좋은 의견이 있으시다면 꼭… 문의 부탁드립니다..꼭..!
