김마드 2024. 8. 4. 12:28

1. nextCache 기능을 알아보자.

 

- cache를 통해 db에서 중복적으로 여러번 조회하는걸 막을 수 있다.

- 개발 모드 && 브라우저의 개발자 도구를 open한 상태라면 unstable_cache가 캐싱 된 데이터를 반환하지 않고 db에 접근하는 함수를 매번 실행함

- unstable_cache https://nextjs.org/docs/app/api-reference/functions/unstable_cache

- unstable_cache 는 나중에 nextjs에서 고유 이름이 바뀔 수 있으니 최신 정보 확인 필요

 

*home/page.tsx

import ProductList from "@/components/product-list";
import db from "@/lib/db";
import { PlusIcon } from "@heroicons/react/24/solid";
import { Prisma } from "@prisma/client";
import { unstable_cache as nextCache } from "next/cache";
import Link from "next/link";

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

async function getInitialProducts() {
  console.log("hit!!!!");
  const products = await db.product.findMany({
    select: {
      title: true,
      price: true,
      created_at: true,
      photo: true,
      id: true,
    },
    orderBy: {
      created_at: "desc",
    },
  });
  return products;
}

export type InitialProducts = Prisma.PromiseReturnType<
  typeof getInitialProducts
>;

export const metadata = {
  title: "Home",
};

export default async function Products() {
  const initialProducts = await getCachedProducts();
  return (
    <div>
      <ProductList initialProducts={initialProducts} />
      <Link
        href="/products/add"
        className="bg-orange-500 flex items-center justify-center rounded-full size-16 fixed bottom-24 right-8 text-white transition-colors hover:bg-orange-400"
      >
        <PlusIcon className="size-10" />
      </Link>
    </div>
  );
}

 

1) nextjs에서 제공하는 unstable_cache를 사용하면 되고, 첫번째 인자에는 db조회할 함수명, 두번째 인자에는 저장되는 고유 cache 이름이다.

2) 실제 사용할 때는 cache화 된 변수명을 사용 하면 된다. 

결과적으로, 새로고침을 하더라도 메모리에 저장된 cache 정보만 가지고 온다. (db를 계속 조회하지 않음)

그렇다면, 정보가 업데이트 될 때는 어떻게 해야하나?  이어서 설명 에정

 

*home/page.tsx

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

 

1) revalidate를 이용해서 사용자가 호출한 시점으로부터 특정 시간 이후에 캐시가 업데이트 되게끔 설정 할 수 있다.(내가 특정 시점 이후에 다시 호출 할 때 revalidate시간과 비교)

 

*home/page.tsx

import ProductList from "@/components/product-list";
import db from "@/lib/db";
import { PlusIcon } from "@heroicons/react/24/solid";
import { Prisma } from "@prisma/client";
import { unstable_cache as nextCache, revalidatePath } from "next/cache";
import Link from "next/link";

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

async function getInitialProducts() {
  console.log("hit!!!!");
  const products = await db.product.findMany({
    select: {
      title: true,
      price: true,
      created_at: true,
      photo: true,
      id: true,
    },
    orderBy: {
      created_at: "desc",
    },
  });
  return products;
}

export type InitialProducts = Prisma.PromiseReturnType<
  typeof getInitialProducts
>;

export const metadata = {
  title: "Home",
};

export default async function Products() {
  const initialProducts = await getCachedProducts();
  const revalidate = async () => {
    "use server";
    revalidatePath("/home");
  };
  return (
    <div>
      <ProductList initialProducts={initialProducts} />
      <form action={revalidate}>
        <button>Revalidate</button>
      </form>
      <Link
        href="/products/add"
        className="bg-orange-500 flex items-center justify-center rounded-full size-16 fixed bottom-24 right-8 text-white transition-colors hover:bg-orange-400"
      >
        <PlusIcon className="size-10" />
      </Link>
    </div>
  );
}

 

1) revalidatePath를 통해 고유 url에 접속하면 cache가 업데이트 되게끔 할 수 있다. 하지만 문제는 해당 url에서 cache화 하는 db 조회가 여러건이고, 내가 1건만 update하고 싶다면? 이런 상황에서는 적합하지 않을 수 있다.

 

*products/[id]/page.tsx

