Next.js 시작하기( DATA FETCHING )

짜스의 하루 ·2024년 7월 22일

fetch (기초적인 방법으로)

우선, 가장 기초적인 방법으로 데이터를 가져와보도록 하겠다.
1 . fetch -> 항상 client에서 일어남
2 . fetch한 url이 사용자의 브라우저에서 호출되었음
3 . 이 말은 비밀을 넣을 수 없음 (api키 같은걸 넣을 수 없음)
--> 왜? network 탭에 들어가면 api키를 확인할 수 있기 때문임

'use client';

import { useEffect, useState } from 'react';

export default function Page() {
  const [isLoading, setIsLoadig] = useState(true);
  const [movie, setMovie] = useState([]);

  const getMovie = async () => {
    const response = await fetch(
      'https://nomad-movies.nomadcoders.workers.dev/movies'
    );
    const json = await response.json();
    setMovie(json);
    setIsLoadig(false);
  };

  useEffect(() => {
    getMovie();
  }, []);

  return (
    <>
      <div>{isLoading ? 'Loading...' : JSON.stringify(movie)}</div>
    </>
  );
}
  • fetch는 client component에서 이루어져야 하기 때문에, "use client"를 명시해 주어야 하고,
    metadata는 사용하지 못한다 (왜? metadata => server component에서 사용 가능하기 때문)

실제로 network에 들어가면 fetch url도 확인이 가능해버린다.

client에서 React가 작동하기 때문에, React앱이 항상 API를 썼어야 함
그리고 API가 데이터베이스와 통신을 했어야 한다
백엔드 개발자는 항상 API를 만들어야 했음

하지만, 최신 버전의 Next.js, server component만 있다면, API는 더 이상 필요하지 않음

  • 만약 server component에서 fetch를 사용하면?
    --> useEffect useState, 로딩상태를 구현하지 않아도 된다는 것!
    --> metadata도 사용 가능해진 다는 것!

server component에서 데이터 받아오기

import { Metadata } from 'next';

export const metadata: Metadata = {
  title: 'Home ',
};

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

async function getMovies() {
  return fetch(URL).then((response) => response.json());
}

export default async function HomePage() {
  const movies = await getMovies();
  return (
    <>
      <div>{JSON.stringify(movies)}</div>
    </>
  );
}

HomePage() 컴포넌트 밖에서 getMovies()로 fetch를 통해서 데이터를 받아오고, 이를 movies로 받아온 영화 데이터는 JSON.stringify를 통해 문자열로 변환되어 화면에 출력되게 된다.

여기는 server component 로, server component를 실행하게 되면, fetch된 url를 캐싱시켜줄 거임(자동으로)
( 캐싱은 자주 요청되는 데이터를 미리 저장해두어, 같은 데이터에 대한 요청이 반복될 때마다 다시 가져오는 대신 미리 저장된 데이터를 제공하는 방법 )

Next.js의 서버 컴포넌트에서 데이터 가져오기(fetch)가 이루어질 때, 해당 네트워크 요청은 백엔드에서 처리되기 때문에 클라이언트 측의 네트워크 탭에서는 해당 API 요청을 볼 수 없게 된다.

그러기 때문에, 새로고침을 하게 되도 이미 캐싱된 데이터를 보여주기 때문에, 로딩 상태가 보이지 않게 된다. 그럼 로딩 상태가 없을까?
--> 아니다 맨 처음에는 로딩 상태가 존재한다 (처음으로 데이터를 캐싱할 때)

그럼 여기서 아예 로딩 상태를 없애면 어떻게 할 까?
프로그램을 멈춰서 느리게 만드는 간단한 트릭을 쓸 수 있다

await new Promise((resolve) => setTimeout(resolve, 5000));

getMovie 함수가 5초 동안 멈추게 되면서 그럼 로딩 상태를 볼 수 있는데, 로딩은 title 옆에 나타남 --> 백엔드에서 뭔가가 일어났다는 것을 의미함.


새 탭에서 주소를 입력했지만, 새탭에서 5초동안 멈췄다가 (옆에 로딩 이미지가 뜨면서) 5초가 지나면, 링크가 열리면서 화면이 보여지게 된다.

그럼 사용자는 5초동안 아무 페이지도 보지 못하게 되는 것이다.
그래도, 사용자에게 nav 바 정도는 보여줘야 하지 않을까?
--> not-found와 같이 Loading 페이지를 따로 만들 수 있다고 한다 !


loading.tsx

