[NextJS] SSG (Static-Site-Generation)

상민·2022년 8월 5일
3

NextJS with TypeScript

목록 보기
3/6
post-thumbnail

React vs Next

React: CSR(Client Side Rendering)
Next: SSR(Server Side Rendering), SSG(Server Side Generation)

리액트는 CSR 방식이다.
우선 빈 HTML 파일을 먼저 받고(public/index.html) 그 후 리액트로 코딩한 여러 컴포넌트들을 번들링하여 전달하고, 브라우저는 해당 js 파일을 전달받은 hydration을 한다.

따라서 CSR에서는 처음 로딩되는 HTML 문서는 빈 페이지다
번들링 된 js 파일을 전달받기 전까지는 사용자는 빈 화면을 보게 되는 것이다

Next.js는 브라우저에서 렌더링 할 때 기본적으로 사전 렌더링을 해준다
사전 렌더링은 서버단에서 DOM 요소들을 Build하여 HTML 문서를 렌더링 하는 것을 말한다

사전 렌더링 된 HTML은 자바스크립트 요소가 빠진 가벼운 상태이기 때문에 클라이언트에서 빠르게 로딩이 가능하다
이후에 자바스크립트 요소들이 렌더링 될 때는 먼저 받아와진 HTML DOM 요소에 hydrate되는 것이기 때문에 웹 페이지를 다시 그리는 과정이 일어나지 않는다

리액트와 다르게 HTML에 마크업 구조가 포함되어 있기 때문에 SEO에도 좋다


SSR vs SSG

서버에서 사전 렌더링 하는 것까지가 Next.js의 특징이고
사전 렌더링을 동적으로 해서 페이지를 생성하느냐, 정적으로 페이지를 생성하느냐의 차이가 SSRSSG의 차이라고 보면 된다


SSG

SSG는 빌드를 진행할 때 pages 폴더에서 작성한 각 페이지들에 대해 각각의 HTML 문서를 생성해 static한 파일로 생성한다
그리고 해당 페이지에 요청이 올 때 마다 이미 생성된 HTML 문서를 반환한다

따라서 CSR보다 응답속도가 빠르고 Next.js에서도 SSG를 사용하는 것을 권장한다

getStaticProps

getStaticProps 함수는 모든 page파일에 추가할 수 있다

export async function getStaticProps(context) {...}

이렇게 하면 Next.js에서 사전 생성할 때 이 getStaticProps를 호출한다
이는 Next.js에 사전 생성해야하는 페이지임을 알리는 것이다

  • pages/index.tsx

import { GetStaticProps, NextPage } from "next";

interface IProduct {
  id: string;
  title: string;
}
interface HomePageProps {
  products: IProduct[];
}
const HomePage: NextPage<HomePageProps> = ({ products }) => {
  // getStaticProps에서 반환받은 props

  return (
    <ul>
      {products.map((item: IProduct) => (
        <li key={item.id}>{item.title}</li>
      ))}
    </ul>
  );
};

// 이게 먼저 실행되고 컴포넌트 함수가 실행될 것임
export const getStaticProps: GetStaticProps = async () => {
  // Client side에서는 실행되지 않음
  const products: IProduct[] = [{ id: "p1", title: "Product 1" }];
  return {
    props: {
      products,
    },
  }; // props키가 있는 객체를 반환
};
export default HomePage;

getStaticProps함수가 HomePage컴포넌트 보다 먼저 실행이 될 것이다
getStaticProps는 클라이언트 사이드에 제공되는 코드가 아니라 서버에서 실행되는 코드이다
즉 서버 측 작업을 수행할 수 있음을 의미한다

getStaticProps는 반드시 props키가 있는 객체를 반환해야 한다
반환된 객체가 HomePage 컴포넌트의 props로 전달이 되어져서 사용된다

  • pages/index.tsx

import { GetStaticProps, NextPage } from "next";
import fs from "fs/promises"; // 브라우저 측 자바스크립트가 파일 시스템에 접근할 수 없기 때문에 클라이언트 사이드에서는 fs 모듈 작업이 안됨
import path from "path";

interface IProduct {
  id: string;
  title: string;
  description: string;
}
interface HomePageProps {
  products: IProduct[];
}
const HomePage: NextPage<HomePageProps> = ({ products }) => {
  // getStaticProps에서 반환받은 props

  return (
    <ul>
      {products.map((item: IProduct) => (
        <li key={item.id}>
          {item.title} {item.description}
        </li>
      ))}
    </ul>
  );
};

