티스토리 뷰

코딩/Next.js

[Next.JS] Data fetching

김기지 2024. 1. 28. 00:42

 

노마드 Next.js 강의가 업데이트 되어 수강한 내용을 정리해 보았다.

 

🖥️ Client Component

클라이언트 컴포넌트에서 useState, useEffect, fetch를 사용해 데이터를 화면에 뿌린다.

 

export default function HomePage(){
	const [movies, setMovies] = useState([]);
    
    const getMovies = async () =>{
    	const response = await fetch(API_URL);
        const json = response.json();
        setMovies(json);
    }
    
    useEffect(()=>{
    	getMovies();
    },[])
    
    return (
    	<div>
          {movies.map((movie) => (
            <li key={movie.id}>
              <Link href={`/movies/${movie.id}`}>{movie.title}</Link>
            </li>
            ))}
        </div>
    )
}

 

클라이언트측에서 데이터 요청이 이루어지기 때문에 페이지에 방문할 때마다 요청이 이루어져 화면이 끊기는 것같이 동작한다.

여기서 가장 중요한 것은 데이터 요청이 보내지면 개발자 도구의 네트워크 탭에서 요청에 대한 정보에 접근할 수 있다는 것이다.

 

 

API키와 같은 중요한 정보가 노출되는 위험이 있기 때문에 넣을 수 없다.

그리고 Next.js의 metadata도 사용할 수 없게 된다ㅜ

 

 

하지만 서버측에서 데이터 요청을 보내면 이러한 문제를 해결할 수 있다.

 

 

 


🖥️ Server Component

서버 컴포넌트에서는 useState, useEffect를 사용할 수 없다.

대신 함수 내부에서 await를 사용하기 위해 async로 함수를 선언한다.

 

async function getMovies() {
  const response = await fetch(API_URL);
  const json = response.json();
  return json;
}

export default async function HomePage() {
  const movies = await getMovies();
  return (
    <div>
      {movies.map((movie) => (
        <li key={movie.id}>
          <Link href={`/movies/${movie.id}`}>{movie.title}</Link>
        </li>
      ))}
    </div>
  );
}

useState와 useEffect를 사용하지 않고 간단하게 데이터 요청을 완료한다.

서버측에서 데이터 요청을 보내고 완료되면 클라이언트측으로 보내기때문에 네트워크탭에서 요청에 대한 정보를 찾을 수 없다.

또, 한 번 받은 UI를 재사용하기 때문에 페이지를 재방문해도 화면이 바로 표시된다.

 

하지만 첫 페이지 접속 시 로딩 시간 동안은 전체 UI가 표시되지 않는다는 단점이 있다.

// app/layout.tsx

export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <html lang="en">
      <body>
        <Navigation />
        {children}
        &copy; Next JS
      </body>
    </html>
  );
}


// app/(home)/page.tsx

async function getMovies() {
  await new Promise((resolve) => setTimeout(resolve, 5000));
  const response = await fetch(API_URL);
  const json = response.json();
  return json;
}

RootLayout에 네비게이션 컴포넌트를 추가한 후 HomePage에서 데이터 요청을 보내는 시간을 설정한 후 확인한다.

 

 

데이터 요청이 모두 이루어지기 전까지 네비게이션을 포함한 모든 UI가 표시되지 않는다.

 

이런 상황을 방지하기 위해 로딩 페이지를 추가한다.

 

 

 

 

Loading 페이지 추가하기

공식문서를 보면 loading.js 파일로 대체 UI를 지원한다

https://nextjs.org/docs/app/building-your-application/routing/loading-ui-and-streaming

 

Routing: Loading UI and Streaming | Next.js

Built on top of Suspense, Loading UI allows you to create a fallback for specific route segments, and automatically stream content as it becomes ready.

nextjs.org

 

같은 폴더 위치에 존재하는 layout.js에 중첩되기 때문에 RootLayout과 같은 위치에 loading 파일을 생성한다.

