[TIL] Next.js 기초 공부하기 - 3 (Data Fetching, Loading Components, Parallel Requests, Promise.all(), Suspense, fallback, Error Handling)

👉🏼 KIM·2024년 11월 13일

TIL

목록 보기
35/54

오늘 공부한것 & 기억하고 싶은 내용

Data Fetching

원래 리액트에서는 useState, useEffect 등 훅을 사용해서 페이지를 만들고, api를 가져와 데이터 패칭할때도 꽤나 복잡했다.
하지만 Next.js 프레임워크는 다르다. 아주 간단하게 코드 만들기 가능!

  • useState로 movies와 loading 상태를 관리한다.
  • useEffect는 컴포넌트가 처음 렌더링될 때 fetchMovies함수를 호출
  • 데이터가 로딩 중일때는 "Loading" 메시지를 보여주고 로딩이 끝나면 moives 데이터를 화면에 출력
import React, { useState, useEffect } from 'react';

const URL = "url";

function HomePage() {
  const [movies, setMovies] = useState(null); 
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    const fetchMovies = async () => {
      try {
        // 비동기 호출을 통해 데이터를 패치
        await new Promise((resolve) => setTimeout(resolve, 10000));
        const response = await fetch(URL);
        const json = await response.json();
        setMovies(json);
      } catch (error) {
        console.error("Failed to fetch movies:", error);
      } finally {
        setLoading(false);
      }
    };

    fetchMovies();
  }, []); 

  if (loading) return <div>Loading...</div>;

  return (
    <div>
      {movies ? JSON.stringify(movies) : "No movies available"}
    </div>
  );
}

export default HomePage;
//Next.js
export const metadata = {
  title: "Home",
};

const URL = "url";

// 비동기적으로 영화 데이터를 가져오는 함수
async function getMovies() {
  await new Promise((resolve) => setTimeout(resolve, 10000));
  const response = await fetch(URL);
  const json = await response.json();
  return json;
}

// Next.js의 기본 비동기 함수 컴포넌트
export default async function HomePage() {
  const movies = await getMovies();
  return <div>{JSON.stringify(movies)}</div>; // 데이터를 JSON 문자열로 변환해 렌더링
}
  • HomePage 컴포넌트는 서버 컴포넌트이며, 이 안에서 getMovies를 호출해 데이터를 가져온다.
  • 서버에서 데이터를 받아 클라이언트로 렌더링하므로, 클라이언트 사이드의 추가적인 API 호출이나 useEffect 설정없이 데이터를 사용할 수 있다.
  • URL로 fetch 요청을 보내고, 10초의 지연을 통해 비동기 호출이 느리게 완료되는 상황을 가정.
  • await를 사용하여 응답을 JSON으로 변환한 뒤 리턴
  • 서버사이드 렌더링의 특징
    - 서버사이드에서 실행되기 때문에 브라우저가 아닌 서버에서 데이터 패치를 한다.
    • 클라이언트 사이드에서 API 요청을 따로 보내지 않고 서버에서 렌더링 html을 받기 때문에 브라우저 콘솔창에서는 요청 과정을 볼 수 없고, 페이지는 서버에서 완성된 상태로 전달

Loading Components

homePage component가 async여야 하는 이유는 Next.js가 이 component에서 await해야 하기 때문이다.
await이 끝나면 마지막 html 부분을 전달해주기 때문에 그 전까지는 브라우저가 백엔드 작업이 덜 끝났다고 생각한다.
그래서 그 사이에 loading.tsx/jsx을 넣어주어 화면에 뿌려준다.

<Loading />
const html = await HomePage()
isLoading? <Loading /> : html

-> 이와 같은 작업을 Next.js 프레임 워크에서는 자동으로 처리해준다.(async, await, loading.jsx로)

먼저, layout에 있는 공통 요소를 뿌려주고, 그 다음에 데이터 패칭된 부분을 보여주는데 보여주기전 loading.jsx 파일이 있다면 자동으로 로딩 페이지를 뿌려주기 때문에 위에는 layout안에 있는 공통 헤더가 나오고 있고, 그 아래에 Loading... 텍스트가 보여지는 것이다.

  • page.jsx 파일과 같은 경로에 있어야 하고, 파일명은 무조건 loading.jsx여야만 한다.

Parallel Requests

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

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, 5000));
  const response = await fetch(`${API_URL}/${id}/videos`);
  return response.json();
}

