변경된 데이터가 왜 반영이 안될까요? (Nextjs의 Data Cache)

김철준·2024년 12월 4일
0

next.js

목록 보기
16/18
post-thumbnail

Nextjs의 Data Cache

결론부터 말하자면 nextjs의 Data Cache의 기능으로 캐싱된 데이터를 계속 불러오기 때문입니다.

상황 및 원인 분석

퀴즈 관련 프로젝트를 진행하고 있습니다.
퀴즈 페이지는 하나의 퀴즈를 가지고 있습니다.
그리고 각 문제들은 관리자에서 관리해주고 있는 상황입니다.


2가지 상황에서 문제가 발생했는데요.

  1. 페이지에서 퀴즈 데이터 호출시
  2. 빌드시

차례로 상황을 살펴보겠습니다.

페이지에서 퀴즈 데이터 호출시

퀴즈 페이지에서 퀴즈 데이터 호출시, 관리자에서 변경한 데이터가 아닌 이전 데이터를 계속 호출하고 있는 상황입니다. 데이터가 최신화되지 않는 것이죠.

변경전 데이터

기존에 개발을 위해 관리자에서 다음과 같이 임의의 데이터를 집어넣었어요.


변경후 데이터

개발을 완료하고 관리자에서 위 퀴즈 데이터를 수정을 해줬는데요. 원래라면 다음과 같이 화면이 제가 원하는대로 나와야합니다.


관리자에서 데이터를 업데이트했지만 데이터가 변경전 데이터가 나온다?

하지만 관리자에서 업데이트한 데이터가 나오지않고 변경전 데이터가 화면으로 나옵니다.
확인해보니 데이터 자체도 이전 데이터가 들어오니, 당연히 변경전 데이터가 나오는 것이겠지요.


DB를 확인해보면 관리자에서 변경한 데이터가 잘 들어가 있는 것도 확인했습니다.

그렇다면 왜 이전 데이터가 나오는 것일까요?

빌드시에도 이전 데이터를 기반으로 페이지 생성

빌드를 할때도 마찬가진데요.

현재 저는 퀴즈 페이지를 SSG 방식으로 구성하고 있습니다.
빌드시에 페이지를 생성하는 방식이죠.

그래서 현재 DB에는 퀴즈 관련 데이터가 아래와 같이 3개의 데이터가 있습니다.

그렇다면 프론트에서도 빌드를 하게 되면 3개의 페이지가 나오는 것이 맞겠죠?

하지만 프론트에서는 아래와 같이 하나의 데이터에 대한 페이지 빌드하고 있습니다.

즉, 변경된 데이터가 아닌 이전 데이터에 대하여 빌드하고 있다는 건데요.

그렇다면 왜 이전 데이터에 대해 빌드를 하는걸까요?

cache

답은 캐시(cache)에 있었습니다.

서버에 있는 캐시 데이터를 불러오게 되어 변경후 데이터는 불러오지 않게 되는 것이죠.

nextjs 문서에 cache 관련 내용 중 Data Cache 부분을 살펴보면 이에 대한 내용이 자세히 나옵니다.

Data Cache

Data Cache
Next.js has a built-in Data Cache that persists the result of data fetches across incoming server requests and deployments.

Next.js는 서버에서 데이터를 요청(fetch)할 때, 데이터를 메모리에 저장해두는 Data Cache라는 공간을 제공하는데요.

이 Data Cache는 서버 요청 사이에서도 유지되며, 서버가 다시 시작되거나 코드가 배포(deployment)되더라도 계속 유지될 수 있습니다.

즉,nextjs 서버 캐싱 공간에 백엔드 API에서 불러왔던 데이터가 저장되있다는 것인데요.

때문에 DB에 있는 데이터가 변경된다하더라도 DB에 있는 변경된 데이터를 가져오는 것이 아닌, nextjs 서버내 캐싱 데이터를 계속해서 가져오는 것입니다.

백엔드 서버 로그를 통해서도 알 수 있는데요.

cache 설정 안할시

    // 퀴즈 상세 조회(상세 URL)
    async fetchQuizDetailByUrl(detailUrl: string): Promise<IResponse<QuizItem>> {
        return this.request<QuizItem>(`quiz/detail-url/${detailUrl}`, {
            method: "GET",
            cache: "no-store",
        });
    }

위는 API 요청 함수이며 fetch를 사용하고 요청하고 있는데요.
option 값으로 cache 값을 no-store로 설정하면 캐싱을 하지않도록 설정가능합니다.

