웹 성능 최적화 : Dynamic Import로 js 코드 스플릿하기

KJH·2023년 7월 22일
2

⏳ 동적 불러오기(Dynamic Import)에 대해 알아보자

🤨 동적 불러오기가 필요한 이유

일반적으로 spa 구조의 어플리케이션을 빌드할 때 js가 하나로 나오며 그 크기가 비대해지는 경험을 해봤을 것이다.

이렇게 되면 브라우저가 처음 index.html을 불러오고 DOM을 그릴때 js 로딩을 기다리는 시간이 길어지므로 FCP 즉 첫 화면이 그려지는데까지 오랜시간이 걸린다. 이는 CSR SPA 구조의 문제점이었다.

그렇다면 하나로 번들링되는 js를 쪼개서 빌드할 수 없을까?
라는 생각에서 나온 대안 중 하나가 바로 동적 불러오기이다.

동적 불러오기를 적용하게 되면 js를 쪼개서 빌드할 수 있게되고
이는 브라우저가 DOM을 그릴때 첫화면에 필요한 js만 로드해서 페인팅할 수 있다는 말로 FCP가 앞당겨지며 화면이 빨리 나타나 최종적으로 사용자 경험이 좋아진다.

적용전

적용후

🎯 적용 방법

일반적인 사용법

간단히 적용할 수 있다.
React이든 Vue든 사용방법은
const ** = ()=> import('./**')
위와 같은 형식으로 불러오는 파일을 읽어오면 된다.

적용하면 번들러로 무엇을 쓰든 js가 스필릿되어 빌드될 것이다.

보통 가장 많이 사용하는 곳은 라우터이다.
페이지를 로드할때 js 크기를 절감하여 화면을 빨리 그리려는 생각으로
라우터에 적용하게되고 일부 큰 크기가 컴포넌트에도 적용할 경우도 있다.

React에서 사용법

리액트에서는 lazy라는 함수를 사용해 비동기적으로 리액트 컴포넌트를 불러와 리턴해주어 사용하는 경우가 많다.

  • 사용예시
const HomePage = lazy(async () => await import('../pages/HomePage.tsx'));
  • 라우터 사용예시(react router dom v6)
const HomePage = lazy(async () => await import('../pages/HomePage.tsx'));

const router = createBrowserRouter([
    {
      path: '/',
      element: <HomePage />,
    },
  ]);
  • React Component 파일 내부에서 사용예시
const SomeComponent = lazy(async () => await import('./SomeComponent.tsx'))

function Example(){
	...

	return (
    	<>
        	<SomeComponent/>
        </>
    )
}

default export Example

Vue에서 사용법

뷰에서는 라우터에서 사용할때는 따로 전용함수를 사용하지 않아도 되지만
일반적인 vue 파일에서 사용할때는 defineAsyncComponent함수를 사용해서 불러와야한다.

  • 사용예시
const HomePage = async () => await import('../pages/HomePage.vue');
  • 라우터 사용예시(vue router)
const HomePage = async () => await import('../pages/HomePage.vue');

const routes: Array<RouteRecordRaw> = [
    {
      path: '/',
      name: 'home',
      component: HomePage,
    },
  ];
  
const router = createRouter({
  history: createWebHashHistory(),
  routes,
});
  • Vue 파일 내부에서 사용예시
<template>
...
<SomeComponent/>
...
<template/>

<script setup lang='ts'>
const SomeComponent = defineAsyncComponent(() => import('./SomeComponent.vue'));
<script/>

비동기적 페이지 로드에 Suspense 적용

페이지를 비동기적으로 불러올때 로딩시간이 길어진다면 흰화면을 보게 될 것으로 Suspense를 적용해 fallback UI를 적용하면 좋다.

React

<Suspense fallback={<div>loading...</div>}>
  <RouterProvider router={router} />
</Suspense>

Vue

<Suspense>
  <router-view/>
  <template #fallback>
    Loading...
  </template>
</Suspense>

위처럼 적용하면 각각 loading UI를 적용해두면 느린 네트워크에도 화면이 비지 않고 나오기 좋다. 즉 사용자 경험이 좋아진다.

추가적으로 Error UI 처리하기

React는 ErrorBoundary()를

Vue는 errorCaptured, onErrorCaptured

사용해서 Error UI를 구현하고 적용하면 된다.

적용예시

<ErrorBoundary>
  <TestPage />
</ErrorBoundary>

😶‍🌫️ 적용 시 장단점

장점

  • index.js 용량 감소된것을 확인할 수 있음
  • 각 페이지 별로 이동시 해당 페이지에서 사용하는 js만 불러서 초기 페이지 로드시 도움이 된다
  • 새로고침시 네트워크가 느리더라도 필요 js만 사용하기에 페이지 진입이 빠르다
  • 구버전을 사용하는 사용자 브라우저에서 오류가 있는 버전인 줄 모르고 사용할 수 있는 경우를 방지할 수 있음 (새로고침을 안하고 쭉 사용할 수 있는 경우 예방)

단점

  • 네트워크가 느리면 페이지 이동시 해당 페이지에서 사용하는 js를 다 받기 전까지 페이지 전환이 느릴 수 있다
  • 사용자의 브라우저에서 이전 배포버전에서 새로고침을 해서 js 최신화를 하지 않고 페이지 이동시 해당 이동 페이지에 해당하는 이전 버전 js 파일이 저장소에 존재하지 않아 새로고침을 해주어 다시 리소스를 받아야한다.
    (이 경우에 새로고침 전에 리소스를 가져오는데 실패했다는 오류 페이지가 잠깐 나오고 새로고침 된다.)
  • 리소스 관리를 달리하여 이전 버전의 js와 새로운 버전의 js를 둘다 가지고 있더라도 자원관리를 지속적으로 스케쥴링해서 삭제 해주어야함

🫡 해결법 및 적용 예시

여러 해결법이 있지만 에러 발생시 자동으로 새로고침하여 빌드된 저장소에서 새롭게 빌드된 파일을 받아오도록 하는 방법을 채택해서 적용했다.

리액트 라우터 사용예시

const retryLazy = (componentImport: any) =>
  lazy(async () => {
    const pageAlreadyRefreshed = JSON.parse(window.localStorage.getItem('pageRefreshed') ?? 'false');
    try {
      const component = await componentImport();
      window.localStorage.setItem('pageRefreshed', 'false');
      return component;
    } catch (error) {
      if (!pageAlreadyRefreshed) {
        window.localStorage.setItem('pageRefreshed', 'true');
        return window.location.reload();
      }
      throw error;
    }
  });

const HomePage = retryLazy(async () => await import('../pages/HomePage.tsx'));

const router = createBrowserRouter([
    {
      path: '/',
      element: <HomePage />,
    },
  ]);

Vue도 위와 유사하게 적용하면 된다.

참조사이트

https://stackoverflow.com/questions/53704950/webpack-code-splitting-loading-chunk-failed-error-wrong-file-path/62038528#62038528

https://velog.io/@goon126/%EC%B2%AD%ED%81%AC-%EC%97%90%EB%9F%AC

profile
Front-End Developer

0개의 댓글