import db from "@/lib/db";
import getSession from "@/lib/session";
import { formatToWon } from "@/lib/utils";
import { UserIcon } from "@heroicons/react/24/solid";
import Image from "next/image";
import Link from "next/link";
import { notFound } from "next/navigation";
import { unstable_cache as nextCache, revalidateTag } from "next/cache";

async function getIsOwner(userId: number) {
  const session = await getSession();
  if (session.id) {
    return session.id === userId;
  }
  return false;
}

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

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

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

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

export async function generateMetadata({ params }: { params: { id: string } }) {
  const product = await getCachedProductTitle(Number(params.id));
  return {
    title: product?.title,
  };
}

export default async function ProductDetail({
  params,
}: {
  params: { id: string };
}) {
  const id = Number(params.id);
  if (isNaN(id)) {
    return notFound();
  }
  const product = await getCachedProduct(id);
  if (!product) {
    return notFound();
  }
  const isOwner = await getIsOwner(product.userId);
  const revalidate = async () => {
    "use server";
    revalidateTag("xxxx");
  };
  return (
    <div className="pb-40">
      <div className="relative aspect-square">
        <Image
          className="object-cover"
          fill
          src={`${product.photo}/width=500,height=500`}
          alt={product.title}
        />
      </div>
      <div className="p-5 flex items-center gap-3 border-b border-neutral-700">
        <div className="size-10 overflow-hidden rounded-full">
          {product.user.avatar !== null ? (
            <Image
              src={product.user.avatar}
              width={40}
              height={40}
              alt={product.user.username}
            />
          ) : (
            <UserIcon />
          )}
        </div>
        <div>
          <h3>{product.user.username}</h3>
        </div>
      </div>
      <div className="p-5">
        <h1 className="text-2xl font-semibold">{product.title}</h1>
        <p>{product.description}</p>
      </div>
      <div className="fixed w-full bottom-0  p-5 pb-10 bg-neutral-800 flex justify-between items-center max-w-screen-sm">
        <span className="font-semibold text-xl">
          {formatToWon(product.price)}원
        </span>
        {isOwner ? (
          <form action={revalidate}>
            <button className="bg-red-500 px-5 py-2.5 rounded-md text-white font-semibold">
              Revalidate title cache
            </button>
          </form>
        ) : null}
        <Link
          className="bg-orange-500 px-5 py-2.5 rounded-md text-white font-semibold"
          href={``}
        >
          채팅하기
        </Link>
      </div>
    </div>
  );
}

 

1) nextCache의 tag 기능을 통해 내가 원하는 것들만 선택적으로 revalidate해줄 수 있다. 또한 tag는 여러 캐시에 대해 중복으로 사용 할 수 있다.

지금까지 다룬 cache는 내가 직접 db를 조회 할 때 이고, 보통은 fetch를 통해 데이터를 가지고 온다. 이럴 때 어떻게 해야하는지 보자.

 

*products/[id]/page.tsx

import db from "@/lib/db";
import getSession from "@/lib/session";
import { formatToWon } from "@/lib/utils";
import { UserIcon } from "@heroicons/react/24/solid";
import Image from "next/image";
import Link from "next/link";
import { notFound } from "next/navigation";
import {
  unstable_cache as nextCache,
  revalidatePath,
  revalidateTag,
} from "next/cache";

async function getIsOwner(userId: number) {
  const session = await getSession();
  if (session.id) {
    return session.id === userId;
  }
  return false;
}

async function getProduct(id: number) {
  fetch("https://api.com", {
    next: {
      revalidate: 60,
      tags: ["hello"],
    },
  });
}

const getCachedProduct = nextCache(getProduct, ["product-detail"], {
  revalidate: 60,
  tags: ["product-detail", "hello"],
});

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

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

export async function generateMetadata({ params }: { params: { id: string } }) {
  const product = await getCachedProductTitle(Number(params.id));
  return {
    title: product?.title,
  };
}

