NextJS "unstable_cache"

박재현·2024년 5월 14일
2

NEXT.JS

목록 보기
7/17
post-thumbnail

오랜만에 C언어가 아닌 NextJS다...

기억의 저편에서 사라져버린 NextJS 강의... 하지만 아직 완강을 못해서 마음한켠에 찝찝함과 뭔지모를 불안감이랄까?

오랜만에 강의를 이어 들으려고 재생했는데 10분도 되지않아서 허리가 미친듯이 아픈 기분...? ㅋㅋ

목감기도 걸리고...😨 (다들 감기 조심 하세요)

여튼 그래도 강의보다가 "unstable_cahce"라는걸 접했는데 와우 어썸한놈 같다는 느낌이 빡! 들어서 까먹기 전에 적어두려고 한다.

사실 data caching을 잘만 사용하면 성능적인 면이나 비용적인 면에서 많은 이점을 얻을 수 있지 않은가?

일단 기본적으로 우리의 강력한 NextJS는 fetch를 통해서 받아온 데이터들은 기본적으로 cached 상태로 만든다.

즉, fetch를 통해서 데이터를 받아오면, 이 값을 기억한다는 소리다.

문서 초반부에 그런식으로 설명이 되어있고, 쉬운 내용은 아닌듯하여 여러번 더 읽어 봐야할거같긴 하다.


여튼! fetch를 통해서 API의 response를 caching하는것 말고, prisma를 통해서 DB에 있는 값을 얻어올때 이것들을 cached 상태로 만들수는 없을까?

이럴때 사용가능한게 바로 unstable_cache다!

일단 당장 이름은 unstable인데, stable되면 이름은 아마 바뀌지 않을까 생각된다.

그래서 위 unstable_cache를 이용해서 크게 3가지 방법으로 DB를 조회한 데이터를 cached 상태로 만들 수 있다.


unstable_cache

먼저 unstable_cache는 "next/cache" 에서 import 시키면 된다.

사용방법은 생각보다 간단한데, react query하고 큰 차이가 없다.

첫번째 인자로 fetchData를 넣어줘야 하는데 이건 그냥 return해주는 값이 있는 함수를 넣어주면 해결된다.

문서에 적혀있듯이 must be a function that returns a Promise !

그리고 두번째 인자로는 keyParts 라는건데, react query를 사용할때 query key를 배열의 형태로 넣어주는데 그거랑 똑같다고 생각하면 될거같다.

따라서 2번째 인자로 넣어주는 값은 유니크한 키값이 되어야 한다는거고, 이 키값으로 cached된 데이터를 관리한다라는걸 알 수 있다.

그냥 하나의 커다란 오브젝트라고 생각하면 이해하기 편하지 않을까 라는 생각이든다. 아니면 local storage ??
key와 cached data가 서로 쌍으로 되어있는 모습인데!?

const getCachedProducts = unstable_cache(getInitialProducts, ["home-products"]);

async function getInitialProducts() {
    const products = await PrismaDB.product.findMany({
        select: {
            id: true,
            title: true,
            price: true,
            photo: true,
            created_at: true,
        },
        orderBy: {
            created_at: "desc",
        },
        // take: 1,
    });

    return products;
}

따라서 코드를 이런식으로 작성하게되면 getCachedProducts 가 호출되는 순간 아래와 같은 일이 일어난다.

  1. home-products 라는 key로 cached된 데이터가 있는지 찾아본다.
  2. 없다면 첫번째 인자로 넘겨준 Promise를 반환하는 함수를 실행하고 cached 상태로 만든다.

따라서 처음 실행이 될때는 cached된 data가 없기때문에 getInitialProducts 함수가 실행되겠지만, 이후에는 아무리 리프레쉬를 해봐도 getInitialProducts 함수가 실행되지 않는걸 확인할 수 있다.

멋지다!!!

근데 한가지 문제가 있다, 바로 update된 데이터를 가지고 오지 못하는데, 새로운 데이터로 업데이트 하기 위해서는 revalidate 라는 과정을 거쳐야한다.


revalidate