// 이게 먼저 실행되고 컴포넌트 함수가 실행될 것임
export const getStaticProps: GetStaticProps = async () => {
  // Client side에서는 실행되지 않음
  const filePath = path.join(process.cwd(), "data", "dummy-backend.json"); // file경로를 루트 디렉토리의 data/dummy-backend.json
  const jsonData = await fs.readFile(filePath); // dummy-backend.json 파일을 읽음
  const { products } = JSON.parse(jsonData as unknown as string);
  return {
    props: {
      products,
    },
  }; // props키가 있는 객체를 반환
};
export default HomePage;

getStaticProps 함수는 브라우저 측 자바스크립트가 접근하지 않고 서버에서 실행되는 코드이므로 fs, path같은 파일시스템 모듈을 사용할 수 있다
그리고 Next.js는 이 모듈의 import 구문을 제거하고 클라이언트 사이드 코드 번들링을 한다

페이지 소스를 확인해보면 처음부터 모든 데이터가 포함되어 있는 것을 확인할 수 있다
서버에서 해당 페이지를 getStaticProps의 도움으로 사전렌더링 한 것을 알 수 있다
이는 Next.js가 페이지를 사전 렌더링 할 때만 실행된다
Next.js를 빌드할 때 getStaticProps 함수를 실행하여 해당 페이지의 HTML을 사전 생성하는 것이다

ISR(증분 정적 생성)

하지만 만약 자주 바뀌는 데이터라면 어떻게 될까?
페이지를 사전 생성하는 것은 꽤나 정적인 것을 구축하는 경우에는 좋은 방법이다
이 방식은 데이터가 바뀌면 다시 Next.js를 빌드하고 다시 배포해야 한다

첫 번째 해결책으로는 업데이트 된 데이터 페칭을 위해 useEffect를 리액트 컴포넌트에 추가하는 것이다
이는 사전 렌더링 된 데이터를 포함하지만 데이터가 오래됐을 수 있으니 백그라운드에서 최신 데이터를 페칭하여 페이지를 업데이트 하는것이다

두 번째 해결책은 ISR(증분 정적 생성)을 사용하는 것이다
getStaticProps 함수는 Next.js 프로젝트를 빌드할 때 실행되기도 하지만 반드시 이때 실행되는 것은 아니다
ISR은 페이지를 빌드할 때 정적으로 한 번만 생성하는 것이 아니라 배포 후에도 재배포 없이 계속 업데이트 된다는 뜻이다
따라서 페이지 사전 생성을 하긴 하지만 최대 X초마다 들어오는 요청에 대해 주어진 페이지를 Next.js가 재생성 하도록 할 수 있다

이를 수행하기 위해서 getStaticProps에서 반환하는 객체에 props뿐만 아니라 revalidate도 추가해주면 된다
값으로는 Next.js가 이 페이지를 재생성할 때까지 기다려야 하는 시간을 초단위로 설정한다

물론 개발서버에서는 항상 최신 데이터를가 포함된 최신 페이지가 표시되고 이는 프로덕션에서 적용되는 것이다

export const getStaticProps: GetStaticProps = async () => {
  // Client side에서는 실행되지 않음
  console.log("(Re-)Generating...");
  const filePath = path.join(process.cwd(), "data", "dummy-backend.json"); // file경로를 루트 디렉토리의 data/dummy-backend.json
  const jsonData = await fs.readFile(filePath); // dummy-backend.json 파일을 읽음
  const { products } = JSON.parse(jsonData as unknown as string);
  return {
    props: {
      products,
    },
    revalidate: 10 // 10초마다 재빌드
  }; // props키가 있는 객체를 반환
};

10초마다 재빌드하도록 설정하고 getStaticProps가 실행될 때 콘솔에 (Re-)Generating...를 출력하도록 해보자

npm run build 명령어를 입력하여 Next.js 프로젝트를 빌드하여 결과를 확인해보자

index 페이지가 ISR 방식으로 10초 주기로 재생성 되어야 한다는 것을 나타내고 있다
프로젝트를 빌드 했으니 npm start 명령어를 입력해 실제 프로덕션 모드로 사이트를 확인해보자

