본문 바로가기
JavaScript/Next.js

[Next.js] Data fetching하기, Loading 및 Error Handling

by junvely 2024. 3. 17.

Next.js 13이후 Data를 fetching하는 방법

1. 기존 React에서 컴포넌트에서 Data를 fetching하는 방법(클라이언트 컴포넌트 형식)

"use client";
import { useEffect, useState } from "react";

// export const metadata = {
//   title: "Home",
// }; // 클라이언트 컴포넌트에서는 meta data 사용불가

export default function Page() {
  const [movies, setMovies] = useState([]);
  const [isLoading, setisLoading] = useState(true);

  const getMovies = async () => {
    const res = await fetch(
      "https://nomad-movies.nomadcoders.workers.dev/movies"
    );
    const json = await res.json();
    setMovies(json);
    setisLoading(false);
  };

  useEffect(() => {
    getMovies();
  }, []);
  return (
    <>
      <div>{isLoading ? "Loading...🔥" : JSON.stringify(movies)}</div> // 또는 usequery 등으로 로딩 상태 관리
     
    </>
  );
}

+ 네트워크 탭에서 우리가 요청한 api정보 등을 확인 가능하다. 보안에 취약하다.

 

2. 서버 컴포넌트에서 Data를 fetching하는 방법

클라이언트 컴포넌트와 달리 서버 컴포넌트를 사용하게 되면 useState나 useEffect, useQuery 등의 필요성이 사라지는 등 좀 더 편리해졌다.

  • 컴포넌트 함수 외부에서도 함수 사용 가능하다.
  • useState, useEffetct를 사용하지 않아도 된다. -> 서버사이드 렌더링으로 데이터 fetching이 완료된 HTML페이지를 전달해 주기 때문 + 서버 컴포넌트에서는 클라이언트 사이드와 달리 사용자 상호 작용이 없으므로 상태 변화(라이프 사이클)를 관리할 필요가 없다. 서버 컴포넌트의 주된 목적은 초기 데이터 로딩 및 HTML 생성이므로, 상태를 관리할 필요성이 크게 줄어든다.
  • fetch 사용 시 프레임워크에서 자동으로 URL을 캐싱 해준다(useQuery 필요성 사라짐).
  • 자동 캐싱 때문에 초기 data fetching 때에만 loading 상태가 필요하다. -> ❗하지만 서버에서 이루어지기 때문에 fetching될 때 까지 사용자가 아예 페이지 자체를 사용자가 확인할 수 없다. 때문에 사용자에게 로딩상태를 알려줄 수 있는 조치가 필요하다(아래에서 정리)
  • 캐싱된 데이터가 아닌 최신 데이터가 필요할 경우 revalidation을 공부해 보도록 하자
export const metadata = {
  title: "Home",
}; //서버 컴포넌트에서는 metadata 가능

const URL = "https://nomad-movies.nomadcoders.workers.dev/movies";

//1. 서버 컴포넌트 -> 외부에 함수 작성 가능
async function getMovies() {
  console.log("i'm fetching!"); // 서버 컴포넌트기 때문에 서버에서 로그 찍힘
  //2. 서버 컴포넌트 사용시, Next.js에서 fetch한 URL을 자동으로 캐싱해줌, useQuery 필요없음
  // 최신 데이터가 필요할 경우 revalidation에 대해 공부하기
  const res = await fetch(URL);
  const json = await res.json();
  return json;
}
export default async function HomePage() {
  //3. useState, useEffect 필요 없음
  //4. loading state도 필요 없음 -> 캐싱되기 때문,
  //❗but, 처음에는 api요청에 따른 로딩 발생 -> 서버에서 html를 받을 때까지 사용자는 페이지를 확인하지 못함(서버가 로딩중인 상태)
  // 사용자가 로딩중인걸 바로 확인해야 함
  const movies = await getMovies();
  return (
    <>
      <div>{JSON.stringify(movies)}</div>
    </>
  );
}

컴포넌트에 async를 붙이는 것은 NextJs가 해당 컴포넌트에서 await 해야 하기 때문이다.

+ 리액트 라이프사이클(Life cycle)과 훅(Hooks)의 목적 복습하기

라이프사이클과 훅의 목적은 주로 컴포넌트의 생명주기에 따른 동작을 관리하고, 상태(state)를 효율적으로 관리하여 UI를 최신 상태로 유지하는 데 있다.

