How to lazy load Client Components and libraries

김동현·2026년 3월 4일

next.js 공식문서 번역

목록 보기
27/79

Next.js의 지연 로딩(Lazy loading)은 라우트(페이지)를 렌더링하는 데 필요한 자바스크립트의 양을 줄여주어 애플리케이션의 초기 로딩 성능을 획기적으로 향상시키는 데 도움을 줍니다.

이 기능을 사용하면 클라이언트 컴포넌트(Client Components) 와 외부에서 가져온 라이브러리의 로딩 시점을 뒤로 미룰 수(defer) 있어요. 즉, 당장 필요하지 않은 코드는 빼두었다가 클라이언트(브라우저)에서 정말로 필요해지는 순간에만 클라이언트 번들에 포함시키는 거죠. 예를 들어, 사용자가 클릭해서 열기 전까지는 모달(Modal) 창의 코드를 굳이 불러오지 않도록 지연시키는 것이 대표적인 예입니다.

💡 실무 팁:
초기 로딩 속도 최적화는 프론트엔드 개발자의 핵심 역량입니다. 모든 코드를 한 번에 불러오면 화면이 뜨기까지 딜레이가 생겨 사용자 경험(UX)이 떨어집니다. 지연 로딩을 적절히 활용하면 불필요한 네트워크 요청을 줄여 빠릿빠릿한 웹을 만들 수 있습니다.

Next.js에서 지연 로딩을 구현하는 방법은 크게 두 가지가 있습니다:

  1. next/dynamic을 활용한 동적 임포트(Dynamic Imports)
  2. Suspense와 함께 React.lazy() 사용하기

기본적으로 서버 컴포넌트(Server Components)는 자동으로 코드 스플리팅(code split)이 적용됩니다. 그리고 스트리밍(streaming) 기술을 사용해서 서버에서 클라이언트로 UI 조각들을 점진적으로 전송할 수 있죠. 따라서 오늘 다루는 '지연 로딩'은 주로 클라이언트 컴포넌트에 적용되는 개념이라고 이해하시면 됩니다.


next/dynamic

next/dynamicReact.lazy()Suspense를 하나로 묶어놓은 합성 도구입니다. 이 도구는 app 디렉토리와 구버전인 pages 디렉토리 양쪽에서 동일한 방식으로 작동하기 때문에, 점진적인 시스템 마이그레이션이 가능합니다.


예제 모음 (Examples)

클라이언트 컴포넌트 가져오기 (Importing Client Components)

아래 코드는 클라이언트 컴포넌트들을 다양한 방식으로 불러오는 예시입니다.

//filename="app/page.js"
'use client'

import { useState } from 'react'
import dynamic from 'next/dynamic'

// Client Components:
const ComponentA = dynamic(() => import('../components/A'))
const ComponentB = dynamic(() => import('../components/B'))
const ComponentC = dynamic(() => import('../components/C'), { ssr: false })

export default function ClientComponentExample() {
  const [showMore, setShowMore] = useState(false)

  return (
    <div>
      {/* 즉시 로드되지만, 별도의 클라이언트 번들로 분리되어 로드됩니다. */}
      <ComponentA />

      {/* 조건이 충족될 때만, 온디맨드(요청 시) 방식으로 로드됩니다. */}
      {showMore && <ComponentB />}
      <button onClick={() => setShowMore(!showMore)}>Toggle</button>

      {/* 오직 클라이언트 사이드에서만 로드됩니다 (SSR 무시). */}
      <ComponentC />
    </div>
  )
}

참고: 서버 컴포넌트 내부에서 클라이언트 컴포넌트를 동적으로(dynamically) 임포트하는 경우, 자동 코드 스플리팅은 현재 지원되지 않습니다.

SSR (서버 사이드 렌더링) 건너뛰기 (Skipping SSR)

React.lazy()와 Suspense를 사용할 때, 클라이언트 컴포넌트라 할지라도 기본적으로 서버 측에서 미리 렌더링(prerendered)이 진행됩니다 (SSR 적용).

참고: ssr: false 옵션은 오직 클라이언트 컴포넌트에서만 작동합니다. 클라이언트에서의 코드 스플리팅이 제대로 작동하도록 보장하려면, 이 옵션은 클라이언트 컴포넌트 내부에서 적용하세요.

만약 특정 클라이언트 컴포넌트에 대해 서버에서의 프리렌더링(사전 렌더링)을 아예 비활성화하고 싶다면, ssr 옵션을 false로 설정할 수 있습니다:

const ComponentC = dynamic(() => import('../components/C'), { ssr: false })

💡 부연 설명 & 팁:
"클라이언트 컴포넌트인데 왜 굳이 ssr: false를 또 쓰나요?" 라고 궁금하실 수 있습니다. Next.js는 기본적으로 빠른 초기 화면을 보여주기 위해 클라이언트 컴포넌트라도 HTML 껍데기를 서버에서 먼저 그려서 내려보냅니다. 하지만 windowdocument 같은 브라우저 전용 객체를 사용하는 라이브러리(예: 차트, 지도 맵, 리치 텍스트 에디터)는 서버(Node.js) 환경에서 에러를 뿜어냅니다. 이럴 때 ssr: false를 사용하면 에러를 방지하고 순수하게 브라우저에서만 컴포넌트를 그리도록 강제할 수 있습니다.