React를 사용했을 때에는 useQuery에서 제공하는 isLoading을 사용해서 isLoading ? "Loading..." : ... 이런식으로 코드를 작성했던 기억이 있는데,

Next.js에서는 아주 마술이 보여진다..
여기서 볼 수 있듯이, loading.tsx 폴더를 생성하고, 단순하게 로딩 페이지 임을 알려주는 코드만 작성해주었다.

import { Metadata } from 'next';

export const metadata: Metadata = {
  title: 'Home ',
};

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

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

export default async function HomePage() {
  const movies = await getMovies();
  return (
    <>
      <div>{JSON.stringify(movies)}</div>
    </>
  );
}

데이터를 fetch하는 코드를 작성하고 ( getMovies() ), 그걸 우리의 component에 넣으면 백엔드에서 fetch가 일어나게 된다.

자, loading 컴포넌트를 만들기 이전에는,
await new Promise((resolve) => setTimeout(resolve, 5000)); 로 인해서
화면 자체가 5초동안 보여지지 않다가, 5초 후에 페이지가 등장하는 ? 것을 확인할 수 있었는데,

loading 컴포넌트를 생성하고는 어떻게 바뀔까?

server component에서 fetch하는 중에, loading 파일을 제공해주면, 그 파일이 페이지에 나타나게 된다.

우린 Loading 페이지를 가지고 있고, 로딩 중에는 을 브라우저에 넘겨주고, await된 component에서 HTML이 반환되면, 그걸 브라우저에 넘겨주고, 프론트엔드에서 교체되는 것이다.

localhost의 응답 시간이 5초가 걸린 것을 확인할 수 있다.

요약

  • 백엔드에서 데이터 fetch: Next.js의 서버 컴포넌트에서 데이터를 fetch할 때, 이는 서버에서 실행되므로 클라이언트 측에서는 네트워크 요청이 보이지 않는다.

  • 로딩 상태 표시: 데이터를 가져오는 동안, useState나 다른 상태 관리 훅을 사용하지 않고도 로딩 상태를 표시할 수 있다.
    Loading 컴포넌트를 사용하여 데이터를 fetch하는 동안 로딩 중임을 사용자에게 표시한다.

  • 결과값으로 자동 교체: 데이터 fetch가 완료되면, Next.js 프레임워크가 로딩 컴포넌트를 실제 결과값으로 자동으로 교체한다.
    --> 사용자는 최종 데이터를 로딩 상태에서 실제 데이터로 전환되는 것을 보게 된다.

진짜.. 너무 간편해진 next 다.. next 안할거야 했던 이서연 반성해.. 🥲


await new Promise((resolve) => setTimeout(resolve, 50000)); 는 어떤 의미일까?

  • new Promise: 이 부분은 새로운 Promise 객체를 생성.
    --> Promise는 JavaScript의 비동기 작업을 처리하는 객체

  • setTimeout(resolve, 5000): setTimeout 함수는 지정된 시간(밀리초 단위)이 지나면 주어진 콜백 함수를 실행.
    --> 여기서는 5000밀리초(5초) 후에 resolve 함수를 호출한다.

  • await: await 키워드는 Promise가 해결될 때까지 실행을 중단
    --> 즉, 여기서는 5초 동안 기다린 후 다음 코드로 넘어가게 된다.
    --> 5초 후에 return fetch 함수가 실행된다


Promise.all()

import { Metadata } from 'next';
import Link from 'next/link';

export const metadata: Metadata = {
  title: 'Home ',
};

export const API_URL = 'https://nomad-movies.nomadcoders.workers.dev/movies';

async function getMovies() {
  // await new Promise((resolve) => setTimeout(resolve, 5000));
  return fetch(API_URL).then((response) => response.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>
    </>
  );
}

(home)/page.tsx 에서 movie.title을 받아오고, movie.title을 눌렀을 때,
/movies/해당 영화의 id로 이동시키도록 코드를 작성해 두었다.

자, 여기서 /movies/${movie.id}는 어떻게 이동할까?
movies라는 폴더안에 [id]라는 폴더를 생성하면 된다.

movie.id와 같은 동적 라우팅(dynamic routing)을 사용하려면 movies 폴더 안에 [id]라는 폴더를 생성하여 해당 id에 맞는 페이지를 렌더링할 수 있다.


요렇게 작성하게 되면
(movies)/movies/[id]로 연결이 되면서, page.tsx 가 화면에 보이게 될 것이다.