unstable_cache의 revalidate에는 크게 3가지 방법이 있는데, 첫번째로는 시간을 정해주는 방법이다.

const getCachedProducts = unstable_cache(
    getInitialProducts,
    ["home-products"],
    {
        revalidate: 60,
    }
);

unstable_cache 함수의 3번째 인자가 바로 options인데, 이 options에 revalidate 시간을 정해줄수 있다.

참고로 단위는 ms 단위가 아니라 sec 단위라서 1000을 곱하는 실수는 하지 말아야 한다!

또 중요한건 위에 적어주는 숫자는 interval이 아니라 threshold 개념으로 접근해야 한다.

즉, data가 cached된 시점에서 매 60초마다 revalidate해서 update를 하겠다는 의미가 아니라, 60초가 지나지 않은 시점에서 unstable_cache가 호출된다면 첫번째 인자로 넘겨준 함수를 실행시키지 않고 cached된 data를 넘겨주겠다는 소리다.

기억하자 60초마다 실행하는게 아니다.

NextJS가 "어? 이 cached된 데이터 너무 오래됐는데? 상했는데? 냄새나는데? 싱싱한거로 바꿔야 겠는데?" 라고 판단하는 기준이라는 의미다.

따라서, 60초가 지난 시점에서 새로운 요청이 있다면 그때 NextJS가 첫번쨰 인자로 넘겨준 함수를 다시 호출할거라는 의미다.

여튼, 이 방법도 꽤나 유용한 방법이라고 나는 생각한다.

꼭 실시간으로 업데이트가 될 필요가 없는 정보들은 이런식으로 revalidate되는 시간을 길게 갖고가면 DB를 hit하는 횟수가 줄어들면서 비용적인 측면에서도 긍정적인 효과를 갖고올것 같다.
(물론 사용자 경험 측면에서는 아닐 수 있긴하니까, 잘 조율을 해야 한다고 생각한다.)

다만 한가지 아쉬운 부분은 바로 "내가 원하는 시점에서 update" 하는게 어렵다라는 생각이 든다.


revalidatePath

그래서 사용할 수 있는게 바로 revalidatePath 라는 친구다.

이름으로 쉽게 유추할 수 있듯이, URL의 특정 path에 해당되는 cached된 data들을 update할 수 있도록 해준다.

일단 코드를 아래처럼 조금 수정해볼 필요가 있다.

const getCachedProducts = unstable_cache(getInitialProducts, ["home-products"]);

async function getInitialProducts() {
    console.log("run get initial products function!");
    const products = await PrismaDB.product.findMany({
        select: {
            id: true,
            title: true,
            price: true,
            photo: true,
            created_at: true,
        },
        orderBy: {
            created_at: "desc",
        },
        // take: 1,
    });

    return products;
}

export default async function Products() {
    const initialProducts = await getCachedProducts();

    async function Revalidate() {
        "use server";
        revalidatePath("/products");
    }

    return (
        <div>
            <form action={Revalidate}>
                <button>Revalidate</button>
            </form>

            <ProductList initialProducts={initialProducts} />
            <Link
                href="/products/add"
                className="flex items-center justify-center bg-green-500 rounded-full size-14 fixed bottom-24 right-8 text-[ghostwhite] transition-colors hover:bg-green-600"
            >
                <PlusIcon className="size-10" />
            </Link>
        </div>
    );
}

코드를 위와같이 조금 수정을 했는데 크게 많이 바꾼거는 없고, Inline Server Action 함수와 Form + Button을 추가했다.

그래서 Revalidate 라고 적혀있는 버튼을 누르면 서버액션, 즉 Revalidate 라는 함수가 실행된다.

그리고 해당 함수 내부에서는 revalidatePath 라는 함수에 특정 path를 적어주었다.

이러면 모든 준비는 끝난다.

참고로 inline server action function은 server component내부에서 작성가능한 함수다.

그러면 위 사진과 같이 가장 상단에 새로 만든 Revalidate 버튼이 보이는걸 확인할 수 있다.

그 다음 위 영상처럼 새로고침 버튼을 아무리 아무리 아무리 열심히 눌러도 아래 사진처럼 콘솔에 run get initial products function! 이라는 로그가 찍히지 않는걸 확인할 수 있다.