export default async function ProductDetail({
  params,
}: {
  params: { id: string };
}) {
  const id = Number(params.id);
  if (isNaN(id)) {
    return notFound();
  }
  const product = await getCachedProduct(id);
  if (!product) {
    return notFound();
  }
  const isOwner = await getIsOwner(product.userId);
  const revalidate = async () => {
    "use server";
    revalidatePath("/home");
  };
  return (
    <div className="pb-40">
      <div className="relative aspect-square">
        <Image
          className="object-cover"
          fill
          src={`${product.photo}/width=500,height=500`}
          alt={product.title}
        />
      </div>
      <div className="p-5 flex items-center gap-3 border-b border-neutral-700">
        <div className="size-10 overflow-hidden rounded-full">
          {product.user.avatar !== null ? (
            <Image
              src={product.user.avatar}
              width={40}
              height={40}
              alt={product.user.username}
            />
          ) : (
            <UserIcon />
          )}
        </div>
        <div>
          <h3>{product.user.username}</h3>
        </div>
      </div>
      <div className="p-5">
        <h1 className="text-2xl font-semibold">{product.title}</h1>
        <p>{product.description}</p>
      </div>
      <div className="fixed w-full bottom-0  p-5 pb-10 bg-neutral-800 flex justify-between items-center max-w-screen-sm">
        <span className="font-semibold text-xl">
          {formatToWon(product.price)}원
        </span>
        {isOwner ? (
          <form action={revalidate}>
            <button className="bg-red-500 px-5 py-2.5 rounded-md text-white font-semibold">
              Revalidate title cache
            </button>
          </form>
        ) : null}
        <Link
          className="bg-orange-500 px-5 py-2.5 rounded-md text-white font-semibold"
          href={``}
        >
          채팅하기
        </Link>
      </div>
    </div>
  );
}

 

1) 실제 위 코드는 돌아가지 않으니 참고(테스트용으로 fetch 쓴것임)

2) fetch 부분을 보면 이전에 nextCache 한것과 동일하게 적용 할 수 있으니 참고하자.

 

2. build시 cache 관련

- npm run build 후 npm run start 하게 되면 build된 코드가 실행됨

 

<npm run build 후 cmd창에 나오는 화면이고, O는 static 페이지, λ는 dynamic 페이지를 뜻한다. (nextjs가 자동으로함)

- static과 dynamic 구분자는 여러가지가 있는 것 같다. 누가 보느냐에 따라서 달라지는 페이지다 싶으면 dynamic으로 nextjs에서 구분한다. (해당 url에서 사용하는 쿠키, header, url params 등을 보는 것 같다..)

- dev 환경이 아닌 build를 하게 될 때는 자동으로 url에 따라 static과 dynamic 페이지로 구분이 된다. dev 환경에서 새로고침할 때 마다 db 조회가 되었던게, build 이후에는 안될 때가 있다.

 그 이유는 nextjs에서는 쿠키에 따라서 다이내믹 페이지 인지 아닌지 확인 하고 있기 때문이다. profile 같은 경우는 쿠키 id를 가져와 유저 정보를 확인하기 떄문에 다이내믹 페이지로 새로고침 시 db가 새롭게 계속 업데이트 된다.

하지만 /home 은 db를 새로고침 할 때 마다 db가 조회되어야 함에 불구하고, static으로 구분되어 있다. 따라서 새로고침을 해도 build시 자동으로 저장되는 cache 데이터만 불러오고, 업데이트 된 db 정보를 불러오지 못한다.(테스트로, 단순 다이렉트로 db 조회코드만 넣었고, 위~ 에서 배운 cache 관련것들은 따로 추가 않은 상태)

 결론적으로, 내가 이런 상황을 내 입맛에 맞게 컨트롤 하기 위한 방법을 알아보자.

 

방법

1) 위에서 배운 cache를 적절하게 사용하기

2) 리다이렉팅 시 revalidatePath호출하기

3)  export const dynamic = 'force-dynamic'로 해당 페이지에 넣으면 강제로 다이내믹 페이지가됨

export const revalidate = 60를 해당 페이지에 넣으면 static 페이지라고 하더라도, 60초 뒤에 새로운 데이터를 받아 올 수 있음. (요거를 적절히 사용 하면 될듯)

**아래 내용 참고

Route Segment Config
https://nextjs.org/docs/app/api-reference/file-conventions/route-segment-config

 

File Conventions: Route Segment Config | Next.js

Learn about how to configure options for Next.js route segments.

nextjs.org

 

3. generateStaticParams

 