프로젝트를 서버를 실행하고 10초가 지난 뒤 페이지를 새로고침 해보면 콘솔에 (Re-)Generating...이 출력되는 것을 확인할 수 있다
이는 페이지를 재생성 했다는 것을 의미한다

getStaticProps 옵션

getStaticProps에서 리턴하는 객체에 대해서 알아보자
반환되는 객체의 키로 props, revalidate이외에 notFound가 있다

notFound는 불리언 값을 필요로 하는 프로퍼티다
값을 true로 설정하면 페이지가 404 에러를 반환하며 일반 페이지 대신 404 에러 페이지를 렌더링한다

export const getStaticProps: GetStaticProps = async () => {
  // Client side에서는 실행되지 않음
  console.log("(Re-)Generating...");
  const filePath = path.join(process.cwd(), "data", "dummy-backend.json"); // file경로를 루트 디렉토리의 data/dummy-backend.json
  const jsonData = await fs.readFile(filePath); // dummy-backend.json 파일을 읽음
  const { products } = JSON.parse(jsonData as unknown as string);
  return {
    props: {
      products,
    },
    revalidate: 10, // 10초마다 재빌드
    notFound: true // 404 에러 페이지를 렌더링
  }; // props키가 있는 객체를 반환
};

이는 getStaticProps함수에서 데이터 페칭에 실패하면 에러 페이지를 보여주는데 활용할 수 있다

  ...
if (data.products.length === 0) {
	return { notFound: true };
}
  ...

또 반환 객체에 redirect키를 설정할 수 있다
redirect키를 사용하면 사용자를 리다이렉션 할 수 있다
다시말해 페이지 컨텐츠나 컴포넌트 컨텐츠를 렌더링 하지 않고 다른 페이지로 리다이렉션 하는 것이다

이 역시 데이터 페칭에 실패할 경우 활용할 수 있는 키다

export const getStaticProps: GetStaticProps = async () => {
  // Client side에서는 실행되지 않음
  console.log("(Re-)Generating...");
  const filePath = path.join(process.cwd(), "data", "dummy-backend.json"); // file경로를 루트 디렉토리의 data/dummy-backend.json
  const jsonData = await fs.readFile(filePath); // dummy-backend.json 파일을 읽음
  const { products } = JSON.parse(jsonData as unknown as string);
  if (products) {
    return {
      redirect: { // redirect의 값으로 destination과 permanent 프로퍼티가 필요함
        destination: "/no-data", // products가 없는 경우 /no-data로 라우팅
        permanent: false, 
      },
    };
  }
  if (products.length === 0) {
    return { notFound: true };
  }
  return {
    props: {
      products,
    },
    revalidate: 10, // 10초마다 재빌드
  }; // props키가 있는 객체를 반환
};

getStaticProps에는 context가 인자로 올 수 있다
이 context는 Next.js로 실행될 때 페이지에 대한 추가 정보를 가진 매개변수이다
대표적인 예로 context를 통해 동적 매개변수값을 얻어올 수 있다

export async function getStaticProps(context) {...}

매개변수 context를 이용해서 동적 매개변수에 접근해 데이터를 받아와보자

  • pages/[pid].tsx

import { GetStaticProps, NextPage } from "next";
import path from "path";
import fs from "fs/promises";
import React from "react";
import { IProduct } from ".";

interface ProductDetailPageProps {
  product: IProduct;
}
const ProductDetailPage: NextPage<ProductDetailPageProps> = ({ product }) => {
  return (
    <>
      <h1>{product.title}</h1>
      <p>{product.description}</p>
    </>
  );
};

export const getStaticProps: GetStaticProps = async (context) => {
  // params는 context 객체의 프로퍼티 중 하나
  // params는 키-값 쌍이 있는 객체이며 키의 식별자는 동적 경로 세그먼트
  const { params } = context;
  const productId = params?.pid;
  const filePath = path.join(process.cwd(), "data", "dummy-backend.json"); // file경로를 루트 디렉토리의 data/dummy-backend.json
  const jsonData = await fs.readFile(filePath); // dummy-backend.json 파일을 읽음
  const { products }: { products: IProduct[] } = JSON.parse(
    jsonData as unknown as string
  );
  return {
    props: {
      product: products.find((product) => product.id === productId), // products 배열 중 동적 매개변수 id값과 일치하는 데이터만 반환
    },
  };
};