그럼 이번에는 만들어둔 Revalidate 버튼을 눌러보는건 어떨까?

위 영상처럼 버튼을 수차례 열심히 눌러보면, 아래 사진과 같이 전체 물건을 DB로 부터 읽어오는 함수가 실행되는걸 확인할 수 있다.

따라서 server action을 통해서 revalidatePath 함수를 이용하면 우리가 원하는 특정 path에 해당되는 cached된 데이터들을 update시킬수 있는걸 확인했다.

그럼 어떤식으로 써먹어볼 수 있을까 라고 생각을 해보면, 흠...
1. 사용자가 새로운 제품을 업로드 하고
2. server단에서 DB에 새로운 제품을 업로드하는 과정을 거치고
3. 정상적으로 업로드가 되었다면 redirect함수를 통해서 특정 path로 유저를 보내고
4. 동시에 revalidatePath 함수를 이용해서 user를 redirect하는 path를 함수 인자로 넘겨주면, 관련된 cached된 데이터를 업데이트 하도록

지금 당장 생각나는건 위와같은 시나리오에서 사용할 수 있을것 같다.

근데 한가지 아쉬운 부분이 남아있는데, 바로 "특정 cached key"에 해당하는 data만 update 하는게 불가능 하다라는 부분이다.

무슨말이냐???

위에 작성한 코드로 예시를 들어보자.

위 코드에서 Revalidate함수가 실행되면 revalidatePath("/products") 가 실행된다.

그러면 localhost:3000/products 해당 path에 관련된 unstable_cache가 예를들어서 3개라고 가정해보자.

my_product / all_product / recommendation 이렇게 3개라고 가정할때, 나는 my_product만 update하고 싶다라고 할때 가능한 방법이 없다.

즉, 여러개의 unstable_cache가 존재할때 특정 cached된 데이터만 update할 수 있는 방법은 없고, 특정 path의 모든 cached된 데이터가 업데이트 된다는 이야기다.

좋은건가 나쁜건가?? 라고 물어보면 전략에 따라 다르기때문에 뭐라 말은 못하겠지만 나에게 어떤 데이터를 업데이트 하고싶다! 라는 선택권이 없음은 확실하다.

그럼 어떤식으로 해야할까??


revalidateTag

바로 revalidateTag를 이용하면 된다.

일단 기본적으로 revalidatePath와 사용하는 방법에 있어서 큰 차이는 없다.

server action을 통해서 revalidateTag 함수를 호출하고, 함수 인자로 tag를 넣어만 주면 된다.

일단 그러면 revalidateTag를 이용해서 cached된 데이터를 다르게 업데이트 하는걸 봐야하니까 코드를 먼저 조금 수정해볼 필요가 있다.

const getCachedProduct = unstable_cache(getProduct, ["product-detail"], {
    tags: ["product-detail"],
});

const getCachedProductTitle = unstable_cache(
    getProductTitle,
    ["product-title"],
    {
        tags: ["product-title"],
    }
);

export async function generateMetadata({ params }: { params: { id: string } }) {
    const product = await getCachedProductTitle(+params.id);

    return {
        title: `${product?.title}`,
    };
}

async function getProduct(id: number) {
    console.log("run getProduct function!");
    const product = await PrismaDB.product.findUnique({
        where: {
            id: id,
        },
        include: {
            user: {
                select: {
                    username: true,
                    profile_photo: true,
                },
            },
        },
    });
    return product;
}

async function getProductTitle(id: number) {
    console.log("run getProductTitle function!");
    const product = await PrismaDB.product.findUnique({
        where: {
            id: id,
        },
        select: {
            title: true,
        },
    });
    return product;
}

일단 unstable_cache를 2번 사용했다.

하나는 metadata의 title을 위해서 사용했고, 하나는 화면에 정보를 렌더링하기 위해서 사용했다.

따라서 크롬 탭에 정보를 보여주는 부분과, 화면에 정보를 보여지는 부분은 서로 다른 함수를 통해서 정보를 가지고 온다는라는걸 기억해야 한다.

