[nextjs] Data Fetching & Pre-Rendering

dev stefanCho·2021년 12월 26일
0

nextjs

목록 보기
4/9
post-custom-banner

useEffect로 fetch하는 경우

Pre-rendering 없이 useEffect로 fetch하는 경우

function HomePage() {
  const [loadedMeetups, setLoadedMeetups] = useState([]);
  
  useEffect(() => {
    // send a http request and fetch data
    const res = await fetch('https://some-url.com');
    const data = await res.json();
    setLoadedMeetups(data);
  }
  
  return <MeetupList data={loadedMeetups} />
}

export default HomePage;

위와 같이 useEffect를 사용해서 fetch된 data를 표시할 수 있다. useEffect는 최초에 랜더링이 된 다음에 실행된다. 따라서 처음 진입시에는 데이터가 없는 상태로 랜더링이 된다. (View page source로 확인해보면 데이터가 없음을 확인할 수 있습니다.)

Page Pre-Rendering

npm run build

next build를 하면 ./next/server/pages에 파일들이 생성됩니다.

Pre-Rering Process

Request(요청)
-> /some-route (어떤 라우트)
-> Return pre-rendered page (Good for SEO)
-> Hydrate with React code once loaded (React코드도 같이 받게 됩니다.)
-> Page/App is interactiv

페이지로드시에 완성된 html 파일과 리액트코드를 함께 받음으로써, Server-side-rendering과 SPA의 기능을 다 갖추게 됩니다.

2가지 형태의 Pre-Rendering

  • Static Generation
  • Server-side Rendering

pages안에 server코드 작성시 유의할 점

import path from 'path';
import { promises } from 'fs';

function HomePage(props) {
  return (
    <div>Test</div>
  );
}

export async function getStaticProps() {
  const filePath = path.join(process.cwd(), 'data', 'dummy-data.json');
  return {
    props: {
      products: {}
    }
  }
}

export default HomePage;

위와 같이 client환경에서는 사용할 수 없는 fs와 path를 사용하는 경우, 반드시 server코드(getStaticProps)내에서 사용을 해줘야 합니다.
server코드에서 사용시에 import된 node 코드들은 자동으로 삭제가 됩니다. 하지만 아래와 같이 import만 해두고 사용하지 않으면 client에서 import를 시도하게 되므로 아래와 같은 에러가 발생합니다.

Static Generation

getStaticProps

getStaticProps가 실행된 다음에 component function이 실행됩니다.

export async function getStaticProps(context) { ... }
  • pages안에서만 사용이 가능합니다.
  • build process에서 실행됨, client side에서 실행되지 않습니다. (따라서 client 소스를 확인하면 getStaticProps는 없습니다.)
  • async를 써서 Promise가 resolve될때까지 기다리게 할 수 있습니다.
export async function getStaticProps() {
  // fetch data from an API
  return {
    props: {
    },
    revalidate: 10 // 
  }
}

context parameter

context의 params에는 file name에 대한 정보가 key value pair로 들어 있습니다.

// pages/[pid].js
export async function getStaticProps(context) {
  const { params } = context;

  const productId = params.pid;
  // (...)
}

getStaticProps의 Return property

props

필수 property로 props는 pages component의 props으로 들어갑니다.

revalidate (Incremental Static Generation)

기본적으로 false입니다. 즉 next build를 다시 하지 않는 이상 데이터가 그대로입니다.

revalidate를 사용하면 Incremental Static Generation이 가능합니다. Pre-generate된 Page를 request가 올떄마다 ~초 후에 다시 Re-generate 하는 것입니다.
즉, revalidate에 숫자(second를 의미)를 넣으면, ~초 후에 generation이 다시 된다는 것을 의미합니다. (demo 페이지 참고)

만약 revalidate: 10 이라면

step (second)dataregeneration
페이지 접속 (0s)(always) old dataregeneration start
2번째 접속(7s)old datawait for timeout
3번째 접속(11s)new dataregeneration end

참고로 revalidate는 npm run dev에서는 유효하지 않습니다. production인 상황에서만 유효합니다.


npm run build를 하면?
revalidate가 추가된 상황에서 npm run build를 하면 위 이미지와 같이 ISR(incremental static regeneration)이 루트 페이지(/)에 ISR: 10 Seconds로 추가 된 것을 볼 수 있습니다.

not found

notFound를 true로 하면 정상적인 페이지 대신에, 404페이지로 표시됩니다.
예를들어 data가 없는 경우, 404로 표시하도록 할 수 있습니다.

export async function getStaticProps() {
  // (...)
  if (data.products.length === 0) {
    return {
      notFound: true
    }
  }
  // (...)
}

redirect

data fetch를 실패한경우 redirect를 할 수 있습니다.

export async function getStaticProps() {
  // (...)  
  if (!data) {
    return {
      redirect : {
        destination: '/no-data'
      }
    }
  }
  // (...)
}

getStaticPaths

dynamic page([id].js)에서 getStaticProps를 쓴다면 반드시 필요합니다.

export async function getStaticPaths(context) { ... }
  • nextjs는 몇개의 page가 생성되어야하는지, 어떤 page id가 유효한 값인지 알지 못합니다.
  • getStaticProps에서 params로 dynamic path를 알 수 있는 것 처럼 보이지만(아래 예시에서의 meetupId 값을 가져올 수 있는 것처럼 보이지만), pre-generate를 해야하는 상황에서는 실제유저가 방문하기전에는 알수가 없으므로 pre-generated value라고 볼수 없습니다.
  • getStaticPaths가 없다면 아래와 같은 오류를 만날 것입니다.
  • 실제 Request에 대한 내용에 접근할 수 없습니다. (일반적으로 build시에 실행되기 때문입니다.)

    Server Error
    Error: getStaticPaths is required for dynamic SSG pages and is missing for '/[pid]'.
    Read more: https://err.sh/next.js/invalid-getstaticpaths-value

Example
// pages/[meetupId]/index.js
export async function getStaticProps(context) {
  const meetupId = context.params.meetupId;
  
  return {
    props: {
      meetup: {
        title: "Our Meetup!",
        id: meetupId
      }
    }
  }
}

위와같이 쓰면, context.params는 [meetupId]의 값을 가져오기 때문에, 문제가 없는 것처럼 보입니다. 하지만 미리 페이지를 만들어야 하는 상황에서 meetupId가 어떤 값일지는 알 수가 없으므로 getStaticPaths가 필요합니다.

export async function getStaticPaths() {
  return {
    fallback: false,
    paths: [
      { params: { meetupId: 'm1' } }
    ]
  }
}

미리 생성되야할 meetupId(dynamic segment value)에 대해서 정의합니다.

export async function getStaticPaths() {
  return {
    fallback: false,
    paths: [
      { params: { meetupId: 'm1' } },
      { params: { meetupId: 'm2' } },
      { params: { meetupId: 'm3' } }
    ]
  }
}

meetupId가 여러개가 있다면, 위와같이 paths array안에 넣어주면 됩니다.
보통은 api로 받아서 params들을 만듭니다.
모든 path에 대해서 다 만들기 보다는 popular page 일부만 (예를 들어 100개정도) 생성할 수도 있습니다.

fallback

fallback은 기본값이 false입니다.
fallback: true로 하면, 생성하지 않는 path에 대해서도 컴포넌트를 생성할 수 있게 됩니다. (다만 just-in-time으로 생성되므로, pre-generated된 page는 아닙니다.)
true인 경우에는 nextjs가 path에 대한 blocking을 하지 않습니다. false인 경우에는 유효하지 않은 path로 들어간다면 404를 보여주지만, ture는 유효하지 않은 path에 대해서 막지 않고 랜더링을 하려고 하기 때문에 따로 조건문 처리가 필요합니다. (아래 코드에서 주석확인)

Example
// pages/[pid].js
import path from 'path';
import { promises } from 'fs';
function ProductDetailPage(props) {
  const { loadedProduct } = props;

  if (!loadedProduct) { // props가 없는 경우에 대한 처리 (모든 path에 대해서 랜더링을 시도하기 때문에 필요합니다.)
    return <p>Loading...</p>
  }

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

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

  const productId = params.pid;

  const filePath = path.join(process.cwd(), 'data', 'dummy-data.json');
  const json = await promises.readFile(filePath);
  const data = JSON.parse(json);

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

  if (!data) {
    return {
      notFound: true,
    }
  }

  if (!product) {
    return {
      redirect: {
        destination: '/'
      }
    }
  }

  return {
    props: {
      loadedProduct: product
    }
  }
}

export async function getStaticPaths() {
  return {
    paths: [
      { params: { pid: 'p1' } },
    ],
    fallback: true
  }
}

export default ProductDetailPage

getStaticPaths에서 p1에 대해서만 페이지를 생성했지만, fallback: true이기 때문에 https://my-domain/p2 페이지도 정상적으로 진입할 수 있습니다. 다만 미리 생성된 페이지가 아니므로, props가 없을때에 대한 처리를 해줘야합니다.(코드에서 주석 확인)

fallback: true를 사용하는 것은 useEffect내에서 데이터를 fetch하는 것과 동일합니다. (useEffect 코드가 필요 없다는 약간의 장점이 있습니다.)
fallback: 'blocking'if (!loadedProduct) 이라는 조건도 필요하지 않습니다. 데이터를 가져온 후에 랜더링이 되게 합니다. Loading...이라는 화면을 보여주지 않아도 되는 장점이 있지만, 데이터를 가져오는데 시간이 오래걸린다면, fallback: true를 사용하는것이 나을 것 입니다.

fallback 처리 방식

fallback: true인 경우에는 아래와 같은 방식으로 처리가 됩니다.

미리 생성된 페이지가 아닌 경우

  • 우선 getStaticPaths를 실행합니다.
  • 미리 생성된 페이지가 없으므로(params가 정상적으로 생성되지 못했음) component를 일단 생성해서 client에 랜더링하도록 합니다.
  • 다시 getStaticPaths로 왔다가 getStaticProps로 넘어갑니다.
    • getStaticProps에서 props return이 가능하다면, 정상적으로 처리합니다.
    • getStaticProps에서 예외처리를 한 코드로 이동하게 됩니다. (getStaticProps에서 notFound(404) 혹은 redirect 처리를 해놓았기 때문에)

// pages/[pid].js (예외처리 예시)
export async function getStaticProps(context) {
  const { params } = context;
  const productId = params.pid;
  const data = await getData();
  const product = data.products.find(product => product.id === productId)

  if (!data) {
    return {
      notFound: true,
    }
  }

  if (!product) {
    return {
      redirect: {
        destination: '/'
      }
    }
  }

  return {
    props: {
      loadedProduct: product
    }
  }
}

미리 생성된 페이지인 경우

getStaticPaths에서 정상적으로 params가 생성되므로, 정상적인 순서로 실행되고 client에 component가 랜더링 됩니다.

Server-side Rendering

getServerSideProps

build process에서 실행되지 않고, deploy가 된 후에 (항상)server에서 실행됩니다. (development server에서도 실행됩니다.)

export async function getServerSideProps(context) {...}
  • request마다 pre-render가 필요하고, request object에 접근해야하는 경우에 사용합니다. (e.g. cookies 사용)
  • 항상 server에서 실행됩니다. (client에서 실행되지 않음)
  • every incoming request에서 실행되는 것이므로, revalidate property는 없습니다. (논리적으로 말이 안되기 때문에)
  • every incoming request에 대해서 page generation을 기다려야 하는 단점이 있습니다.

getStaticProps와 차이점

  • revalidate를 사용하지 못합니다.
  • req, res object를 사용할 수 있습니다.
export async function getServerSideProps(context) {
  const {params, req, res} = context; // req object는 authentication에서 활용할 수 있음
  // fetch data from an API
  return {
    props:{}
  }
}

next build

npm run build
build 성공시 파일이 어떻게 생성되었는지 결과가 나옵니다.

Client-side Data Fetching

useEffect()내에서 fetch()를 하는 방식

pre-render이 필요하지 않은 경우

some data는 항상 pre-rendered될 필요가 없습니다.

  • 데이터가 빠르게 변하는 경우
    • server-side-rendering을 하면 항상 데이터가 old data 올 것이기 때문에 적합하지 않습니다.
    • (e.g.) stock data
  • 유저의 특정 데이터인 경우에는 SEO가 필요하지 않습니다.
    • (e.g.) 온라인 샵의 최종 order 상품
  • 데이터중에서 일부만 바뀌어야하는 경우에는 적합하지 않습니다.
    • (e.g.) 페이지에서 특정부분 데이터만 바뀌는 경우

client-side fetch 방식 (3가지)

sales.json 데이터는 아래와 같습니다. (아래예시는 firebase에서 Test db를 생성하여 진행했습니다.)

{
  "s1": {
    "username": "cho",
    "volumn": 100
  },
  "s2": {
    "username": "heo",
    "volumn": 50
  },
  "s3": {
    "username": "julie",
    "volumn": 80
  }
}

1. 가장 일반적인 방식

useEffect와 fetch를 사용한 방식으로 일반적인 React에서 사용하는 방식입니다.
pre-rendering되는 부분이 없고, View Page Source를 보면 아래코드에서 Loading...만 있습니다.

import { useEffect, useState } from 'react';

function LastSalesPage() {
  const [sales, setSales] = useState();
  const [isLoading, setIsLoading] = useState(false);
  useEffect(() => {
    setIsLoading(true);
    fetch('https://nextjs-course-bcd7e-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, volumn: data[key].volumn })
        }
        setSales(transformedSales)
        setIsLoading(false)
      })
  }, [])

  if (isLoading) {
    return <p>Loading...</p>
  }

  if (!sales) {
    return <p>No data...</p>
  }

  return (
    <ul>
      {sales?.map(sale => <li key={sale.id}>name: {sale.username}, volumn: ${sale.volumn}</li>)}
    </ul>
  )
}