export default ProductDetailPage;

ProductDetailPage도 페이지를 사전 렌더링 하기 위해 getStaticProps 함수를 사용해서 데이터를 받아올 것이다
context 객체에는 params 프로퍼티가 있는데 params에 동적 경로 세그먼트 값이 들어있다
params를 이용해서 입력받은 동적 매개변수 값을 추출하여 해당 id값의 product를 ProductDetailPage에 props로 넘겨주었다


실행은 잘 되지만 product1의 상세 페이지에 접속하기 위해 클릭을 해보면 아래와 같은 에러가 날 것이다

getStaticPaths

Next.js 페이지들은 기본적으로 사전 생성을 하는데 동적 세그먼트를 가지는 페이지 (페이지 이름이 []로 된 경우)는 사전 생성하지 않는다
이 페이지는 하나가 아니라 여러 페이지로 이루어지기 때문이다

위 경우 ProductDetailPage는 [pid] 동적 매개변수를 갖는 페이지 이므로 기본 동작으로 페이지를 사전 생성하지 않는다

어떤 pid가 올지 모르기 때문에 동적 페이지는 기본적으로 사전 생성하지 못하고 대신 서버에서 항상 그때그때 생성되는 것이다

getStaticProps를 추가하면 페이지를 사전 렌더링 하도록 Next.js에 요청하기 때문에 작동하지 않는 것이다

Next.js에 동적 페이지에서 어떤 인스턴스가 사전 생성되어야 되는지 알려줄 수 있다
어떤 값에 대한 페이지가 사전 생성되어야 하는지 알아야 nextjs가 그 페이지의 여러 인스턴스를 사전 생성할 수 있게 된다
그 역할이 바로 getStaticPaths 함수이다

// 동적 페이지의 어떤 인스턴스를 생성할지 Next.js에 알리는 함수
export const getStaticPaths: GetStaticPaths = async () => {
  return {
    paths: [
      { params: { pid: "p1" } },
      { params: { pid: "p2" } },
      { params: { pid: "p3" } },
    ],
    fallback: false,
  };
};

export default ProductDetailPage;

getStaticPaths는 동적 페이지의 어떤 구체적인 인스턴스를 사전 생성할 지 알려주는 함수이다

getStaticPathspathsfallback키가 있는 객체를 반환해야 한다
paths에는 params키를 가진 객체의 배열인데, params 값에는 동적 세그먼트의 키-값 객체가 들어간다

코드를 조금 더 깔끔하게 작성해보자

// 동적 페이지의 어떤 인스턴스를 생성할지 Next.js에 알리는 함수
export const getStaticPaths: GetStaticPaths = async () => {
  const paths = Array.from({ length: 3 }, (_, index) => ({
    params: { pid: "p" + (index + 1) },
  }));
  return {
    // paths: [
    //   { params: { pid: "p1" } },
    //   { params: { pid: "p2" } },
    //   { params: { pid: "p3" } },
    // ],
    paths,
    fallback: false,
  };
};

위 코드대로 작성하면 동적 페이지가 세 번 사전 생성되어야 하며 세 가지 값을 가진다는 사실을 Next.js에 알릴 수 있다
그러면 Next.js는 각 Id에 대해 getStaticProps를 세 번 호출하고 해당 Id를 추출할 수 있다

npm run build 명령어를 입력하여 Next.js 프로젝트를 빌드하여 결과를 확인해보자

/[pid] 동적 페이지는 /p1, /p2, /p3세 가지 인스턴스를 생성한 것을 확인할 수 있다

fallback 키는 사전 생성되어야 할 페이지가 많을 때 도움이 된다
fallbacktrue로 설정하면 paths에 포함되지 않은 페이지라도 페이지 방문 시 로딩되는 값이 유효할 수 있도록 Next.js에 요청할 수 있다

하지만 페이지가 사전 생성되는 것은 아니고 요청이 서버에 도달하는 시점에 생성되는 것이다

fallback을 true로 설정하고 pid가 "p1"인 것만 사전 생성하도록 해보자

export const getStaticPaths: GetStaticPaths = async () => {
  const paths = Array.from({ length: 1 }, (_, index) => ({
    params: { pid: "p" + (index + 1) },
  }));
  return {
    // paths: [
    //   { params: { pid: "p1" } },
    //   { params: { pid: "p2" } },
    //   { params: { pid: "p3" } },
    // ],
    paths,
    fallback: true,
  };
};

