본문 바로가기

Frontend/Next.js

Next.js를 배워보자-3

페이지 사전 렌더링 & 데이터 페칭

표준 React라면 빈 HTML 파일과 모든 js 코드가 표시될 것이다.

그리고 js 코드가 실행되면 내용이 표시될 것이다.

이때 서버로부터 데이터를 불러오는 경우 시간이 소요될 수도 있다.

이는 React사전 렌더링된 페이지를 반환하지 않기 때문이다.

 

사용자가 페이지를 방문하면 Next.js사전 렌더링된 페이지를 반환한다.

사전에 HTML 페이지를 완성해놓고 완전히 채워진 HTML 파일을 클라이언트에게 전송하게 된다.

그래서 SEO 관점에서 좋다는 것이다.

 

또한, Next.js는 포함된 js 코드를 모두 재전송하는데 이를 hydrate라고 한다.

이 코드들은 나중에 사전 렌더링된 페이지를 대체하고 React는 그에 알맞은 작업을 한다.

 

사전 렌더링은 오직 최초 로딩하는 경우에만 영향을 미친다.

사용자가 페이지를 방문하면 로딩되는 첫 번째 페이지가 사전 렌더링된 것이다.

Next.js와 React로 구성된 페이지에서는 페이지의 hydration이, 즉 첫 번째 렌더링이 끝나면 다시 SPA로 돌아간다.

 

그때부터는 React가 처리하게 된다.

그 후, 페이지가 바뀔 때는 해당 페이지는 사전 렌더링되지 않고 React를 통해 클라이언트 사이드에서 생성된다.

 

Next.js에는 사전 렌더링하는 방법이 두 가지 있다.

하나는 정적 생성, 다른 하나는 서버 사이드 렌더링(SSR)이다.

정적 생성빌드되는 동안 모든 페이지가 사전 생성되고, SSR배포 후 요청이 서버까지 오는 바로 그때 모든 페이지가 생성된다.

 

정적 생성 (getStaticProps 이용)

일반적으로 권장되는 방식이다.

정적 생성빌드하는 동안 페이지를 사전 생성하는 것이다.

빌드 시간은 배포 전 애플리케이션을 구축할 때를 말한다.

여기서 사전 생성은 콘텐츠를 구성하는 모든 HTML 코드와 모든 데이터를 사전에 준비시켜 놓는다는 뜻이다.

즉, 보통 서버 사이드에서만 실행되는 코드를 빌드 프로세스 동안 실행되도록 허용하는 것이다.

그래서 배포되고 나면 해당 페이지는 서버나 앱을 실행시키는 CDN을 통해서 캐시로 저장된다.

그래서 즉시 입력 요청이 실행될 수 있는 것이다.

 

페이지가 다 표시된 후에도 React 앱을 통해 hydrate 작업을 거친다.

여기서 문제는 사전 생성할 페이지에 어떤 데이터가 포함되어야 하는지 어떻게 지정하는 지이다.

이는 페이지 컴포넌트에서 가져올 수 있는 특정 함수에서 찾을 수 있다.

이 함수는 pages 폴더의 component 파일 내부에 있어야 한다.

그 안에서 특수한 비동기 함수인 getStaticProps를 가져오는데, 이 함수에서는 보통 서버 사이드에서만 실행되는 모든 코드도 실행할 수 있다는 것이다.

 

또한, 이 함수 내부 코드는 클라이언트에게 재전송되는 코드로 포함되지 않는다.

해당 함수 내 포함하는 코드는 클라이언트는 볼 수 없다.

페이지가 로드된 후에만 클라이언트 사이드에서 전송되는 HTTP 요청 대신 컴포넌트를 생성하기 전에 Next.js가 데이터를 프리페치해야 한다.

 

그리고 해당 함수는 항상 객체를 반환하도록 해야 한다.

getStaticProps는 위 컴포넌트에 대한 props를 준비하는 것이라고 보면 된다.

 

getStaticProps 함수를 먼저 실행하고 그 위 컴포넌트가 실행된다.

