Next.js에서 dynamic imports를 사용한 Code splitting

이효범·2022년 6월 5일
7

React

목록 보기
2/5
post-thumbnail

최근 새로운 프로젝트를 진행하며, Lighthouse CI와 Github-actions를 이용하여 어플리케이션의 성능 검사를 하고자 하면서 어플리케이션의 성능을 최대치로 끌어올릴 수 있는 방법은 어떠한 것들이 있을까에 대한 관심이 많아졌다.
Next.js를 만든 vercel은 commerce라는 베스트 프렉티스 어플리케이션 오픈 소스를 공개하고 있다

vercel/commerce

위 오픈 소스의 성능이 굉장히 뛰어나기 때문에, 나 또한 위 소스를 많이 참고하고 정독하면서 어떤 방식으로 코드를 작성했길래 퍼포먼스가 이렇게 뛰어날 수 있을까에 대해 많은 탐구를 하게 되었다.

그 중 가장 눈에 뛰는 부분 중 하나가 이번 포스팅에서 다루고자 하는 dynamic imports와 lazy-load에 관한 것이었다. 자 그럼 이제 시작해보자.

코드 분할 및 스마트 로딩 전략으로 Next.js 앱 속도를 높이는 방법.

출처 : Milica Mihajlija , vercel/with-dynamic-import


// import Puppy from "../components/Puppy";
import dynamic from "next/dynamic";

// ...

const Puppy = dynamic(import("../components/Puppy"));

앱을 처음 로드하면 index.js만 다운로드, Puppy 컴포넌트에 대한 코드가 포함되어 있지 않기 때문에 사이즈가 0.5KB 더 작다 (37.9KB에서 37.4KB로 감소).

실제 적용 시, 구성 요소가 훨씬 더 큰 경우가 많으며, 컴포넌트를 필요 시 lazyload를 활용하면 초기 JavaScript 페이로드를 수백 킬로바이트까지 줄일 수 있다.

리소스를 lazyload할 때 지연이 있는 경우에 대비하여 로드 표시기를 제공하는 것이 좋으며 Next.js에서, dynamic() 함수에 추가 인수를 제공하여 이러한 작업을 수행할 수 있다.

const Puppy = dynamic(() => import("../components/Puppy"), {
  loading: () => <p>Loading...</p>
});

클라이언트 측에서만 구성 요소를 렌더링해야 하는 경우(예: 채팅 위젯) ssr 옵션을 false로 설정하여 이러한 작업을 수행할 수 있다.

const Puppy = dynamic(() => import("../components/Puppy"), {
  ssr: false,
});

다음은 vercel에서 제시한 dynamic imports에 대한 example 코드

import { useState } from 'react'
import Header from '../components/Header'
import dynamic from 'next/dynamic'

const DynamicComponent1 = dynamic(() => import('../components/hello1'))

const DynamicComponent2WithCustomLoading = dynamic(
  () => import('../components/hello2'),
  { loading: () => <p>Loading caused by client page transition ...</p> }
)

const DynamicComponent3WithNoSSR = dynamic(
  () => import('../components/hello3'),
  { loading: () => <p>Loading ...</p>, ssr: false }
)

const DynamicComponent4 = dynamic(() => import('../components/hello4'))

const DynamicComponent5 = dynamic(() => import('../components/hello5'))

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