이렇게 하면 product1, product2, product3 모두 링크를 눌렀을 때 렌더링이 잘 된다

하지만 p2, p3의 경우 링크를 클릭하지 않고 직접 URL에 입력하여 페이지를 요청하면 에러가 발생한다
그 이유는 동적 사전 생성 기능이 즉시 끝나지 않기 때문이다

따라서 컴포넌트에서 fallback 상태를 반환할 수 있게 해줘야 한다

ProductDetailPage 컴포넌트에서 콘텐츠를 리턴하기 전에 if문으로 product가 존재하는 체크를 한다
product가 없다면 아직 서버에서 받아오는 중이므로 로딩 컴포넌트를 보여주자

  • pages/[pid].tsx

import { GetStaticPaths, GetStaticProps, NextPage } from "next";
import path from "path";
import fs from "fs/promises";
import React from "react";
import { IProduct } from ".";

interface ProductDetailPageProps {
  product: IProduct;
}
const ProductDetailPage: NextPage<ProductDetailPageProps> = ({ product }) => {
  if (!product) return <div>Loading...</div>; // product가 존재하지 않으면 로딩
  return (
    <>
      <h1>{product.title}</h1>
      <p>{product.description}</p>
    </>
  );
};

export const getStaticProps: GetStaticProps = async (context) => {
  // params는 context 객체의 프로퍼티 중 하나
  // params는 키-값 쌍이 있는 객체이며 키의 식별자는 동적 경로 세그먼트
  const { params } = context;
  const productId = params?.pid;
  const filePath = path.join(process.cwd(), "data", "dummy-backend.json"); // file경로를 루트 디렉토리의 data/dummy-backend.json
  const jsonData = await fs.readFile(filePath); // dummy-backend.json 파일을 읽음
  const { products }: { products: IProduct[] } = JSON.parse(
    jsonData as unknown as string
  );
  return {
    props: {
      product: products.find((product) => product.id === productId), // products 배열 중 동적 매개변수 id값과 일치하는 데이터만 반환
    },
  };
};

// 동적 페이지의 어떤 인스턴스를 생성할지 Next.js에 알리는 함수
export const getStaticPaths: GetStaticPaths = async () => {
  const paths = Array.from({ length: 1 }, (_, index) => ({
    params: { pid: "p" + (index + 1) },
  }));
  return {
    // paths: [
    //   { params: { pid: "p1" } },
    //   { params: { pid: "p2" } },
    //   { params: { pid: "p3" } },
    // ],
    paths,
    fallback: true,
  };
};

export default ProductDetailPage;

product를 서버에서 받아오는 동안 로딩을 보여주고 받아오면 컴포넌트를 보여준다
이제 페이지를 새로고침 하거나 URL로 직접 접근하여도 페이지를 볼 수 있다

또 다른 방법으로는 fallback'blocking'으로 설정하는 방법이다
이 방법은 페이지가 서비스를 제공하기 전에 서버에 완전히 사전 생성되도록 Next.js가 기다리도록 한다
따라서 if문으로 fallback 체크를 해 줄 필요가 없다

하지만 이 방법은 페이지 방문자가 응답받는 시간이 길어진다

빠르게 무엇인가를 보여주는게 중요하다면 fallback을 true로, 방문자에게 불완전한 페이지를 보여주고 싶지 않다면 blocking으로 설정하는 것이 좋은 방법인 것 같다

동적으로 경로 로딩하기

위 방법은 getStaticPaths에 경로를 하드코딩을 했다
실제 작업에서는 데이터베이스나 파일로부터 이 정보를 패칭하여 작성한다

따라서 데이터를 페칭해오는 함수를 작성해서 사용해보자

  • utils/getProducts.ts

import path from "path";
import fs from "fs/promises"; // 브라우저 측 자바스크립트가 파일 시스템에 접근할 수 없기 때문에 클라이언트 사이드에서는 fs 모듈 작업이 안됨

export interface IProduct {
  id: string;
  title: string;
  description: string;
}

// products 데이터를 가져오는 함수
export const getProducts = async () => {
  const filePath = path.join(process.cwd(), "data", "dummy-backend.json"); // file경로를 루트 디렉토리의 data/dummy-backend.json
  const jsonData = await fs.readFile(filePath); // dummy-backend.json 파일을 읽음
  const { products }: { products: IProduct[] } = JSON.parse(
    jsonData as unknown as string
  );
  return products;
};
  • pages/[pid].tsx

