Next.js 튜토리얼 (3) - 비동기 요청부터 제어까지

sham·2024년 11월 20일
0

Next.js 제로베이스

목록 보기
3/9
post-thumbnail

Next의 데이터 요청

클라이언트 사이드

  • 기존의 CSR에서 데이터를 요청할 때는 브라우저에 API에 요청을 보내고, 기다린 후 응답을 받아 데이터를 뿌려주는 방식이었다.
  • 개발자 도구의 네트워크 항목에서 해당 요청을 조회할 수 있기에 보안적으로 취약하다.
  • 클라이언트 단에서 API를 요청하면 로딩 상태를 필수적으로 거치면서 이에 대한 상태, 처리를 고려해야 했다.
"use client";

import { useState, useEffect } from "react";

const url = "https://api.nationalize.io/";

export default function Name() {
  const [data, setData] = useState(null); // 데이터를 저장할 상태
  const [loading, setLoading] = useState(true); // 로딩 상태 관리

  useEffect(() => {
    // 데이터를 비동기로 가져오는 함수
    async function fetchData() {
      try {
        const response = await fetch(url + "?name=john");
        const result = await response.json();
        setData(result); // 데이터 저장
      } catch (error) {
        console.error("Failed to fetch nationality data:", error);
      } finally {
        setLoading(false); // 로딩 상태 종료
      }
    }

    fetchData();
  }, []); // 컴포넌트가 처음 렌더링될 때 한 번 실행

  return (
    <div>
      <h1>name page.js</h1>
      <p>name page.js is a page.js in the name directory.</p>
      <div>
        {/* 로딩 중 메시지 표시 */}
        {loading && <p>Loading...</p>}

        {/* 데이터 렌더링 */}
        {data &&
          data.country.map((country) => (
            <div key={country.country_id}>
              <p>{country.country_id}</p>
              <p>{country.probability}</p>
            </div>
          ))}

        {/* 데이터가 없는 경우 */}
        {!loading && !data && <p>No data found.</p>}
      </div>
    </div>
  );
}

서버 사이드

해당 서버 컴포넌트의 선언 함수, API 요청 함수에 async를 지정해주고 await으로 비동기 요청을 하면 자동으로 data안에 응답이 들어오고 해당 응답을 가지고 next는 HTML 파일을 전송해 줄 수 있다.

  • useState, useEffect로 데이터와 로딩 상태를 하나하나 고려해주는 기존의 방식이 아닌, 프레임워크인 Next가 서버에서 데이터를 미리 가져와서 렌더링된 HTML을 클라이언트에 전송한다.
  • 클라이언트에서는 데이터를 로딩하는 경험을 하지 않고, 플래시 없이 렌더링된 페이지만을 제공받을 수 있다.
  • API를 자동으로 캐싱해주기에 반복된 요청 시 캐싱된 데이터를 제공해준다.
  • 백엔드, 서버 단에서 요청하기에 로딩 상태는 서버에서 관리하게 된다. 즉, 로딩 중에는 HTML 페이지 자체의 접속이 지연되는 것이다.
export const metadata = {
  title: "name page.js",
};

const url = "https://api.nationalize.io/";

async function getNationality(name) {
  const response = await fetch(url + "?name=" + name);
  const data = await response.json();
  return data;
}

export default async function Name() {
  const data = await getNationality("john");

  return (
    <div>
      <h1>name page.js</h1>
      <p>name page.js is a page.js in the name directory.</p>
      <div>
        {data.country.map((country) => (
          <div key={country.country_id}>
            <p>{country.country_id}</p>
            <p>{country.probability}</p>
          </div>
        ))}
      </div>
    </div>
  );
}

Loading State

데이터를 요청되는 동안의 상태를 정의해주어야 하는데, loading.js라는 파일이 존재하면 Next가 자동으로 비동기 작업이 진행되는 동안, loading.js 의 컴포넌트를 대신해서 클라이언트 단에 보내준다.

Next 프레임워크가 페이지를 잘게 나눠 준비된 부분부터 먼저 보여주고 있는 것이다.

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