export default async function MovieDetail({
  params: { id },
}: {
  params: { id: string };
}) {
  console.log("===========");
  console.log("start fetching");
  const movie = await getMovie(id); //순차적으로 진행되기에 너무 느림
  const video = await getVideos(id); //순차적으로 진행되기에 너무 느림
  console.log("end fetching");
  return <h1>{movie.title}</h1>;
}

Promise.all() - 병렬적으로 fetch하는 방법

  • 자바스크립트에서 여러 비동기 작업을 동시에 실행하고, 모든 작업이 완료될 때까지 기다렸다가 결과를 배열 형태로 반환하는 함수
  • 여러 Promise를 모두 이행할 때까지 기다린 후 그 결과를 한꺼번에 받아볼 수 있다.
  • 하나라도 reject된다면 전체가 reject되므로 오류 처리를 잘 해준다면 여러 Promise의 비동기 처리를 할 때 정말 유용하게 사용 가능
const [movie, videos] = await Promise.all([getMovie(id), getVideos(id)])
import { API_URL } from "../../../(home)/page";

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, 5000));
  const response = await fetch(`${API_URL}/${id}/videos`);
  return response.json();
}

export default async function MovieDetail({
  params: { id },
}: {
  params: { id: string };
}) {
  console.log("===========");
  console.log("start fetching");
  const [movie, videos] = await Promise.all([getMovie(id), getVideos(id)]); //한번에 진행
  console.log("end fetching");
  return <h1>{movie.title}</h1>;
}

Suspense

  const [movie, videos] = await Promise.all([getMovie(id), getVideos(id)]); //한번에 진행
  return <h1>{movie.title}</h1>;

지금 위 소스는 Promise.all부분이 다 끝나야지만 리턴값이 화면에 뿌려진다.
지금은 뿌려지는 데이터가 2개뿐이라 모르겠지만 많은 데이터를 fetch하게 되면 화면에 뿌려지는 것이 없으니... 좋지 못한 코드인 것이다.
그래서 다 끝날때까지 기다리는 것이 아니라 다 분리를 시켜서 한번에 끝나도록 해야함.
그것이 suspense임.
1. 파일 생성(movie-videos.tsx, movie-info.tsx)
2. 데이터 패칭해주는 소스 코드 복붙해서 넣어줌

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

export default async function Movievideos({id}: {id:string}) {
  const videos = await getVideos(id);
	return <h6>{JSON.stringify(videos)}</h6>
}
// 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}`);//import
  return response.json();
}

export default async function MovieInfo({id}: {id:string}) {
  const movie = await getMovie(id);
	return <h6>{JSON.stringify(movie)}</h6>
}
export default async function MovieDetail({
  params: { id },
}: {
  params: { id: string };
}) {
  //각각 다른 파일로 분리시켰기 때문에 주석 혹은 지우기
  //const [movie, videos] = await Promise.all([getMovie(id), getVideos(id)]); 
  return <h1>{movie.title}</h1>;
}
  • 두 파일 다 async 파일이고, 서버 컴포넌트이다.
  • 각각 개별적인 파일이 됐고, 이 컴포넌트들을 suspense라는걸로 묶어줘야 함
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>
)

Page 단위 로딩 => loading.tsx
서버 컴포넌트 단위 로딩 => Suspense

Error Handling

error.js

  • error.js 파일을 만들면 중첩된 경로에서 예기치 않은 런타임 오류를 적절하게 처리할 수 있다.
  • 중첩된 하위 세그먼트 또는 page.jsx 구성요소를 래핑하는 React Error Boundary를 자동으로 생성한다.
  • error.js 파일에 내보낸 React 컴포넌트가 fallback 컴포넌트로 사용된다.
  • error.jsx 파일은 그 해당 디렉토리 안에서만 적용된다.(page.jsx와 같은 폴더 안에 있어야 함)
"use client";//필수로 선언해줘야 함

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

배운점 & 느낀점

프레임워크라 확실히 간단하고 좋은 방법이 많아서 사용하기에 편해보이지만 외워야할것(?)이 많아서 더 헷갈리는 부분이 많다.
아직 데이터 패칭부분이 어렵지만 지금까지 배웠던 개념중에는 nextjs가 가장 간단하고 효과적인 코드인건 확실해보인다.
예제를 더 풀어보면서 열심히 손에 익혀야겠다.

profile
프론트는 순항중 ¿¿

0개의 댓글