HomePage에서 데이터가 요청되는 동안 표시할 UI를 loading 파일에 작성한다.

 

// app/(home)/loading.tsx

export default function Loading() {
  return <div>Loading...</div>;
}

 

 

 

페이지에 처음 접속하면 데이터 요청이 진행되는 동안 Loading 컴포넌트가 HomePage 컴포넌트를 대신한다.

데이터 통신이 완료되면 Loading 컴포넌트 자리에 HomePage가 들어와 표시된다.

 

⭐️ loading 파일은 동일한 위치의 layout에 중첩되기 때문에 정확한 위치에 생성한다.

 

 


Suspense로 병렬 요청하기

fetch 요청이 여러개인 경우 앞의 요청이 완료되어야 다음 요청이 실행되기 때문에 시간이 오래 소모된다.

사용자는 로딩 시간을 모두 기다려야하기 때문에 사용자 경험에 매우 좋지 않다.

 

이를 해결하는 방법으로는 Promise.all( )메소드로 모든 요청을 한 번에 처리하도록 만드는 것이다.

 

 

영화 목록을 클릭하면 해당 영화의 상세 정보를 받아와 표시하는 페이지를 만들어보자.

이 페이지에서는 두 가지의 데이터 요청을 보낸다.

  1. 영화 정보를 요청
  2. 영화 비디오를 요청

각 요청을 보낼 함수를 작성한다.

여기서 지연 시간을 각각 다르게 설정해 페이지가 표시되는 시점을 확인한다.

async function getMovie(id: string) {
  console.log(`Fetching movies: ${Date.now()}`);
  await new Promise((resolve) => setTimeout(resolve, 5000));
  const response = await fetch(`${API_URL}/${id}`);
  return response.json();
}

async function getVideos(id: string) {
  console.log(`Fetching videos: ${Date.now()}`);
  await new Promise((resolve) => setTimeout(resolve, 2000));
  const response = await fetch(`${API_URL}/${id}/videos`);
  return response.json();
}

 

 

세그먼트로 표시한 id를 전달해 데이터 요청을 보낸다.

export default async function MovieDetail({
  params: { id },
}: {
  params: { id: string };
}) {
  const movie = await getMovie(id);
  const videos = await getVideos(id);
  return (
    <div>
    	<h2>{movie.title}</h2>
        {JSON.stringify(videos)}
    </div>
  );
}

 

 

데이터를 요청하고 터미널을 확인하면 Fetching Movies가 찍힌 후 5초 뒤에 Fetching videos가 찍힌다.

 

두 요청을 Promise.all로 묶어 한 번에 요청한 뒤 비교해보자.

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

 

터미널의 콘솔에 두 메세지가 동시에 뜬다.

하지만 페이지에 표시되는 것은 마지막 데이터 요청이 완료된 후이다.

=> 요청이 동시에 보내지지만 모든 요청이 완료된 시점에 데이터 표시가 됨

 

 

 

 

Next.js에서는 Suspense를 이용한 스트리밍이 가능하다.

(Suspense는 Next.js 요소가 아닌 React 요소이다.)

https://nextjs.org/docs/app/building-your-application/routing/loading-ui-and-streaming#streaming-with-suspense

 

Routing: Loading UI and Streaming | Next.js

Built on top of Suspense, Loading UI allows you to create a fallback for specific route segments, and automatically stream content as it becomes ready.

nextjs.org

  • Streaming
    • 페이지의 HTML을 더 작은 청크로 나누고 점진적으로 해당 청크를 서버에서 클라이언트로 보낼 수 있다.
    • UI를 렌더링하기 전에 모든 데이터를 기다릴 필요 없이 페이지의 일부를 더 빨리 표시할 수 있음

 

 

Suspense 컴포넌트를 사용하면 각 요청이 완료된 다음 바로 UI에 표시할 수 있다.

