Post

[NextJS] Suspense 사용하기

Suspense

<Suspense>는 자식 요소를 로드하기 전까지 화면에 대체 UI(Fallback)를 보여주는 React의 기능이다.

1
2
3
<Suspense fallback={<Loading />}>
  <Albums />
</Suspense>

Suspense는 children에 필요한 모든 코드와 데이터를 로딩할 때까지 loading fallback을 보여준다.

Suspense는 아래 데이터에 대해서만 활성화된다.

  • Relay와 Next.js 같이 Suspense가 가능한 프레임워크를 사용한 데이터 가져오기
  • lazy를 활용한 지연 로딩 컴포넌트
  • use를 사용해서 캐시된 Promise 값 읽기
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// lazy를 활용한 지연 로딩 컴포넌트
import { Suspense, lazy } from "react";

const Chart = lazy(() => import("./Chart"));

export default function Analytics() {
  return (
    <div>
      <h1>분석</h1>

      <Suspense fallback={<div>차트 로딩 ...</div>}>
        <Chart />
      </Suspense>
    </div>
  );
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// use를 사용해서 캐시된 Promise 값 읽기
export default function Albums({ artistId }) {
  const albums = use(fetchData(`/${artistId}/albums`));
  return (
    <ul>
      {albums.map(album => (
        <li key={album.id}>
          {album.title} ({album.year})
        </li>
      ))}
    </ul>
  );
}

export default function ArtistPage({ artist }) {
  return (
    <>
      <h1>{artist.name}</h1>
      <Suspense fallback={<Loading />}>
        <Albums artistId={artist.id} />
      </Suspense>
    </>
  );
}

또한 Effect 또는 이벤트 핸들러 내부에서 가져오는 데이터는 감지하지 않는다.

Suspense는 컴포넌트가 렌더링 중에 Promise를 throw 할 때만 이를 감지하여 fallback UI를 보여주는데, useEffect는 컴포넌트의 렌더링이 끝난 후 실행되기 때문에 Suspense는 이 상황을 감지할 수 없다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
console.log("1. 컴포넌트 함수 실행 시작");

function Component() {
  console.log("2. 렌더링 단계");

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

  useEffect(() => {
    console.log("4. 이펙트 실행 (렌더링 완료 후)");
    // 이 시점에는 이미 Suspense 판단이 끝남
    fetchData().then(setData);
  }, []);

  console.log("3. JSX 반환");
  return <div>{data || "로딩..."}</div>;
}

Next.js에서의 Suspense 사용

Next.js의 App Router에서 Suspense는 동적 경계를 표시하는 데 사용된다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import { Suspense } from "react";
import StaticComponent from "./StaticComponent";
import DynamicComponent from "./DynamicComponent";
import Fallback from "./Fallback";

export const experimental_ppr = true;

export default function Page() {
  return (
    <>
      <StaticComponent />
      <Suspense fallback={<Fallback />}>
        <DynamicComponent />
      </Suspense>
    </>
  );
}

Next.js는 빌드 시 StaticComponent와 fallback UI를 미리 렌더링하고, Suspense 안의 DynamicComponent는 사용자가 해당 페이지에 접근할 때 스트리밍된다.

스트리밍은 경로를 여러 개의 청크로 분할하고, 준비되는 대로 클라이언트에 점진적으로 스트리밍한다.

이를 통해 사용자는 전체 콘텐츠의 렌더링이 완료되기 전에 페이지의 일부를 즉시 볼 수 있다.

위에서 Suspense의 예제는 React에서 lazyuse를 사용한 것이었다.

Next.js의 App Router에서는 Suspense의 children이 비동기 서버 컴포넌트인 경우에도 Suspense가 활성화된다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import { Suspense } from "react";

// 서버 컴포넌트
async function UserList() {
  const users = await fetch("https://jsonplaceholder.typicode.com/users").then(
    (res) => res.json()
  );

  return (
    <ul>
      {users.map((user) => (
        <li key={user.id}>{user.name}</li>
      ))}
    </ul>
  );
}

export default function UsersPage() {
  return (
    <div>
      <h1>사용자 목록</h1>
      <Suspense fallback={<div>사용자 정보 로딩 ...</div>}>
        <UserList />
      </Suspense>
    </div>
  );
}

클라이언트 컴포넌트에서 Suspense를 활성화하려면 서버 컴포넌트로부터 Promise 데이터를 prop으로 전달받아 use 훅으로 감싸면 된다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// app/ui/posts.tsx
'use client'
import { use } from 'react'

export default function Posts({
  posts,
}: {
  posts: Promise<{ id: string; title: string }[]>
}) {
  const allPosts = use(posts)

  return (
    <ul>
      {allPosts.map((post) => (
        <li key={post.id}>{post.title}</li>
      ))}
    </ul>
  )
}

// app/blog/page.tsx
import Posts from '@/app/ui/posts'
import { Suspense } from 'react'

export default function Page() {
  // Don't await the data fetching function
  const posts = getPosts()

  return (
    <Suspense fallback={<div>Loading...</div>}>
      <Posts posts={posts} />
    </Suspense>
  )
}
This post is licensed under CC BY 4.0 by the author.