export default LastSalesPage

2. useSWR hooks 사용

next team에서 만든 swr 라이브러리를 사용하는 방식입니다.
결과는 1.과 동일합니다. useSWR을 사용하면 좀 더 많은 기능을 쉽게 사용할 수 있습니다. (예를 들면, db 내용이 바뀌면 알아서 fetch를 해줍니다.)

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

const fetcher = (url) => fetch(url).then(res => res.json())
function LastSalesPage() {
  const [sales, setSales] = useState();
  const { data, error } = useSWR('https://nextjs-course-bcd7e-default-rtdb.firebaseio.com/sales.json', fetcher);

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

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

  }, [data])

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

  if (error) {
    return <p>Failed to Load.</p>
  }

  return (
    <ul>
      {sales?.map(sale => <li key={sale.id}>name: {sale.username}, volumn: ${sale.volumn}</li>)}
    </ul>
  )
}

export default LastSalesPage

3. pre-fetching과 client-side fetch 조합

pre-fetching & client-side fetching 조합입니다.
현재 db 데이터로 static page를 만들고, 페이지 요청시에 client-side에서 다시 fetching하는 방식입니다.
View Page Source로 보면, props.sales로 초기값을 받은 부분은 pre-rendering되게 됩니다.

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

const fetcher = (url) => fetch(url).then(res => res.json())
function LastSalesPage(props) {
  const [sales, setSales] = useState(props.sales); // 초기값
  const { data, error } = useSWR('https://nextjs-course-bcd7e-default-rtdb.firebaseio.com/sales.json', fetcher);

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

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

  }, [data])

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

  if (error) {
    return <p>Failed to Load.</p>
  }

  return (
    <ul>
      {sales?.map(sale => <li key={sale.id}>name: {sale.username}, volumn: ${sale.volumn}</li>)}
    </ul>
  )
}

export async function getStaticProps() {
  const response = await fetch('https://nextjs-course-bcd7e-default-rtdb.firebaseio.com/sales.json');
  const data = await response.json();
  const transformedSales = [];

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

  return { props: { sales: transformedSales } }
}

export default LastSalesPage

getServerSideProps와는 함께 쓰지 않는다.

Client-side fetch + getStaticProps = Okay
Client-side fetch + getServerSideProps = Nope

Client-side fetch는 getServerSideProps와 함께 쓰지 않습니다. getServerSideProps는 매 request마다 실행되므로, client-side fetch와 함께 쓸 이유가 없습니다.

profile
Front-end Developer
post-custom-banner

0개의 댓글