-이 강의에서 기본적으로 상품 상세 페이지는 dynamic 페이지이다. 하지만 특정 고유 id(url에 변수가 있는 케이스)에 대한 리턴값을 우리가 확실히 알고 있다면 그 페이지를 static으로 바꿔 줄 수 있다.

 

*products/[id]/page.tsx

import db from "@/lib/db";
import { formatToWon } from "@/lib/utils";
import { UserIcon } from "@heroicons/react/24/solid";
import Image from "next/image";
import Link from "next/link";
import { notFound } from "next/navigation";
import {
  unstable_cache as nextCache,
  revalidatePath,
  revalidateTag,
} from "next/cache";

async function getIsOwner(userId: number) {
  // const session = await getSession();
  // if (session.id) {
  //   return session.id === userId;
  // }
  return false;
}

async function getProduct(id: number) {
  const product = await db.product.findUnique({
    where: {
      id,
    },
    include: {
      user: {
        select: {
          username: true,
          avatar: true,
        },
      },
    },
  });
  return product;
}

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

async function getProductTitle(id: number) {
  const product = await db.product.findUnique({
    where: {
      id,
    },
    select: {
      title: true,
    },
  });
  return product;
}

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

export async function generateMetadata({ params }: { params: { id: string } }) {
  const product = await getCachedProductTitle(Number(params.id));
  return {
    title: product?.title,
  };
}

export default async function ProductDetail({
  params,
}: {
  params: { id: string };
}) {
  const id = Number(params.id);
  if (isNaN(id)) {
    return notFound();
  }
  const product = await getCachedProduct(id);
  if (!product) {
    return notFound();
  }
  const isOwner = await getIsOwner(product.userId);
  const revalidate = async () => {
    "use server";
    revalidateTag("xxxx");
  };
  return (
    <div className="pb-40">
      <div className="relative aspect-square">
        <Image
          className="object-cover"
          fill
          src={`${product.photo}/width=500,height=500`}
          alt={product.title}
        />
      </div>
      <div className="p-5 flex items-center gap-3 border-b border-neutral-700">
        <div className="size-10 overflow-hidden rounded-full">
          {product.user.avatar !== null ? (
            <Image
              src={product.user.avatar}
              width={40}
              height={40}
              alt={product.user.username}
            />
          ) : (
            <UserIcon />
          )}
        </div>
        <div>
          <h3>{product.user.username}</h3>
        </div>
      </div>
      <div className="p-5">
        <h1 className="text-2xl font-semibold">{product.title}</h1>
        <p>{product.description}</p>
      </div>
      <div className="fixed w-full bottom-0  p-5 pb-10 bg-neutral-800 flex justify-between items-center max-w-screen-sm">
        <span className="font-semibold text-xl">
          {formatToWon(product.price)}원
        </span>
        {isOwner ? (
          <form action={revalidate}>
            <button className="bg-red-500 px-5 py-2.5 rounded-md text-white font-semibold">
              Revalidate title cache
            </button>
          </form>
        ) : null}
        <Link
          className="bg-orange-500 px-5 py-2.5 rounded-md text-white font-semibold"
          href={``}
        >
          채팅하기
        </Link>
      </div>
    </div>
  );
}

export async function generateStaticParams() {
  const products = await db.product.findMany({
    select: {
      id: true,
    },
  });
  return products.map((product) => ({ id: product.id + "" }));
}

 

 

1) 맨 아래쪽 보면 generateStaticParams 함수가 있다. (고유이름을 써야함) 해당 함수의 리턴값은 반드시 배열이 들어가야하고, 해당 리턴값에 들어간 값들의 url은 static 페이지가 된다.

 

2) 아래 cmd 화면을 보면 19,20,21,22번 id의 페이지가 static이 되었는데, 추가로 상품을 생성하면 23번째 id도 자동으로 static페이지로 추가가 된다.

 

export const dynamicParams = true; 가 디폴트 값으로 따로 코드에 넣을 필요는 없지만, 만약 위처럼 추가되는 페이지에 대해 자동으로 static 페이지로 만들고 싶지 않다면 코드를 추가해, false로 바꿔주어야한다.

 

*빌드 후 cmd 화면

 

**코드 챌린지

-1. 캐싱 전략을 짜고 상품 업로드, 수정, 삭제할 때마다 revalidate하기
-2. 상품 수정 페이지 만들기

https://nomadcoders.co/carrot-market/lectures/4839