서버 컴포넌트 가져오기 (Importing Server Components)

만약 서버 컴포넌트를 동적으로 임포트하게 되면, 서버 컴포넌트 그 자체가 지연 로딩되는 것은 아닙니다. 대신, 해당 서버 컴포넌트의 자식으로 있는 클라이언트 컴포넌트들만 지연 로딩이 적용됩니다.
또한 서버 컴포넌트 내부에서 이 방식을 사용하면, CSS 같은 정적 자산(static assets)을 미리 로드(preload)하는 데 도움이 됩니다.

//filename="app/page.js"
import dynamic from 'next/dynamic'

// Server Component:
const ServerComponent = dynamic(() => import('../components/ServerComponent'))

export default function ServerComponentExample() {
  return (
    <div>
      <ServerComponent />
    </div>
  )
}

참고: ssr: false 옵션은 서버 컴포넌트에서는 지원되지 않습니다. 서버 컴포넌트 안에서 이 옵션을 사용하려고 시도하면 에러가 발생할 거예요.
ssr: false는 서버 컴포넌트 내의 next/dynamic과 함께 사용할 수 없으니, 해당 로직은 클라이언트 컴포넌트 안으로 옮겨주세요.

외부 라이브러리 로딩하기 (Loading External Libraries)

컴포넌트뿐만 아니라 외부 라이브러리들도 표준 import() 함수를 사용하면 필요할 때만 불러오도록(on demand) 만들 수 있습니다.
아래 예시는 유사도 검색(fuzzy search)을 위해 fuse.js라는 외부 라이브러리를 사용하는 모습입니다. 이 모듈은 사용자가 검색창에 타이핑을 시작한 이후에야 클라이언트 측에 로드됩니다.

//filename="app/page.js"
'use client'

import { useState } from 'react'

const names = ['Tim', 'Joe', 'Bel', 'Lee']

export default function Page() {
  const [results, setResults] = useState()

  return (
    <div>
      <input
        type="text"
        placeholder="Search"
        onChange={async (e) => {
          const { value } = e.currentTarget
          // fuse.js 라이브러리를 동적으로 로딩합니다.
          const Fuse = (await import('fuse.js')).default
          const fuse = new Fuse(names)

          setResults(fuse.search(value))
        }}
      />
      <pre>Results: {JSON.stringify(results, null, 2)}</pre>
    </div>
  )
}

💡 부연 설명:
무거운 자바스크립트 라이브러리(데이터 파싱, 이미지 편집 등)를 첫 페이지 접속 시 무조건 로드하면 번들 사이즈가 엄청나게 커집니다. 이렇게 이벤트(onClick, onChange 등) 안에서 await import() 문법을 쓰면 꼭 필요할 때만 라이브러리를 다운받게 되어 매우 효율적입니다.

커스텀 로딩 컴포넌트 추가하기 (Adding a custom loading component)

동적으로 컴포넌트를 가져오는 동안 빈 화면이 보이면 곤란하겠죠? 로딩 중임을 알려주는 UI를 별도로 지정할 수 있습니다.

//filename="app/page.js"
'use client'

import dynamic from 'next/dynamic'

const WithCustomLoading = dynamic(
  () => import('../components/WithCustomLoading'),
  {
    loading: () => <p>Loading...</p>,
  }
)

export default function Page() {
  return (
    <div>
      {/* <WithCustomLoading/> 이 로드되는 동안 로딩 컴포넌트(<p>Loading...</p>)가 렌더링됩니다. */}
      <WithCustomLoading />
    </div>
  )
}

💡 팁:
실무에서는 단순히 텍스트만 넣기보다 스켈레톤(Skeleton) 컴포넌트나 스피너(Spinner) 아이콘 컴포넌트를 연결하여 사용자 경험을 더 매끄럽게 만듭니다.

네임드 익스포트(Named Exports) 가져오기

export default가 아닌 이름이 지정된 익스포트(Named export)를 동적으로 임포트하려면, import() 함수가 반환하는 Promise에서 .then()을 사용하여 해당 모듈을 직접 반환해주면 됩니다.

//filename="components/hello.js"
'use client'

export function Hello() {
  return <p>Hello!</p>
}
//filename="app/page.js"
import dynamic from 'next/dynamic'

// .then()을 통해 모듈 객체(mod)에서 Hello 만을 콕 집어 가져옵니다.
const ClientComponent = dynamic(() =>
  import('../components/hello').then((mod) => mod.Hello)
)

모든 문서에 대한 의미론적 개요(semantic overview)를 보시려면, https://nextjs.org/docs/sitemap.md 문서를 참조하세요.

사용 가능한 모든 문서의 전체 인덱스를 보시려면, https://nextjs.org/docs/llms.txt 문서를 참조하세요.

profile
프론트에_가까운_풀스택_개발자

0개의 댓글