동작 원리

  • Suspense 기반의 로딩 처리 Next.js는 페이지나 레이아웃 컴포넌트를 로드할 때 React의 Suspense를 사용하는데, 비동기 작업이 진행 중일 때 로딩 상태를 렌더링하도록 설계되었다.
    • loading.js 파일은 기본적으로 React의 Suspense에 의해 로딩 상태로 사용.
  • 비동기 렌더링 감지 Next.js의 App Router는 컴포넌트가 비동기 데이터 패칭(예: fetch, async 함수)이나 동적 import로 인해 로드가 지연되면, 이를 감지하고 loading.js를 렌더링한다.
  • 파일 시스템 기반 라우팅 특정 경로에 loading.js 파일이 있으면, Next.js는 해당 경로와 연관된 비동기 작업이 있을 때 이를 자동으로 사용한다.
    • app/dashboard/loading.js 파일은 /dashboard 경로와 관련된 로딩 상태를 처리.
  • 로딩 상태 종료 데이터 패칭이 완료되거나 페이지 컴포넌트가 준비되면, loading.js의 렌더링이 종료되고 실제 페이지가 화면에 렌더링한다.

중복 요청 제어

싱글 스레드인 JS로 여러 요청을 순차적으로 처리하게 되면, 한 요청이 끝날 때까지 다음 코드에 접근할 수가 없게 된다.

Promise.all() 을 활용한 병렬 요청

Pormise.all을 활용해, 여러 개의 비동기 요청을 병렬적으로 실행시켜 각 요청에 대한 응답을 각각 받아 최종적으로 배열에 결과를 받아올 수 있다.

그러나 이 방법으로도 각 요청이 끝났을 때 각자 처리할 수 없고 모든 요청이 끝날 때까지 대기해야만 한다.

"use client";

import { useState, useEffect } from "react";

const urls = [
  "https://api.nationalize.io/?name=john",
  "https://api.nationalize.io/?name=alice",
  "https://api.nationalize.io/?name=emma",
];

export default function MultiFetch() {
  const [data, setData] = useState([]);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    async function fetchData() {
      try {
        const results = await Promise.all(
          urls.map(async (url) => {
            const response = await fetch(url);
            return response.json();
          })
        );
        setData(results);
      } catch (error) {
        console.error("Error fetching data:", error);
      } finally {
        setLoading(false);
      }
    }

    fetchData();
  }, []);

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

  return (
    <div>
      <h1>Nationality Data</h1>
      {data.map((result, index) => (
        <div key={index}>
          <h3>Request {index + 1}</h3>
          {result.country.map((country) => (
            <p key={country.country_id}>
              {country.country_id}: {country.probability}
            </p>
          ))}
        </div>
      ))}
    </div>
  );
}

Suspense를 활용한 병렬 요청

React에서 제공하는 Suspense를 활용한다면 독립적인 컴포넌트에서의 요청에 따른 상태를 개별적으로 관리할 수 있다.

또한, 각 요청을 병렬적으로 수행하면서 요청이 완료된 컴포넌트가 있으면 완료되는 대로 렌더링하게 해준다.

"use client";

const url = "https://api.nationalize.io/?name=aaron";

export default async function FirstRequest() {
  const response = await fetch(url);
  const data = await response.json();

  return (
    <div>
      <h3>First Request</h3>
      {data.country.map((country) => (
        <p key={country.country_id}>
          {country.country_id}: {country.probability}
        </p>
      ))}
    </div>
  );
}
import React, { Suspense } from "react";

import FirstRequest from "./call1";
import SecondRequest from "./call2";
import ThirdRequest from "./call3";

export default function sss() {
  return (
    <div>
      <h1>Suspense Example</h1>
      <Suspense fallback={<p>Loading First Request...</p>}>
        <FirstRequest />
      </Suspense>
      <Suspense fallback={<p>Loading Second Request...</p>}>
        <SecondRequest />
      </Suspense>
      <Suspense fallback={<p>Loading Third Request...</p>}>
        <ThirdRequest />
      </Suspense>
    </div>
  );
}

에러 핸들링 및 제어

try-catch 로 에러 핸링

export default async function Page() {
  try {
    const response = await fetch("https://api.example.com/data", {
      next: { revalidate: 60 },
    });

    if (!response.ok) {
      throw new Error("Failed to fetch data");
    }

    const data = await response.json();
    return (
      <div>
        <h1>Data:</h1>
        <pre>{JSON.stringify(data, null, 2)}</pre>
      </div>
    );
  } catch (error) {
    return <div>Error: {error.message}</div>;
  }
}

error.js로 에러 페이지 처리

에러 페이지(error.js)를 생성해 에러 발생 시 렌더링되도록 설정할 수 있다.

"use client";

export default function Error({ error, reset }) {
  return (
    <div>
      <h2>Something went wrong!</h2>
      <p>{error.message}</p>
      <button onClick={() => reset()}>Try again</button>
    </div>
  );
profile
씨앗 개발자

0개의 댓글