const IndexPage = () => {
  const [showMore, setShowMore] = useState(false)
  const [falsyField] = useState(false)
  const [results, setResults] = useState()

  return (
    <div>
      <Header />

      {/* Load immediately, but in a separate bundle */}
      <DynamicComponent1 />

      {/* Show a progress indicator while loading */}
      <DynamicComponent2WithCustomLoading />

      {/* Load only on the client side */}
      <DynamicComponent3WithNoSSR />

      {/* This component will never be loaded */}
      {falsyField && <DynamicComponent4 />}

      {/* Load on demand */}
      {showMore && <DynamicComponent5 />}
      <button onClick={() => setShowMore(!showMore)}>Toggle Show More</button>

      {/* Load library on demand  요청 시 라이브러리 로드 */} 
      <div style={{ marginTop: '1rem' }}>
        <input
          type="text"
          placeholder="Search"
          onChange={async (e) => {
            const { value } = e.currentTarget
            // Dynamically load 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>
    </div>
  )
}

export default IndexPage

다음은 Next.js에서 만든 commerce 어플리케이션의 코드 중 레이아웃에 관한 한 부분 코드이다.

import cn from 'clsx'
import s from './Layout.module.css'
import dynamic from 'next/dynamic'
import { useRouter } from 'next/router'
import { CommerceProvider } from '@framework'
import LoginView from '@components/auth/LoginView'
import { useUI } from '@components/ui/context'
import { Navbar, Footer } from '@components/common'
import ShippingView from '@components/checkout/ShippingView'
import CartSidebarView from '@components/cart/CartSidebarView'
import { useAcceptCookies } from '@lib/hooks/useAcceptCookies'
import { Sidebar, Button, LoadingDots } from '@components/ui'
import PaymentMethodView from '@components/checkout/PaymentMethodView'
import CheckoutSidebarView from '@components/checkout/CheckoutSidebarView'
import { CheckoutProvider } from '@components/checkout/context'
import { MenuSidebarView } from '@components/common/UserNav'
import type { Page } from '@commerce/types/page'
import type { Category } from '@commerce/types/site'
import type { Link as LinkProps } from '../UserNav/MenuSidebarView'

const Loading = () => (
  <div className="w-80 h-80 flex items-center text-center justify-center p-3">
    <LoadingDots />
  </div>
)

const dynamicProps = {
  loading: Loading,
}

const SignUpView = dynamic(() => import('@components/auth/SignUpView'), {
  ...dynamicProps,
})

const ForgotPassword = dynamic(
  () => import('@components/auth/ForgotPassword'),
  {
    ...dynamicProps,
  }
)

const FeatureBar = dynamic(() => import('@components/common/FeatureBar'), {
  ...dynamicProps,
})

const Modal = dynamic(() => import('@components/ui/Modal'), {
  ...dynamicProps,
  ssr: false,
})

interface Props {
  pageProps: {
    pages?: Page[]
    categories: Category[]
  }
}

const ModalView: React.FC<{ modalView: string; closeModal(): any }> = ({
  modalView,
  closeModal,
}) => {
  return (
    <Modal onClose={closeModal}>
      {modalView === 'LOGIN_VIEW' && <LoginView />}
      {modalView === 'SIGNUP_VIEW' && <SignUpView />}
      {modalView === 'FORGOT_VIEW' && <ForgotPassword />}
    </Modal>
  )
}

const ModalUI: React.FC = () => {
  const { displayModal, closeModal, modalView } = useUI()
  return displayModal ? (
    <ModalView modalView={modalView} closeModal={closeModal} />
  ) : null
}

const SidebarView: React.FC<{
  sidebarView: string
  closeSidebar(): any
  links: LinkProps[]
}> = ({ sidebarView, closeSidebar, links }) => {
  return (
    <Sidebar onClose={closeSidebar}>
      {sidebarView === 'CART_VIEW' && <CartSidebarView />}
      {sidebarView === 'SHIPPING_VIEW' && <ShippingView />}
      {sidebarView === 'PAYMENT_VIEW' && <PaymentMethodView />}
      {sidebarView === 'CHECKOUT_VIEW' && <CheckoutSidebarView />}
      {sidebarView === 'MOBILE_MENU_VIEW' && <MenuSidebarView links={links} />}
    </Sidebar>
  )
}

const SidebarUI: React.FC<{ links: LinkProps[] }> = ({ links }) => {
  const { displaySidebar, closeSidebar, sidebarView } = useUI()
  return displaySidebar ? (
    <SidebarView
      links={links}
      sidebarView={sidebarView}
      closeSidebar={closeSidebar}
    />
  ) : null
}

const Layout: React.FC<Props> = ({
  children,
  pageProps: { categories = [], ...pageProps },
}) => {
  const { acceptedCookies, onAcceptCookies } = useAcceptCookies()
  const { locale = 'en-US' } = useRouter()
  const navBarlinks = categories.slice(0, 2).map((c) => ({
    label: c.name,
    href: `/search/${c.slug}`,
  }))

  return (
    <CommerceProvider locale={locale}>
      <div className={cn(s.root)}>
        <Navbar links={navBarlinks} />
        <main className="fit">{children}</main>
        <Footer pages={pageProps.pages} />
        <ModalUI />
        <CheckoutProvider>
          <SidebarUI links={navBarlinks} />
        </CheckoutProvider>
        <FeatureBar
          title="This site uses cookies to improve your experience. By clicking, you agree to our Privacy Policy."
          hide={acceptedCookies}
          action={
            <Button className="mx-5" onClick={() => onAcceptCookies()}>
              Accept cookies
            </Button>
          }
        />
      </div>
    </CommerceProvider>
  )
}

export default Layout
profile
I'm on Wave, I'm on the Vibe.

0개의 댓글