const Page = async ({
    params
                    }:{
    params:{
        detailUrl:string
    }
}) => {

    const { detailUrl } = await params
    const {data} = await quizApiHandler.fetchQuizDetailByUrl(detailUrl)

    return (
        <QuizDetails
            quizData={data}
        />
    );
};

퀴즈 페이지에서 새로고침을 해보면 백엔드로 API 요청이 갈 것입니다.

그 다음에 백엔드 서버에서 로그를 확인해보니 아래와 같이 DB로 쿼리문이 나가고 있는 것을 확인할 수 있습니다.

페이지에 진입할 때마다 백엔드 서버로 API요청이 가고 있는 것인데요.

cache를 설정했을 때에는 어떨지 확인해보죠.

cache 설정시

  // 퀴즈 상세 조회(상세 URL)
    async fetchQuizDetailByUrl(detailUrl: string): Promise<IResponse<QuizItem>> {
        return this.request<QuizItem>(`quiz/detail-url/${detailUrl}`, 		{
            method: "GET",

        });
    }

기본적으로 별다른 옵션을 주지 않으면 cache를 사용하도록 설정됩니다.

const Page = async ({
    params
                    }:{
    params:{
        detailUrl:string
    }
}) => {

    const { detailUrl } = await params
    const {data} = await quizApiHandler.fetchQuizDetailByUrl(detailUrl)

    return (
        <QuizDetails
            quizData={data}
        />
    );
};

그리고 다시 새로고침을 해보도록 하겠습니다.

그리고 다시 백엔드 서버에서 로그를 확인해보면 DB로 아무 쿼리문도 나가지 않는 것을 확인할 수 있습니다.

즉, nextjs 서버에 있는 캐싱을 사용하고 백엔드로 API 요청을 하지 않게 하는 것이죠.

해결 방법

그렇다면 어떻게 최신 데이터들을 반영할 수 있을까요?

revalidate 활용하면 됩니다.

revalidate

nextjs 문서를 확인해보면

캐시된 데이터는 두 가지 방법으로 재검증할 수 있습니다:

  • 시간 기반 재검증: 일정 시간이 경과한 후 새 요청이 있을 때 데이터를 재검증합니다. 이는 데이터가 자주 변경되지 않고 신선도가 그리 중요하지 않은 경우에 유용합니다.
  • 온디맨드 재검증: 이벤트(예: 폼 제출)에 따라 데이터를 재검증합니다. 온디맨드 재검증은 태그 기반 또는 경로 기반 접근 방식을 사용하여 데이터를 한 번에 재검증할 수 있습니다. 이는 헤드리스 CMS의 콘텐츠가 업데이트될 때 가능한 빨리 최신 데이터를 표시하고 싶은 경우에 유용합니다.

저는 폼을 제출하는 것이 아니기 때문에 시간 기반 재검증 방법을 사용하면 될 것 같네요.

정해진 시간 간격으로 데이터를 재검증하려면 fetch의 next.revalidate 옵션을 사용하여 리소스의 캐시 수명을 초 단위로 설정할 수 있다고 합니다.

// Revalidate at most every hour
fetch('https://...', { next: { revalidate: 3600 } })

위와 같이 next.revalidate 옵션으로 3600을 설정하면 한 시간마다 API 요청을 하게 되는 것이지요.

next.revalidate 얼만큼 설정할까?

페이지에서 퀴즈 데이터 호출시

그렇다면 next.revalidate 시간은 요구사항에 따라 다를텐데요. 현재 저는 하루 단위로 퀴즈 데이터들을 등록,수정하고 있으니 하루만큼의 시간으로 설정하면 될 것 같아요.

// 하루마다 fetch
    async fetchQuizDetailByUrl(detailUrl: string): Promise<IResponse<QuizItem>> {
        return this.request<QuizItem>(`quiz/detail-url/${detailUrl}`, {
            method: "GET",
            next:{
                revalidate:86400
            }
        });
    }

위와 같이 설정하고 하루뒤에 보면

다음과 같이 최신데이터가 적절히 잘 반영되는 것을 확인할 수 있어요.

빌드시

하지만 위와 같이 설정하여도 빌드시에는 이전 데이터를 기반으로 페이지를 생성하는데요.

아래는 빌드시, generateStaticParams의 값에 따라 페이지를 생성하는 퀴즈 페이지 코드입니다.

import QuizDetails from "@/app/(page)/quiz/[detailUrl]/_components/client/quizDetails";
import {quizApiHandler} from "@/app/services/quiz/QuizApiHandler";
import {Metadata} from "next";
import React from 'react';


