LoginGustBookCategories
Frontend

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

thumbnailCreatehb21·2023년 8월 13일 11:52

https://velog.velcdn.com/images/alsghk9701/post/961ad79e-2167-40d8-bcb5-5d5a9f12a48d/image.jpeg


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


위 오픈 소스의 성능이 굉장히 뛰어나기 때문에, 나 또한 위 소스를 많이 참고하고 정독하면서 어떤 방식으로 코드를 작성했길래 퍼포먼스가 이렇게 뛰어날 수 있을까에 대해 많은 탐구를 하게 되었다.
그 중 가장 눈에 뛰는 부분 중 하나가 이번 포스팅에서 다루고자 하는 dynamic imports와 lazy-load에 관한 것이었다. 자 그럼 이제 시작해보자.

// 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
© 2021 Createhb21.