import { GetStaticPaths, GetStaticProps, NextPage } from "next";
import React from "react";
import { getProducts, IProduct } from "../utils/getProducts";

interface ProductDetailPageProps {
  product: IProduct;
}
const ProductDetailPage: NextPage<ProductDetailPageProps> = ({ product }) => {
  // if (!product) return <div>Loading...</div>; // product가 존재하지 않으면 로딩, fallback을 true로 했을 경우 필요한 코드 'blocking'으로 한 경우는 필요 없음
  return (
    <>
      <h1>{product.title}</h1>
      <p>{product.description}</p>
    </>
  );
};

export const getStaticProps: GetStaticProps = async (context) => {
  // params는 context 객체의 프로퍼티 중 하나
  // params는 키-값 쌍이 있는 객체이며 키의 식별자는 동적 경로 세그먼트
  const { params } = context;
  const productId = params?.pid;
  const products = await getProducts();
  return {
    props: {
      product: products.find((product) => product.id === productId), // products 배열 중 동적 매개변수 id값과 일치하는 데이터만 반환
    },
  };
};

// 동적 페이지의 어떤 인스턴스를 생성할지 Next.js에 알리는 함수
export const getStaticPaths: GetStaticPaths = async () => {
  const products = await getProducts();
  const ids = products.map((product) => product.id);
  const paths = ids.map((id) => ({
    params: { pid: id },
  }));
  return {
    // paths: [
    //   { params: { pid: "p1" } },
    //   { params: { pid: "p2" } },
    //   { params: { pid: "p3" } },
    // ],
    paths,
    fallback: false, // true, false, 'blocking'
  };
};

export default ProductDetailPage;

getStaticPaths에서 fallback을 true로 설정하면 paths에 포함되지 않은 페이지라도 렌더링 할 수 있다
하지만 data에 없는 pid인 "p4"에 대한 페이지를 요청했을때 에러 페이지를 보여줘야 한다

이는 getStaticProps에서 설정할 수 있다
getStaticProps에서 pid가 "p4"인 product를 찾아서 만약 값이 없으면 notFount: true 객체를 리턴해주면 된다

  • pages/[pid].tsx

import { GetStaticPaths, GetStaticProps, NextPage } from "next";
import React from "react";
import { getProducts, IProduct } from "../utils/getProducts";

interface ProductDetailPageProps {
  product: IProduct;
}
const ProductDetailPage: NextPage<ProductDetailPageProps> = ({ product }) => {
  if (!product) return <div>Loading...</div>; // product가 존재하지 않으면 로딩, fallback을 true로 했을 경우 필요한 코드 'blocking'으로 한 경우는 필요 없음
  return (
    <>
      <h1>{product.title}</h1>
      <p>{product.description}</p>
    </>
  );
};

export const getStaticProps: GetStaticProps = async (context) => {
  // params는 context 객체의 프로퍼티 중 하나
  // params는 키-값 쌍이 있는 객체이며 키의 식별자는 동적 경로 세그먼트
  const { params } = context;
  const productId = params?.pid;
  const products = await getProducts();
  const product = products.find((product) => product.id === productId); // products 배열 중 동적 매개변수 id값과 일치하는 데이터만 반환
  // product가 없으면 not found 페이지를 보여줌
  if (!product) { 
    return {
      notFound: true,
    };
  }
  return {
    props: {
      product,
    },
  };
};

// 동적 페이지의 어떤 인스턴스를 생성할지 Next.js에 알리는 함수
export const getStaticPaths: GetStaticPaths = async () => {
  const products = await getProducts();
  const ids = products.map((product) => product.id);
  const paths = ids.map((id) => ({
    params: { pid: id },
  }));
  return {
    // paths: [
    //   { params: { pid: "p1" } },
    //   { params: { pid: "p2" } },
    //   { params: { pid: "p3" } },
    // ],
    paths,
    fallback: true, // true, false, 'blocking'
  };
};

export default ProductDetailPage;

p4에 접근했을 때 NOT FOUND 페이지가 보이는 것을 확인할 수 있다


참고: https://www.udemy.com/course/nextjs-react-incl-two-paths/

profile
FE Developer

0개의 댓글