const HomePage = (props) => {
  const { products } = props;

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

export async function getStaticProps() {
  return {
    props: {
      products: [{ id: "p1", title: "Product 1" }],
    },
  };
}

export default HomePage;

 

아래와 같이 파일 시스템을 활용해서 서버 사이드 코드를 실행해볼 수도 있다.

import path from "path";
import fs from "fs/promises";

const HomePage = (props) => {
  const { products } = props;

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

export async function getStaticProps() {
  const filePath = path.join(process.cwd(), "data", "dummy-backend.json");
  const jsonData = await fs.readFile(filePath);
  const data = JSON.parse(jsonData);

  return {
    props: {
      products: data.products,
    },
  };
}

export default HomePage;

 

증분 정적 생성(ISR 이용)

위에서 사용해본 getStaticProps 함수의 경우, 빌드 시간에 데이터를 불러온다.

하지만 자주 바뀌는 데이터라면, 페이지를 다시 빌드하고 다시 배포해야 해서 불편함이 있을 수 있다.

 

이에 대한 해결책으로 페이지를 사전 빌드는 하지만 서버에서 업데이트된 데이터 페칭을 위해 useEffect를 사용하는 React 컴포넌트에 표준 React 코드를 포함시키는 방법이 있다.

즉, 항상 사전 렌더링된 데이터를 일부 포함해 페이지를 제공하지만 백그라운드에서 데이터를 불러와 그 데이터가 도착한 후에 로드된 페이지를 업데이트하는 것이다.

 

또 다른 해결책이 있는데, 증분 정적 생성(ISR)이다.

이 방법은 페이지를 빌드할 때 정적으로 한 번만 생성하는 것이 아니라 배포 후에도 재배포 없이 계속 업데이트하는 것이다.

페이지를 사전 생성하긴 하지만 주어진 시간마다 페이지를 재생성할 수 있도록 할 수 있다.

 

그래서 들어오는 요청에 대해 서버에서 사전 렌더링을 계속 수행할 수 있고 이를 수행하기 위해서는 getStaticProps에서 반환하는 객체에서 props를 반환할 뿐만 아니라 revalidate라고 하는 두 번째 키도 추가하면 된다.

이때 재생성할 때까지 기다려야 하는 시간을 값으로 넣어주면 된다.

개발하는 경우에는 이 값과 관계 없이 항상 새로 생성되는 페이지를 볼 수 있지만, 프로덕션인 경우 해당 값에 따라 변할 것이다.

 

또한 getStaticProps 함수에 인자로 context를 추가할 수 있는데, Next.js에 의해 호출되고 인수를 받기 때문이다.

페이지에 대한 추가 정보를 알려주는 것이다.

 

또한 notFound 키를 추가적으로 반환할 수 있는데 이 키 값을 true로 설정하면 페이지가 404 오류를 반환하고 404 페이지를 렌더링한다.

데이터를 패치하는 코드가 페칭에 실패하면 404 페이지를 렌더링하는 용도로 사용한다.

 

import path from "path";
import fs from "fs/promises";

const HomePage = (props) => {
  const { products } = props;

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

export async function getStaticProps(context) {
  const filePath = path.join(process.cwd(), "data", "dummy-backend.json");
  const jsonData = await fs.readFile(filePath);
  const data = JSON.parse(jsonData);

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

  return {
    props: {
      products: data.products,
    },
    revalidate: 10,
  };
}

export default HomePage;

 

또한 redirect 키도 추가할 수 있어서 다른 페이지로 리다이렉션이 가능하다.

이 경우도 데이터를 가져오는 데 문제가 있는 경우 사용한다.

데이터 자체가 없을 때 사용할 수 있다.

import path from "path";
import fs from "fs/promises";

const HomePage = (props) => {
  const { products } = props;

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

export async function getStaticProps(context) {
  const filePath = path.join(process.cwd(), "data", "dummy-backend.json");
  const jsonData = await fs.readFile(filePath);
  const data = JSON.parse(jsonData);

  if (!data) {
    return {
      redirect: {
        destination: "/no-data",
      },
    };
  }

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

  return {
    props: {
      products: data.products,
    },
    revalidate: 10,
  };
}

export default HomePage;

 

이번에는 특정 제품을 보여주는 페이지에 대한 코드를 작성해보려 한다.

그러면, 어떤 아이디를 가지는 제품을 보여주어야 하는지 알아야 하는데, 이때 getStaticProps의 인자인 context를 사용하면 된다.

context에서 params라는 것을 가져올 수 있는데, params는 키-값 쌍을 가지는 객체이다.

키의 식별자는 동적 경로 세그먼트인데 여기서는 productId 값이 된다.

이 방법 말고도 useRouter 훅을 사용할 수도 있다.

import { Fragment } from "react";

import path from "path";
import fs from "fs/promises";

const ProductDetailPage = (props) => {
  const { loadedProduct } = props;

  return (
    <Fragment>
      <h1>{loadedProduct.title}</h1>
      <p>{loadedProduct.description}</p>
    </Fragment>
  );
};

export async function getStaticProps(context) {
  const { params } = context;

  const productId = params.productId;

  const filePath = path.join(process.cwd(), "data", "dummy-backend.json");
  const jsonData = await fs.readFile(filePath);
  const data = JSON.parse(jsonData);

  const product = data.products.find((product) => product.id === productId);

  return {
    props: {
      loadedProduct: product,
    },
  };
}

export default ProductDetailPage;
import path from "path";
import fs from "fs/promises";
import Link from "next/link";

const HomePage = (props) => {
  const { products } = props;

  return (
    <ul>
      {products.map((product) => (
        <li key={product.id}>
          <Link href={`/${product.id}`}>{product.title}</Link>
        </li>
      ))}
    </ul>
  );
};

export async function getStaticProps(context) {
  const filePath = path.join(process.cwd(), "data", "dummy-backend.json");
  const jsonData = await fs.readFile(filePath);
  const data = JSON.parse(jsonData);

  if (!data) {
    return {
      redirect: {
        destination: "/no-data",
      },
    };
  }

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

  return {
    props: {
      products: data.products,
    },
    revalidate: 10,
  };
}

export default HomePage;

 

하지만 위 코드를 실행시켜보면 아래와 같은 에러가 난다.

Next.js는 기본적으로 모든 페이지를 사전 생성하는데 동적 페이지에서는 그렇지 않다.

[productId].js와 같은 페이지 말이다.

이는 동적으로 생성된 페이지는 하나가 아니라 여러 페이지로 이루어지기 때문이다.

상품 아이디마다 서로 다른 페이지에 프레임 및 HTML 콘텐츠는 같고 데이터만 다르게 구성되는 것이다.

그래서 이런 페이지는 사전 생성이 되지 않는데, 이 페이지에 getStaticProps를 추가한 상태라 작동하지 않는 것이다.

우리는 동적 페이지에서 어떤 인스턴스가 사전 생성되어야 하는지 알려줄 수 있는데, 어떤 [productId] 값을 사용할 수 있는지 등을 알려줄 수 있게 된다.

 

이를 위해서 getStaticPaths라는 함수를 사용하면 된다.

이 함수는 getStaticProps와는 다르게 paths 배열을 반환하면 된다.

 

아래와 같이 코드를 변경해주면 정상적으로 동작한다.

import { Fragment } from "react";

import path from "path";
import fs from "fs/promises";

const ProductDetailPage = (props) => {
  const { loadedProduct } = props;

  return (
    <Fragment>
      <h1>{loadedProduct.title}</h1>
      <p>{loadedProduct.description}</p>
    </Fragment>
  );
};

export async function getStaticProps(context) {
  const { params } = context;

  const productId = params.productId;

  const filePath = path.join(process.cwd(), "data", "dummy-backend.json");
  const jsonData = await fs.readFile(filePath);
  const data = JSON.parse(jsonData);

  const product = data.products.find((product) => product.id === productId);

  return {
    props: {
      loadedProduct: product,
    },
  };
}

export async function getStaticPaths() {
  return {
    paths: [
      {
        params: {
          productId: "p1",
        },
      },
      {
        params: {
          productId: "p2",
        },
      },
      {
        params: {
          productId: "p3",
        },
      },
    ],
    fallback: false,
  };
}

export default ProductDetailPage;

위에서 getStaticPaths() 함수 내부에 작성한 fallback 키를 사용하면 사전 생성되어야 할 페이지가 많은 경우에 도움된다.

데이터가 아주 많거나, 방문하지 않을 페이지를 사전 생성하게 되면 성능에 문제가 생길 수 있는데, fallback 키를 true로 설정하게 되면 일부 페이지만 사전 렌더링할 수 있게 된다.

 

아래와 같이 나타내면 Product 1 페이지만 사전 렌더링을 하게 되고 나머지는 사전 렌더링 없이 정상적으로 동작하게 된다.

이런 식으로 설정하게 되면 방문율이 높은 페이지를 사전 생성할 수 있게 되는 것이다.

 

하지만 링크를 클릭하지 않고 직접 URL에 입력해서 해당 페이지에 새로운 요청을 보내게 되면 에러가 발생한다.

이는 동적 사전 생성 기능이 즉시 끝나지 않기 때문이다.

 

그래서 fallback 기능을 쓰려면 컴포넌트에서 폴백 상태를 반환할 수 있게 해줘야 한다.

그래서 loadedProduct가 존재하지 않는 경우 로딩에 관한 문장을 보여주도록 하였다.

import { Fragment } from "react";

import path from "path";
import fs from "fs/promises";

const ProductDetailPage = (props) => {
  const { loadedProduct } = props;

  if (!loadedProduct) {
    return <p>Loading...</p>;
  }

  return (
    <Fragment>
      <h1>{loadedProduct.title}</h1>
      <p>{loadedProduct.description}</p>
    </Fragment>
  );
};

export async function getStaticProps(context) {
  const { params } = context;

  const productId = params.productId;

  const filePath = path.join(process.cwd(), "data", "dummy-backend.json");
  const jsonData = await fs.readFile(filePath);
  const data = JSON.parse(jsonData);

  const product = data.products.find((product) => product.id === productId);

  return {
    props: {
      loadedProduct: product,
    },
  };
}

export async function getStaticPaths() {
  return {
    paths: [
      {
        params: {
          productId: "p1",
        },
      },
    ],
    fallback: true,
  };
}

export default ProductDetailPage;

 

만약 fallback에 ‘blocking’이라는 문자열을 값으로 설정해주면 컴포넌트에서 폴백 확인할 필요가 없게 된다.

이 경우 페이지가 서비스를 제공하기 전에 서버에 완전히 사전 생성되도록 Next.js가 기다릴 것이다.

이렇게 하면 페이지 방문자가 응답 받는 시간은 길어지지만 수신된 응답은 종료될 것이다.

이제 path 내부 객체들을 dummy-backend.json에서 가져온 데이터들을 불러오도록 변경해보자.

import { Fragment } from "react";

import path from "path";
import fs from "fs/promises";

const ProductDetailPage = (props) => {
  const { loadedProduct } = props;

  if (!loadedProduct) {
    return <p>Loading...</p>;
  }

  return (
    <Fragment>
      <h1>{loadedProduct.title}</h1>
      <p>{loadedProduct.description}</p>
    </Fragment>
  );
};

async function getData() {
  const filePath = path.join(process.cwd(), "data", "dummy-backend.json");
  const jsonData = await fs.readFile(filePath);
  const data = JSON.parse(jsonData);

  return data;
}

export async function getStaticProps(context) {
  const { params } = context;

  const productId = params.productId;

  const data = await getData();

  const product = data.products.find((product) => product.id === productId);

  return {
    props: {
      loadedProduct: product,
    },
  };
}

export async function getStaticPaths() {
  const data = await getData();

  const ids = data.products.map((product) => product.id);

  const pathsWithParams = ids.map((id) => ({
    params: {
      productId: id,
    },
  }));

  return {
    paths: pathsWithParams,
    fallback: false,
  };
}

export default ProductDetailPage;

만약 p4라는 아이디를 가진 페이지에 접속하게 되면 에러가 난다.

 

이때 fallback 값을 true로 설정하고, 없는 페이지에 접속하게 되면 404 페이지를 보여주는 방법이 있다.

요청한 id의 상품을 찾지 못하면 notFound를 true로 설정한 객체를 반환하면 된다.

import { Fragment } from "react";

import path from "path";
import fs from "fs/promises";

const ProductDetailPage = (props) => {
  const { loadedProduct } = props;

  if (!loadedProduct) {
    return <p>Loading...</p>;
  }

  return (
    <Fragment>
      <h1>{loadedProduct.title}</h1>
      <p>{loadedProduct.description}</p>
    </Fragment>
  );
};

async function getData() {
  const filePath = path.join(process.cwd(), "data", "dummy-backend.json");
  const jsonData = await fs.readFile(filePath);
  const data = JSON.parse(jsonData);

  return data;
}

export async function getStaticProps(context) {
  const { params } = context;

  const productId = params.productId;

  const data = await getData();

  const product = data.products.find((product) => product.id === productId);

  if (!product) {
    return { notFound: true };
  }

  return {
    props: {
      loadedProduct: product,
    },
  };
}

export async function getStaticPaths() {
  const data = await getData();

  const ids = data.products.map((product) => product.id);

  const pathsWithParams = ids.map((id) => ({
    params: {
      productId: id,
    },
  }));

  return {
    paths: pathsWithParams,
    fallback: true,
  };
}

export default ProductDetailPage;

index.js와 같이 동적 생성 페이지가 아닌 경우 getStaticProps만 필요한데, 사전 생성한 동적 페이지에는 getStaticProps와 getStaticPaths 둘 다 필요하다.

 

그런데 getStaticProps와 getStaticPaths 함수는 들어오는 실제 요청에 접근할 수 없다.

증분 정적 생성은 유효성 재검사가 필요할 때 유입되는 요청을 위해 호출하기도 하지만 프로젝트를 구축할 때 호출한다.

그래서 getStaticProps의 경우 유입되는 실제 요청에 접근할 방법이 없다.

그리고 접근할 필요도 없다.

 

정적 생성만으로는 충분하지 않은 경우에 서버 사이드 렌더링이 필요하다.

유입되는 모든 요청에 대한 페이지를 사전 렌더링하는 것이다.

유입되는 모든 요청이나 서버에 도달하는 특정 요청 객체에 접근할 필요가 있다.

예를 들어 쿠키를 추출해야 하는 경우다.

 

Next.js는 SSR도 지원하는데 페이지 컴포넌트 파일을 추가할 수 있는 함수를 제공한다.

이는 페이지 요청이 서버에 도달할 때마다 실행되는 함수이다.

빌드 시간이나 매초마다 사전 생성하지 않고 서버에서만 작동하는 코드이다.

애플리케이션 배포 후 유입되는 모든 요청에 대해서만 재실행된다.

 

이 코드는 getServerSideProps에 작성하는데, 이 함수도 비동기로 작성해야 한다.

해당 페이지에 대한 요청이 들어올 때마다 이 함수를 실행한다.

그래서 충돌을 일으키지 않기 위해서 getStaticProps나 getServerSideProps 함수 중 골라서 하나만 사용해야 한다.

revalidate 키는 getServerSideProps 함수에 설정할 필요가 없고, 설정할 수도 없다.

이는 들어오는 요청에 전부 유효성 검사를 실행하기 때문인데, revalidate 키를 사용할 필요가 없다.

getStaticProps 함수의 context와 달리 getServerSideProps 함수의 context는 매개 변수 객체 같은 곳에 접근하는 것만 하는 것이 아니다.

요청 객체 전체에도 접근할 수 있다.

응답 객체에 접근해서 해당 요청을 조정하거나 헤더도 추가할 수 있다.

그래서 context 객체에 든 여러 값과 키를 얻을 수 있고 매개변수 객체에도 접근하는 것은 당연히 가능하다.

 

매우 동적인 데이터가 있는 경우 getServerSideProps 함수를 쓰면 좋다.

const UserProfilePage = (props) => {
  return <h1>{props.username}</h1>;
};

export default UserProfilePage;

export async function getServerSideProps(context) {
  const { params, req, res } = context;

  return {
    props: {
      username: "Max",
    },
  };
}

getStaticProps를 사용할 때는 getStaticPaths를 사용해서 Next.js에 어떤 페이지의 인스턴스를 사전 생성할지를 알려줘야

 

하지만 getServerSideProps는 그렇게 하지 않는다.

이는 서버 사이드 코드에서 모든 요청을 처리해서 사전 생성할 필요도 없고 동적 경로도 미리 설정할 필요 없는 것이다.

다음과 같이 작성해도 잘 동작한다.

const UserIdPage = (props) => {
  return <h1>{props.id}</h1>;
};

export default UserIdPage;

export async function getServerSideProps(context) {
  const { params } = context;

  const userId = params.userId;

  return {
    props: {
      id: "userid-" + userId,
    },
  };
}

getServerSideProps를 작성해 준 페이지는 사전 생성되지 않는다.

다만, 서버 측에서만 사전 렌더링된다.

 

클라이언트 사이드 데이터 페칭

사전 렌더링이 필요 없는 데이터들도 있다.

 

이는 갱신 주기가 잦은 데이터인데, 데이터가 빨리 변경되기 때문에 프리페칭과 사전 렌더링을 하는 의미가 없다.

이런 경우 페이지를 방문했을 때 로딩 아이콘을 표시하고 방문했을 때의 최신 데이터를 가져와 백그라운드에서 업데이트하는 것이 좋다.

 

또는, 특정 유저에만 한정되는 데이터가 있는데, 이 경우에도 페이지를 사전 렌더링할 필요가 없다.

검색 엔진에서도 개인 프로필을 확인하지 않기도 하고 사용자 경험 측면에서도 프로필 페이지에 방문할 때 데이터 로드가 조금 걸려도 괜찮다.

 

또, 데이터의 일부분만 표시하는 경우도 있을 수 있다.

다양한 데이터가 표시되는 대시보드 페이지의 경우 모든 데이터를 한 번에 불러오도록 하면 서버에서 대시보드 요청을 처리하는데 시간이 많이 소요돼서 개발 단계에서 굳이 이 페이지를 사전 렌더링할 이유가 없다.

그 페이지의 정보는 개인적이거나 변동이 잦을 것이기 때문이다.

 

이런 경우에는 사전 렌더링보다 React 앱에 포함된 데이터를 사용자가 페이지에 방문할 때만 불러오는 것이 좋다.

그래서 useEffect나 fetch 같은 함수를 이용해서 클라이언트 측 React 앱 내의 API에서 데이터를 가져오는 것이 좋다.

getStaticProps나 getServerSideProps 대신 서버가 아니라 클라이언트에서 코드가 실행될 때 컴포넌트에서 데이터를 가져오도록 구축하는 것이다.

 

이런 경우, Next.js는 컴포넌트에서 최초로 반환하는 결과로 사전 렌더링을 진행한다.

그래서 데이터를 페칭하고 있는 동안 데이터가 없는 상태로 사전 렌더링이 된다.

 

useSWR

useSWR 훅은 기본적으로 HTTP 요청을 보낼 때 fetch API를 사용한다.

또, 캐싱, 자동 유효성 재검사, 에러 시 요청 재시도 등을 해준다는 장점을 가지고 있다.

SWR은 stale-while-revalidate를 의미한다.

원래 useSWR의 첫 번째 인수로는 요청 보낼 url 주소, 두 번째 인수로는 fetcher 함수를 적는데, 기본 값은 fetch API를 사용한다.

 

import { useEffect, useState } from "react";
import useSWR from "swr";

const LastSalesPage = () => {
  const [sales, setSales] = useState();

  const { data, error } = useSWR(
    "https://netjs-course-c81cc-default-rtdb.firebaseio.com/sales.json"
  );

  useEffect(() => {
    if (data) {
      const transformedSales = [];

      for (const key in data) {
        transformedSales.push({
          id: key,
          username: data[key].username,
          volume: data[key].volume,
        });
      }

      setSales(transformedSales);
    }
  }, [data]);

  if (error) {
    return <p>Failed to load</p>;
  }

  if (!data || !sales) {
    return <p>Loading...</p>;
  }

  return (
    <ul>
      {sales.map((sale) => (
        <li key={sale.id}>
          {sale.username} - ${sale.volume}
        </li>
      ))}
    </ul>
  );
};

export default LastSalesPage;

 

클라이언트 사이드 페칭에 사전 페칭 결합하기

아래 코드처럼 작성하게 되면 초기 sales 상태는 props.sales가 된다.

서버에서 혹은 빌드 프로세스 중에 사전 렌더링된 sales가 초기 상태로 사용되는 sales이고 이후 클라이언트 사이드 데이터 페칭의 결과를 덮어쓰게 될 것이다.

import { useEffect, useState } from "react";
import useSWR from "swr";

const LastSalesPage = (props) => {
  const [sales, setSales] = useState(props.sales);

  const { data, error } = useSWR(
    "https://netjs-course-c81cc-default-rtdb.firebaseio.com/sales.json"
  );

  useEffect(() => {
    if (data) {
      const transformedSales = [];

      for (const key in data) {
        transformedSales.push({
          id: key,
          username: data[key].username,
          volume: data[key].volume,
        });
      }

      setSales(transformedSales);
    }
  }, [data]);

  if (error) {
    return <p>Failed to load</p>;
  }

  if (!data && !sales) {
    return <p>Loading...</p>;
  }

  return (
    <ul>
      {sales.map((sale) => (
        <li key={sale.id}>
          {sale.username} - ${sale.volume}
        </li>
      ))}
    </ul>
  );
};

export async function getStaticProps(context) {
  return fetch("https://netjs-course-c81cc-default-rtdb.firebaseio.com/sales.json")
    .then((response) => response.json())
    .then((data) => {
      const transformedSales = [];

      for (const key in data) {
        transformedSales.push({
          id: key,
          username: data[key].username,
          volume: data[key].volume,
        });
      }

      return {
        props: {
          sales: transformedSales,
        },
        revalidate: 10,
      };
    });
}

export default LastSalesPage;

getStaticProps와 클라이언트 사이드 데이터 페칭은 공존할 수 있지만 getServerSideProps는 공존할 수 없다.

'Frontend > Next.js' 카테고리의 다른 글

Next.js를 배워보자-4  (0) 2023.07.31
Next.js를 배워보자-2  (0) 2023.07.31
Next.js를 배워보자-1  (0) 2023.07.31