본문 바로가기

Next

Loading.jsx와 Suspense fallback의 차이

Suspense와 laoding.jsx의 차이가 궁금해서 알아보았다.

 

Suspense

  • Susepnse는 비동기 컴포넌트가 렌더링 중일 때 작동한다.
  • 그렇다면, 여기서 비동기 컴포넌트란 무엇일까? 비동기 컴포넌트란, 렌더링되기 전에 데이터를 가져오거나, 어떤 비동기 작업을 수행한 후에야 완전히 준비되는 컴포넌트를 말한다. 주로 Server Components, Suspense 등과 사용한다.
// Client Component에서 비동기 처리
import { useEffect, useState } from 'react';

function AsyncComponent() {
  const [data, setData] = useState(null);

  useEffect(() => {
    fetch('/api/data')
      .then(res => res.json())
      .then(json => setData(json));
  }, []);

  if (!data) return <p>Loading...</p>;

  return <div>{data.message}</div>;
}
// Server Component에서 비동기 처리
// 서버 컴포넌트에서 async 사용 가능 (Next.js App Router)

async function fetchData() {
  const res = await fetch('https://api.example.com/data', { cache: 'no-store' });
  return res.json();
}

export default async function Page() {
  const data = await fetchData();

  return <div>{data.title}</div>;
}
// Suspense를 활용한 비동기 컴포넌트 로딩
// 앱이 시작될 때 즉시 로딩되는 기존 import문과 다르게, 해당 컴포넌트가 실제로 필요할 때 동적 로딩되어 앱의 초기 로딩 속도 감소
// LazyComponent가 실제로 화면에 나타날 때만 불러옴
"use client"

const LazyComponent = React.lazy(() => import('./SomeComponent'));

function App() {
  return (
    <Suspense fallback={<p>Loading...</p>}>
      <LazyComponent />
    </Suspense>
  );
}

 

위의 내용을 바탕으로, 비동기 컴포넌트에 대해서 이해했고, suspense를 활용해보았다.

import { Suspense } from "react";
import UserInfo from "./userInfo";

export default function Example1Page() {
  return (
    <div>
      <h1>Example1Page</h1>
      <Suspense fallback={<p>사용자 정보 로딩 중...</p>}>
        <UserInfo />
      </Suspense>
    </div>
  );
}

export default async function UserInfo() {
  // Simulate a delay to mimic data fetching
  await new Promise((resolve) => setTimeout(resolve, 2000));

  return <p>사용자 정보 불러오기 완료!</p>;
}

 

 

loading.jsx는 언제 작동할까?

  • loading.jsx는 클라이언트에서 fetch가 시작되는 시점에 작동하는게 아니라, App Router에서 라우트 전환 시 해당 서버 컴포넌트가 아직 로드되지 않았을 때 자동으로 보여주는 페이지 레벨 로딩 UI이다.
  • loading.jsx는 서버 컴포넌트이면서 비동기 컴포넌트일 때 작동한다.
export default async function SSRLoadingPage() {
  console.log("[SERVER] Fetching user...");
  await new Promise((res) => setTimeout(res, 3000)); // 3초 대기
  const res = await fetch("https://jsonplaceholder.typicode.com/users/1");
  const user = await res.json();

  return (
    <div>
      <h1>서버 컴포넌트 페이지</h1>
      <p>이름: {user.name}</p>
    </div>
  );
}
export default function Loading() {
  return <p>서버 컴포넌트 로딩 중입니다...</p>;
}

 

 

반례

  • 클라이언트 컴포넌트에서 fetch를 하면, 브라우저가 렌더링을 먼저 완료한 뒤, useEffect()가 실행되면서 데이터를 가져오기 때문에 loading.jsx는 등장하지 않는다.
    • 서버 컴포넌트가 아직 완전히 렌더링되지 않았을 때 loading.jsx가 나타나지만, 클라이언트 컴포넌트에서는 이미 화면이 렌더링된 상태에서 fetch가 시작되기 때문에, Next.js가 로딩 상태를 감지하거나 loading.jsx를 보여줄 수 타이밍이 없다.
"use client";

import { useEffect, useState } from "react";

export default function ClientFetchPage() {
  const [user, setUser] = useState(null);

  useEffect(() => {
    console.log("[CLIENT] useEffect(fetch) 시작");
    fetch("https://jsonplaceholder.typicode.com/users/1")
      .then((res) => res.json())
      .then((data) => {
        console.log("[CLIENT] fetch 완료");
        setUser(data);
      });
  }, []);

  if (!user) {
    return <p>클라이언트 useEffect로 로딩 중...</p>;
  }

  return (
    <div>
      <h1>클라이언트 컴포넌트 페이지</h1>
      <p>이름: {user.name}</p>
    </div>
  );
}

 

 

정리

상황 loading.jsx Suspense fallback

  loading.jsx Suspense fallback
라우트 전환 시 첫 진입 ✅ 자동 실행 ❌ X
비동기 컴포넌트 개별 로딩 제어 ❌ X ✅ 개별 감싸기 가능
자동 설정 ✅ 디렉토리명 기반 자동 인식 ❌ 직접 Suspense 작성 필요
페이지 전체 vs 컴포넌트 단위 구분 전체 segment 기준 컴포넌트별 로딩 처리

'Next' 카테고리의 다른 글

mutateAsync 도입기  (0) 2025.11.14
Strict Mode와 useEffect의 반복 실행 이슈 정리  (0) 2025.10.27
Next.js 프로젝트 구조, src/app으로 써야 할까?  (0) 2025.10.19
bodySizeLimit 설정  (0) 2025.06.16