💡 라이프사이클:
라이프사이클은 컴포넌트가 생성되고 소멸되는 과정에서 발생하는 다양한 이벤트를 의미한다. 예를 들어, 컴포넌트가 처음 렌더링될 때, 업데이트될 때, 혹은 제거될 때 특정 작업을 수행하고 싶을 때 라이프사이클 메서드를 사용한다. 이를 통해 컴포넌트의 초기화, 데이터 로딩, 상태 갱신 등을 관리할 수 있다.

💡 Hooks의 목적 :
훅은 함수형 컴포넌트에서 상태와 생명주기 기능을 사용할 수 있게 해주는 React의 기능이다. useState, useEffect, useContext 등의 훅을 사용하여 컴포넌트의 상태를 관리하고, 생명주기 이벤트에 대응하여 특정 작업을 수행할 수 있다. 훅을 사용함으로써 클래스형 컴포넌트에서 제공되는 기능과 유사한 기능을 함수형 컴포넌트에서도 사용할 수 있게 되었다.

- 상태 관리: 컴포넌트의 상태를 관리하여 UI를 최신 상태로 유지한다. useState 훅을 사용하여 컴포넌트의 상태를 선언하고, 상태가 변경될 때마다 UI를 업데이트할 수 있다.
- 생명주기 관리: 컴포넌트의 생명주기에 따른 동작을 관리한다. useEffect 훅을 사용하여 컴포넌트가 마운트되었을 때, 업데이트되었을 때, 혹은 언마운트되었을 때 특정 작업을 수행할 수 있다.
- 부수 효과 관리: 데이터 가져오기, 구독 설정, 타이머 설정 등의 부수 효과를 관리한다. useEffect 훅을 사용하여 컴포넌트의 부수 효과를 관리하고, 필요한 경우 정리(clean-up) 작업을 수행한다.
따라서 리액트에서 라이프사이클과 훅을 사용하여 상태를 관리하고, 컴포넌트의 생명주기를 관리하여 UI를 최신 상태로 유지하는 것이 주요 목적이다.

 

3. Loading Components

page폴더 안에 loading.tsx를 생성하면, 서버가 렌더링될 때 동안 해당 페이지 자리에 로딩 컴포넌트를 보여준다.

사용자가 페이지를 요청하면, 서버가 즉시 먼저 준비된 작은 HTML문서들(청크) Navigator, Loading페이지 등을 먼저 전달하고, 백엔드 통신이 마무리 되지 않이 기다려야 함을 알림 -> component는 await 중 -> 서버가 데이터 fetching이 완료가 되면 해당 컴포넌트를 전달한다. -> Loading컴포넌트를 해당 컴포넌트로 바꿔줌 ->  서버가 content를 streaming 하는 것. 

-> ❗하지만 매번 이런 식으로 폴더에 Loading 컴포넌트를 만들기는 번거롭다. 다른 대안이 필요하다.

 

+ 컴포넌트에 async를 붙이는 것은 NextJs가 해당 컴포넌트에서 await 해야 하기 때문이다.