구체적으로 설명하면, movies/[id]/page.tsx 파일을 생성하고 동적 페이지를 구현하게 되면, movies/${movie.id} 링크를 클릭할 때 해당 id 값에 맞는 페이지가 렌더링하게 된다.

그럼 이제, movies/${movie.id}로 이동하게 되면, 어떤 데이터를 불러올 것인지 작성하러 가보자

우선 해당 영화의 id로 영화의 디테일, 예고편 혹은 티져 영상을 받아올 것이다.

(movies)/page.tsx

export default function MovieDetail({
  params: { id },
}: {
  params: { id: string };
}) {
  return (
    <>
      <div>movie id : {id}</div>
    </>
  );
}

1 . getMovies()로 영화 세부 정보 받아오기

import { API_URL } from '../../../(home)/page';

async function getMovies(id: string) {
  return fetch(`${API_URL}/${id}`).then((response) => response.json());
}

export default async function MovieDetail({
  params: { id },
}: {
  params: { id: string };
}) {
  const movie = await getMovies(id);
  return (
    <>
      <div>{movie.title}</div>
    </>
  );
}

2 . getVideos()로 영화 예고편 영상 받아오기

import { API_URL } from '../../../(home)/page';

async function getMovies(id: string) {
  return fetch(`${API_URL}/${id}`).then((response) => response.json());
}

async function getVideo(id: string) {
  return fetch(`${API_URL}/${id}/videos`).then((response) => response.json());
}

export default async function MovieDetail({
  params: { id },
}: {
  params: { id: string };
}) {
  const movie = await getMovies(id);
  const video = await getVideo(id);
  return (
    <>
      <div>{movie.title}</div>
    </>
  );
}

이렇게 데이터를 받아올 수 있다.
그럼, fetch가 getMovies, getVideo 두 번 이루어 지는데, 순차적으로 fetch가 이루어지나?
--> 맞다.

async 함수 내에서 await를 사용하여 getMovies와 getVideo 함수를 호출하면, 이 호출들은 순차적으로 실행되게 된다. 즉, getMovies가 완료된 후에 getVideo가 실행된다.
이는 각 fetch 요청이 완료될 때까지 기다리기 때문이다.

이러한 순차적 호출은 아래와 같이 동작하게 된다:

  • const movie = await getMovies(id); 가 실행되고, getMovies가 완료될 때까지 기다린다.
  • getMovies가 완료된 후, const video = await getVideo(id);가 실행되고, getVideo가 완료될 때까지 기다린다.
  • 이 과정은 비동기 작업이지만, await 키워드를 사용하여 각 작업이 순서대로 완료될 때까지 기다리게 된다.

그럼 직접 확인해보자
이렇게 각각 fetch함수에 console.log()을 찍어보았다.

결과를 확인해보면, movies가 먼저 fetch가 이루어지고, 그 다음, videos가 fetch가 이루어지는 것을 확인할 수 있다.

그럼 사용자도 맨 처음에는 movies를 먼저보고, 그 다음 videos를 보게 된다(물론 한번 캐싱이 이루어지면 다음 부터는 로딩이 되지 않긴 하지만)

그럼 같이 fetch가 되게 할 수는 없을까 ? --> 이때 Promise.all를 사용하면 된다!

병렬로 fetch 요청을 실행


const [movies, videos] = await Promise.all([getMovies(id), getVideo(id)]);

  • Promise.all을 사용하여 getMovies와 getVideo를 병렬로 실행한다.
    이렇게 하면 두 fetch 요청이 동시에 시작되어, 두 요청 중 느린 요청이 완료될 때까지 전체 대기 시간이 줄어든다.

그럼 다시 console.log()을 찍어보면서 확인해보자!

fetch가 movies와 videos가 동시에 실행되는 것을 확인할 수 있다!

간단하게 설명하자면,
Promise.all() 는 자바스크립트에서 여러 비동기 작업을 동시에 실행하고, 모든 작업이 완료될 때까지 기다렸다가 결과를 배열 형태로 반환하는 함수이다.
쉽게 말해, 여러 Promise를 모두 이행할 때까지 기다린 후, 그 결과를 한꺼번에 받아볼 수 있게 해준다.


Suspense

병렬로 fetch를 하다보니 단점이 있다.
두개의 fetch가 모두 완료된 후에 데이터를 보여주기 때문에, 그 전에는 Loading 페이지를 보고 있어야 한다.

만약 사용자에게 로딩이 완료된 데이터를 기다리지 않고 먼저 보여주고 싶다면?

