Next.js - next/dynamic와 React Suspense로 로딩 경험 극대화하기

Maria Kim·2025년 3월 17일
0
post-thumbnail

Next.js를 사용할 때, 퍼포먼스와 사용자 경험을 동시에 높이는 대표적인 방법 중 하나가 코드 스플리팅레이지 로딩(Lazy Loading)입니다.

특히 next/dynamic은 “React.lazy()와 Suspense의 조합”처럼 보이지만, 실제로는 데이터 페칭에서 사용하는 Suspense와는 조금 다른 작동 방식을 가집니다.

이번 포스트에서는 next/dynamic를 제대로 이해하고, React Suspense와 함께 사용해 이중 로딩 지연을 최소화하는 방법을 살펴보겠습니다.

1. next/dynamic 한눈에 살펴보기

  • 클라이언트 전용 실행: use client를 써도 Next.js는 보통 서버에서 간단한 HTML을 만들어 보내지만, ssr: false 설정을 통해 완전히 클라이언트에서만 렌더링하도록 강제할 수 있습니다.
  • 레이지 로딩: 필요할 때만 import를 실행하여 번들 크기를 줄이고 초기 로딩 속도를 높입니다.
  • 커스텀 로딩 UI: loading 옵션을 사용하면 실제 컴포넌트가 다운로드되는 동안 보일 플레이스홀더(placeholder)를 지정할 수 있습니다.
import dynamic from 'next/dynamic';

const LazyLoadedComponent = dynamic(() => import('./MyComponent'), {
  ssr: false,
  loading: () => <p>Loading component...</p>,
});

위 설정으로 서버 사이드 렌더링을 하지 않음(ssr: false)을 명시하면, Next.js는 미니멀한 HTML과 Loading component... 메시지만 보내고, 브라우저에서 JS 파일이 로드된 후에야 실제 UI를 렌더링합니다.


2. next/dynamic vs. React.lazy() & Suspense

Next.js 문서에는 next/dynamic가 “React.lazy()와 Suspense의 합성”이라고 표현되어 있지만, 아래 차이점을 유의해야 합니다:

  1. loading 옵션은 어디까지나 “컴포넌트 import”에 대한 로딩 UI
  • React.lazy()에서 자동으로 Suspense 경계가 걸리는 것과 유사하지만, 이는 데이터 페칭 시 발생하는 비동기 로직을 대신 처리해주지 않습니다.
  1. 데이터 로딩은 별도의 Suspense 처리
  • 만약 useSuspenseQuery(React Query)처럼 Suspense를 활용하는 데이터 페칭을 한다면, 컴포넌트가 이미 로드된 후에도 “데이터 로딩 상태”에서 Suspense가 다시 발동될 수 있습니다.

즉, 컴포넌트를 다운받는 단계(Next.js의 loading)와, 컴포넌트가 데이터를 불러올 때 쓰는 Suspense 로딩은 별도로 동작합니다. 둘을 잘 조합하지 않으면, 서로 다른 로딩 UI가 순식간에 번갈아 나타날 수 있습니다.

3. Suspense를 왜 사용해야 할까?

React Suspense는 비동기 작업(데이터 페칭, 코드 스플리팅 등)이 완료될 때까지 UI 렌더링을 잠시 ‘유보’하는 메커니즘입니다. 이 로직을 이용해, 네트워크 요청이나 비동기 계산이 진행되는 동안 미리 정해둔 Fallback UI를 보여줄 수 있습니다.

다음 예시를 보겠습니다:

'use client';

import { Suspense } from 'react';
import dynamic from 'next/dynamic';

import { useClientStore } from '@/providers/client-store-provider';
import PantryBoxesSkeleton from './PantryBoxes/PantryBoxesSkeleton';

const GuestUserPantry = dynamic(() => import('./GuestUserPantry'), {
  ssr: false,
  loading: PantryBoxesSkeleton,
});
const LogInUserPantry = dynamic(() => import('./LogInUserPantry'), {
  ssr: false,
  loading: PantryBoxesSkeleton,
});

export default function Pantry() {
  // 예: Zustand로 로그인 상태 확인
  const isLoggedIn = useClientStore((state) => state.user.isLoggedIn);
  const Component = isLoggedIn ? LogInUserPantry : GuestUserPantry;

  return (
    <Suspense fallback={<PantryBoxesSkeleton />}>
      <Component />
    </Suspense>
  );
}
  • 컴포넌트 import 단계: next/dynamic의 loading을 통해 첫 로딩 시 PantryBoxesSkeleton을 노출합니다.
  • 데이터 페칭 단계: 만약 GuestUserPantryLogInUserPantry 안에서 React Query의 useSuspenseQuery 등으로 Suspense가 발생하면, <Suspense fallback={<PantryBoxesSkeleton />}>가 작동하여 다시 같은 스켈레톤을 보여주게 됩니다.

이런 식으로 일관된 로딩 화면을 써서 이중 로딩 UI가 따로따로 뜨는 혼선을 줄일 수 있습니다.

4. 핵심 요약

  1. 완전한 클라이언트 전용이라면 next/dynamic + ssr: false 조합
  • 서버 사이드 렌더링이 필요 없는 코드(브라우저 전용 API 활용 등)에 적합합니다.
  1. next/dynamic의 loading vs Suspense의 fallback
  • loading은 “컴포넌트 다운로드” 지연을 다루고,
  • Suspense의 fallback은 “데이터 로딩”(또는 기타 비동기 작업)을 다룹니다.
  1. 일관된 로딩 컴포넌트 사용
  • 불필요한 UI 점프를 막기 위해, loading과 Suspense fallback에 같은 스켈레톤을 사용하는 전략이 유리합니다.
  1. React.lazy()와의 차이
  • React.lazy()도 비슷한 방식으로 코드 분할을 지원하지만, Next.js 환경에서 SSR 제어나 라우팅 연계 등에 있어서는 next/dynamic이 훨씬 유연합니다.

결론

Next.js의 next/dynamic는 “React.lazy()+Suspense”처럼 보이지만, 실제로는 컴포넌트 import에 대한 로딩만 책임질 뿐, 데이터 페칭을 위한 Suspense는 별도로 동작합니다. 따라서:

  • next/dynamic을 사용해 코드 스플리팅과 클라이언트 전용 렌더링을 제어하고,
  • React Suspense(예: useSuspenseQuery)로 데이터가 준비될 때까지 일관된 로딩 화면을 보여주는 전략

이 두 가지를 잘 결합하면, 부드러운 로딩 경험과 최적화된 성능을 동시에 얻을 수 있습니다.

---

Next.js 애플리케이션에서 이중 로딩이나 UI 깜빡임이 우려된다면, 꼭 ssr: false와 Suspense를 함께 고려해 보세요!

profile
Developer, who has business in mind.

0개의 댓글