page 파일에서 다루는 것이 제일 편하지만 많은 데이터 소스에서 fetch 요청을 보내야하는 경우에는 Suspense 컴포넌트를 사용하는 것이 좋다.

 

fetch 요청을 보내는 함수를 컴포넌트로 분리한 후 각 컴포넌트를 불러와 Suspense 컴포넌트로 감싸준다.

// components/movie-info.tsx
async function getMovie(id: string) {
  console.log(`Fetching movies: ${Date.now()}`);
  await new Promise((resolve) => setTimeout(resolve, 5000));
  const response = await fetch(`${API_URL}/${id}`);
  return response.json();
}

export default async function MovieInfo({ id }: { id: string }) {
  const movie = await getMovie(id);

  return <h6>{JSON.stringify(movie)}</h6>;
}




// components/movie-videos.tsx
async function getVideos(id: string) {
  console.log(`Fetching videos: ${Date.now()}`);
  await new Promise((resolve) => setTimeout(resolve, 2000));
  const response = await fetch(`${API_URL}/${id}/videos`);
  return response.json();

}

export default async function MovieVideos({ id }: { id: string }) {
  const videos = await getVideos(id);

  return <h6>{JSON.stringify(videos)}</h6>;
}




// app/(movies)/movies/[id]/page.tsx
export default async function MovieDetail({
  params: { id },
}: {
  params: { id: string };
}) {
  console.log("start fetching");
  console.log("end fetching");

  return (
    <div>
      <Suspense>
        <MovieInfo id={id} />
      </Suspense>
      <Suspense>
        <MovieVideos id={id} />
      </Suspense>
    </div>
  );
}

 

요청이 시작되고 종료되는 시점을 확인해보면 5초가 걸리고 각 요청이 완료되는 시점에 컴포넌트가 표시된다.

 

 

 

여기서 각 요청의 로딩 시점에 다른 UI를 표시하도록 설정하기 위해 Suspense의 fallback을 사용해 props를 전달한다.

https://react.dev/reference/react/Suspense

 

<Suspense> – React

The library for web and native user interfaces

react.dev

 

fallback으로 넘긴 props는 실제 UI의 로드가 완료되지 않은 경우 대체할 UI가 된다.

    <div>
      <Suspense fallback={<h1>Loading Movie Info</h1>}>
        <MovieInfo id={id} />
      </Suspense>
      <Suspense fallback={<h1>Loading Movie Video</h1>}>
        <MovieVideos id={id} />
      </Suspense>
    </div>

 

=> loading 파일을 추가하지 않고도 로딩 페이지를 구현할 수 있다.

+ 페이지의 어느 부분이 로딩 상태인지를 구체적으로 명시가 가능

 

이 페이지에서는 await를 사용하지 않아 loading 파일이 필요하지 않지만 await를 추가한다면 필요하다.

 

 

 


error.js 로 에러 처리하기

API가 끊겨 에러가 발생한 경우에는 error.js에서 오류를 처리한다.

https://nextjs.org/docs/app/building-your-application/routing/error-handling

 

Routing: Error Handling | Next.js

Handle runtime errors by automatically wrapping route segments and their nested children in a React Error Boundary.

nextjs.org

 

데이터 요청에서 에러를 발생시킨다.

async function getVideos(id: string) {
  console.log(`Fetching videos: ${Date.now()}`);
  await new Promise((resolve) => setTimeout(resolve, 2000));
  // const response = await fetch(`${API_URL}/${id}/videos`);
  // return response.json();
  throw new Error("Something broke...");
}

 

 

에러 처리를 할 page와 같은 위치에 error 파일을 생성한다.

Error 컴포넌트는 클라이언트 컴포넌트로 작성해야 하기 때문에 "use client" 선언을 한다.

"use client";

export default function Error() {
  return <h1>lol something broke...</h1>;
}

 

728x90