const getCachedProduct = unstable_cache(getProduct, ["product-detail"], {
    tags: ["product-detail"],
});

const getCachedProductTitle = unstable_cache(
    getProductTitle,
    ["product-title"],
    {
        tags: ["product-title"],
    }
);

다음으로 unstable_cache에 대해서 조금 더 살펴보면, 해당 함수의 3번째 인자를 위 코드와 같이 넘겨주면 된다.

이때 오해하면 안되는게, 2번째 인자인 Key는 무조건 유니크한 값이어야 하지만, 3번째 인자인 옵션으로 넘겨주는 tags는 유니크할 필요는 없다.

unstable_cache함수가 인스타그램의 게시물이라고 생각해보자!

인스타그램의 게시물은 서로다른 게시물에서 동일한 해시태그를 사용할 수 있지 않은가??? #OOTD, #food, #휴일, #귀멸의칼날 #귀칼 이런 해시태그들이 꼭 1개의 게시물에서 유니크하게 사용가능한게 아니라 여기저기 수많은 사람들이 다 사용할 수 있는것 처럼, unstable_cache의 tags도 유니크하지 않고 중복된 값을 사용할 수 있다.

그러면 revalidate는 어떤식으로 하면 좋을까?

async function Revalidate() {
  "use server";
  revalidateTag("product-title");
}

바로 이전에 했던것 처럼 서버액션을 통해서 revalidate하고 싶은 태그만 함수 인자로 넣어주면 된다.

위 코드를 보면 product-title 태그를 가진 cached된 데이터만 업데이트 될거라는걸 예상할 수 있는데 한번 확인해보자.

일단 강제로 새로고침 버튼을 눌러서 refresh를 시도해보지만 아래 사진과 같이 cached된 값들이 update 되지 않는걸 확인할 수 있다.

그럼 이번에는 Revalidate 함수가 호출될 수 있도록 만들어둔 Revalidate 버튼을 눌러보자.

그러면 예상한것 처럼 revalidate 되는걸 아래 사진과 같이 확인할 수 있다.

여기서 눈여겨 봐야할것은 getProdutTitle 함수만 실행이 된다는 부분인데, 이처럼 revaliateTag를 사용하면 update하고싶은 cached된 값을 내가 부분적으로 선별할 수 있다는 점이다.

만약 revalidatePath를 이용해서 현재 path를 넘겨주었다면, getProduct, getProductTitle 함수가 모두 실행이 되었을거다.

그렇다면, unstable_cache의 tag에 태그를 여러개 넣고, 공통된 태그를 revalidate 시키면 두개의 함수가 모두 다시 실행이 될까??

const getCachedProduct = unstable_cache(getProduct, ["product-detail"], {
    tags: ["product-detail", "detail", "post-malone"],
});

const getCachedProductTitle = unstable_cache(
    getProductTitle,
    ["product-title"],
    {
        tags: ["product-title", "title", "post-malone"],
    }
);

먼저 위 코드처럼 tags를 여러개 넣어보자! (기억하자, key는 유니크해야하지만 tags는 그러지 않아도 된다!)

그 다음 revalidateTag를 아래와 같이 중복된 tag인 post-malone으로 수정한 다음 다시한번 Revalidate 버튼을 눌러보자!

async function Revalidate() {
  "use server";
  revalidateTag("post-malone");
}

버튼을 열심히 눌러본 다음 로그를 확인해보면!

예상한대로 post-malone 태그를 가지고있는 getProduct, getProductTitle 함수 모두 다시 호출되어 실행되는걸 확인할 수 있다!

일단 사용해보니 확실히 알아두면 좋은 기능이라는 느낌을 받았다.

그리고 Server Action + Revalidate를 적절히 잘 사용하면 SSG / ISR을 흉내낼수 있을것 같다는 느낌도 받았는데, 이 부분은 좀 더 SSG / ISR먼저 더 찾아보고 해봐야겠다.

그보다 귀칼 넷플에 새로 나왔던데 얼른 보러 가야겠따!

profile
기술만 좋은 S급이 아니라, 태도가 좋은 A급이 되자

0개의 댓글

관련 채용 정보