export default async function HomePage() {
  await new Promise((resolve) => setTimeout(resolve, 5000));
  const movies = await getMovies();
  return (
    <>
      <div>{JSON.stringify(movies)}</div>
    </>

 

💡 구글 검색엔진은 로딩페이지를 인식할까?
로딩 페이지나 Suspense를 사용하는 경우 Google 봇은 로딩요소를 볼 수 없다. 앞서 본 것처럼 페이지는 기술적으로 로드되지 않았으며 데이터가 도착할 때만 '공식적으로' 로드되기 때문이다. Google 봇은 페이지가 로드될 때까지 기다린다. 브라우저는 대기가 완료될 때까지 페이지가 로드되었다고 '생각'하지 않는다.

 

 

4. Parallel Requests (Promise.all)

같은 컴포넌트에서 동시에 다수의 데이터 통신을 수행할 경우, async await을 사용하면 먼저 실행된 데이터 통신이 마무리 된 후에, 다음 데이터 통신이 실행됨 -> 데이터 통신이 많고 오래걸릴 수록 -> 모든 데이터 통신이 완료될 때 까지 시간이 굉장히 오래걸린다.

const movie = await getMovie(id); // 2개의 데이터 통신 할 경우
const videos = await getVideos(id); // 1번이 완료되어야 2번이 진행됨 -> 병렬적으로 실행 방법 Promise.all

먼저 실행한 데이터 통신  -> 5초 뒤 다음 데이터 통신이 완료됨

-> Promise.all을 사용하면 모든 데이터 통신을 병렬적으로 동시에 수행 가능하다.

  const [movie, videos] = await Promise.all([getMovie(id), getVideos(id)]);

-> ❗하지만 이렇게되면 두 데이터 통신이 전부 끝나야지만 데이터를 확인할 수 있다. 분리할 수 있는 다른 대안이 필요하다.

 

 

5. Suspense

1. 2가지 데이터를 병렬적으로 통신하기 위해 컴포넌트를 분리(각 컴포넌트는 async)

2. Suspense를 이용하여 fallback에 loading 상태동안 보여줄 요소를 전달

3. 데이터 통신을 마치면 해당 컴포넌트로 교체됨

export default async function MovieDetail({
  params: { id },
}: {
  params: { id: string };
}) {
  return (
    <div>
      <Suspense fallback={<h1>Loading movie info</h1>}>
        <MovieInfo id={id} />
      </Suspense>
      <Suspense fallback={<h1>Loading movie videos</h1>}>
        <MovieVideos id={id} />
      </Suspense>
    </div>
  );
}

-> ❗만약 데이터 통신에 실패하거나 에러가 발생한다면 어떻게 에러 처리를 해야 할까?

 

 

6. Error Handling

로딩 컴포넌트와 똑같이 해당 폴더 내부에 error.tsx를 작성하면, 에러 발생시 해당 페이지를 자동으로 보여주게 된다.

나머지 html요소는 영향이 없고, 웹 사이트가 멈추지 않고, 해당 페이지만 error페이지로 보여진다.

애플리케이션이 멈추지 않고 다른 작업을 동시에 할 수 있다는 것이다. 만약 로딩, 에러 상태가 발생했다고 해서 전체 애플리케이션이 멈추는 것처럼 보이는 것은 치명적이며 사용자에게 좋지 않다. 따라서 부분적으로 로딩, 에러 상태를 보여주는 것이 좋다.

 

+ 읽어보면 좋을 참고 자료 :

 

Suspense, Error Boundary로 비동기 로딩, 에러 로직 공통화하기(feat. Next.js, React-Query)

Suspense, Error Boundary를 사용하여 선언적으로 깔쌈하게 비동기 로딩, 에러 로직을 공통화합니다.

velog.io

 

 

7. Dynamic Metadata(동적 메타데이타)

각 페이지마다 메타데이타 내용을 변경하고 싶을 경우 다음과 같이 generateMetadata를 설정해 주면 된다.

컴포넌트에 전달된 id props을 활용해 영화정보를 가져와서 title 정보로 메타데이타를 변경시켜 준다.

metadata를 업데이트 하기 위해 getMovie API를 실행하는건 좋지 않다고 생각할 수 있지만, 이전 버전이 아닌 최신 버전은 하위 컴포넌트에서 호출한 getMovie를 캐싱하기 때문에 두번째 호출한 fetch에서는 캐싱된 데이터를 사용한다.

interface Params {
  params: { id: string };
}

// metadata를 업데이트 하기 위해 getMovie API를 실행하는건 좋지 않다고 생각할 수 있지만,
// 이전 버전이 아닌 최신 버전에서는 하위 컴포넌트에서 호출한 getMovie를 캐싱하기 때문에
// 두번째 호출한 fetch에서는 캐싱된 데이터를 사용한다.
export async function generateMetadata({ params: { id } }: Params) {
  //컴포넌트와 같이 props을 받을 수 있음(id)
  const movie = await getMovie(id);
  return {
    title: movie.title,
  };
}

export default async function MovieDetail({
  params: { id },
}: {
  params: { id: string };
}) {

 

 

8. prefetch props

movie 페이지는 영화정보가 굉장히 많아 데이터가 로드되는데 오래걸리기 때문에 사용자가 비디오 목록을 확인하는데 오래 걸린다. 이 Link 부분에 prefetch props를 추가해준다.

<Link prefetch href={`/movies/${id}`}>
        {title}
      </Link>

이렇게 하면 NextJS는 해당 페이지의 데이터를 미리 로드하여 사용자가 더 빠르게 데이터를 확인할 수 있게 된다.

Network탭을 열어놓고 스크롤을 내리면 사용자가 링크를 클릭하지 않아도 미리 자동적으로 해당 페이지의 데이터를 받아오고 있는것을 확인할 수 있다. 해당페이지에 들어가면 로딩스피너는 더이상 확인할 수 없고 미리 로드한 데이터를 더 빠르게 확인 할 수 있다.

모든 페이지를 prefetch하라는 것은 아니다! DB가 죽을 수도 있다.. 하지만 필요시 적절히 사용할 수 있는 멋진 기능인 것 같다.