// export const config = { amp: true }
/**
 * 퀴즈 문제 페이지
 * 정적 렌더링 방식
 */

// SSG 실행할 페이지 ID 추출, 서버에 받아오는 PK들은 모두 SSG 방식으로 구현
export async function generateStaticParams() {

    const {data} = await quizApiHandler.fetchQuizDetailUrlList();

    return data.map((url) => ({detailUrl:url}))

}


// SEO를 위해 메타데이터(title, description) 설정
export async function generateMetadata({
                                           params
                                       }:{
    params:{
        detailUrl:string
    }
}):Promise<Metadata>{

    const detailUrl = (await params).detailUrl

    const {data} = await quizApiHandler.fetchQuizDetailByUrl(detailUrl)

    return {
        title:data.metaTitle,
        description:data.metaDescription,
        alternates:{
            canonical:`/quiz/${data.detailUrl}`
        }
    }
}

const Page = async ({
    params
                    }:{
    params:{
        detailUrl:string
    }
}) => {

    const { detailUrl } = await params
    const {data} = await quizApiHandler.fetchQuizDetailByUrl(detailUrl)

    return (
        <QuizDetails
            quizData={data}
        />
    );
};

export default Page;

generateStaticParams 함수를 활용하면 SSG 방식을 구현할 수 있습니다. 반환값으로 생성하고자하는 페이지의 params 값을 가진 배열을 넣어주면 배열에 있는 params에 대한 페이지들을 빌드시 생성해주죠.

그렇다면 빌드했을 시, 계속 이전 데이터가 호출되는 것의 원인은 위 코드에서 fetchQuizDetailUrlList에 있다고 추측할 수 있습니다.

   // 퀴즈 전체 DetailUrl 목록 조회
    async fetchQuizDetailUrlList(): Promise<IResponse<string[]>> {

        const response =  await this.request<string[]>("quiz/list-detail-url", {
            method: "GET",
            cache: "no-store"
        });

        const {data} = response

        // 배열이 비어있는 경우, 예외 처리
        ExceptionManager.throwIfArrayEmpty<string>(data,"퀴즈 URL 목록이 비어있습니다.")

        // 데이터가 없을 경우, 예외 처리
        ExceptionManager.throwIfNullOrUndefined(data,"퀴즈 URL 목록이 없습니다.")

        return response

    }

fetchQuizDetailUrlList함수의 역할은 DB에 있는 퀴즈 URL 목록을 모두 반환하는 역할을 하는데요.

이 반환값을 가지고 저는 모든 퀴즈 페이지들을 빌드할 때 생성하려고 하는 목적이었습니다.

하지만 위 코드를 확인해보면 option 값으로 아무것도 설정이 안되있죠?
캐시가 설정되있다는 것입니다.

즉, fetchQuizDetailUrlList함수 호출시, 캐싱 데이터를 호출하여 반환한다는거죠.

그렇기 때문에 fetchQuizDetailUrlList함수의 반환값은 변경된 데이터 ["javascript-closure","javascript-this","javascript-context"]가 아닌 이전 데이터["javascript-closure"]가 반환됩니다.

때문에 generateStaticParams에서는 이전 데이터["javascript-closure"]가 반환되기 때문에 "javascript-closure" params에 대해서만 빌드시 페이지를 생성하는 것이죠.

그렇다면 cache를 어떻게 설정하면 좋을까요? 어차피 빌드는 배포시에만 합니다. 즉,fetchQuizDetailUrlList 함수는 배포시에만 실행될 함수이기때문에 캐싱을 신경쓸 필요가 없는거죠.

그래서 저는 generateStaticParams에서 호출하는 API의 cache 옵션을 no-store로 설정해줬습니다.

   // 퀴즈 전체 DetailUrl 목록 조회
    async fetchQuizDetailUrlList(): Promise<IResponse<string[]>> {

        const response =  await this.request<string[]>("quiz/list-detail-url", {
            method: "GET",
            cache: "no-store"
        });

        const {data} = response

        // 배열이 비어있는 경우, 예외 처리
        ExceptionManager.throwIfArrayEmpty<string>(data,"퀴즈 URL 목록이 비어있습니다.")

        // 데이터가 없을 경우, 예외 처리
        ExceptionManager.throwIfNullOrUndefined(data,"퀴즈 URL 목록이 없습니다.")

        return response

    }
profile
FE DEVELOPER

0개의 댓글