먼저, 두개의 fetch를 각각 컴포넌트로 분리해보자

fetch 함수들이 분리하는 방법

  • 둘다 끝날때까지 기다려서 사용자가 UI를 보는 대신, 둘을 분리하면 된다.
  • 둘이 동시에 시작해서 movie가 fetch가 먼저 끝나면, movie에 대한 UI를 먼저 보여주도록


components 폴더 안에 movie-info와 movie-videos를 통해서 각각 fetch 함수를 정의해보려고 한다.

movie-video.tsx

import { API_URL } from '../app/(home)/page';

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

export default async function MovieVideos({ id }: { id: string }) {
  const videos = await getVideos(id);
  return (
    <>
      <h6>{JSON.stringify(videos)}</h6>
    </>
  );
}

movie-info.tsx

import { API_URL } from '../app/(home)/page';

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

export default async function MovieInfo({ id }: { id: string }) {
  const movies = await getMovie(id);
  return (
    <>
      <h6>{JSON.stringify(movies)}</h6>
    </>
  );
}
  • 둘다 async function , server component 이다 또한, 자신에 관한 데이터만 fetch하고 있다.

이 두 컴포넌트를 이제 page.tsx 에서 불러서 사용하면 되지 않을까?

(movies)/page.tsx

<Suspense/> 컴포넌트 로 감싼 채로, <MovieInfo/>, <MovieVideos/> 컴포넌트를 불러왔다.

여기서 <Suspense/>란?

Suspense는 React에서 비동기 작업을 처리하고, 로딩 상태를 관리하는 기능을 제공하는 컴포넌트이다. Suspense를 사용하면 비동기적으로 데이터를 로드할 때 사용자에게 로딩 상태를 표시할 수 있고, 데이터가 준비될 때까지 기다리게 할 수 있다.

즉, 병렬로 fetch를 받아서 두개의 fetch가 끝날때까지 기다렸던 이전과 다르게,
Suspese를 사용해서 두개의 fetch를 따로 관리할 수 있게 된 것이다.

  • Suspense component에는 fallback 이라는 prop이 있고, component가 await되는 동안 표시할 메시지를 render할 수 있게 해준다.
  • 페이지의 UI는 표시가 되고, 구체적으로 페이지의 어느 부분이 로딩 상태여야 하는지 명시해줄 수 있게 되었다! (이전에는 페이지 전체가 loading 상태였음)

해당 영화를 하나 선택했다고 가정해보면, 동시에 데이터를 가져오기 시작하게 된다
이렇게 데이터를 가져오는 중에는 fallback에 정의한 로딩 문구를 보여주게 되고,

요렇게 데이터 로딩이 완료된 컴포넌트 순서대로 결과를 보여주게 된다.


정리

Suspnese를 사용하면 component를 await할 수 있게 된다.
page.tsx에는 await할 게 없어서 사용자는 바로 페이지에 이동해서 UI를 볼 수 있고, 해당 component-> MovieInfo, MovieVideos 가 await되게 된다.

사용자는 h3와 fallback에 작성한 Loading만 보게 된다.
전체 페이지가 로딩 될 필요가 없다. 각각의 컴포넌트만 로딩되면 된다.
각각의 컴포넌트의 function을 async로 만들고, 이걸 Suspense의 자식 요소로 만들게 되면, Suspense가 component를 await하게 된다.


Error 가 발생한다면 ?

만약에 데이터를 불러오는 과정에서 끊기거나 데이터가 삭제되거나 한다면?
사용자는 단순히 빈 화면만 보게 된다.
오류가 발생했는지 모를지도 모른다.

이때, 오류가 발생했음을 알려주는 간단한 방법은 ?
--> error.tsx error 컴포넌트를 생성하는 방법이다!

error.tsx 페이지를 간단하게 생성하면 ? client component에서만 사용이 가능하다고 오류 메시지가 친절하게 알려준다 (너무 고맙다 ㅋㅋ)

추가해주고 다시, 새로고침을 해보면, 이렇게 오류메시지를 사용자에게 보여주는 것을 확인할 수 있다.

따로 에러 메시지를 (이럴땐, 이렇게 저럴땐, 저렇게) 설정을 해주지 않아도 척척 알아서 척척 해주는 너란 next js.. 사랑한다 ❤️

profile
2024. 01. 02 ~ 백앤드 공부 시작, 2024. 04.01 ~ 프론트 공부 시작

0개의 댓글