본문 바로가기

개발/React

Vite의 manualChunks옵션에 관하여

 

 

 

개요

이번에 사내 프로젝트를 팀원분이 CRA에서 Vite로 개선해주시면서 함께 고민했던 내용 중 잘못 이해했던 내용에 대해 회고하려고 한다.

 

 

서론

먼저 본론을 시작하기에 앞서 React 프로젝트를 작업하면서 사용하게 되는 Chunk(청크)에 대해 생각하면 어떤 상황들이 떠오르는지 생각해보겠다.

 

일반적으로는 React 프로젝트에서 자주 활용하는 상황은 Page단위로 Code Splitting을 적용하여 이들을 Lazy Load 하여 초기 로딩 성능을 향상시키는 상황을 자주 볼 수 있다.

 

예시는 아래와 같다.

 

import { lazy, Suspense, useEffect } from 'react'

const FirstPage = lazy(() => import('@pages/FirstPage'))
const SecondPage = lazy(() => import('@pages/SecondPage'))

/** 
 *  코드 스플리팅을 사용할 때에는 동적으로 load 되어 불러오는 동안 
 *  Loader를 보여주기 위해 Suspense와 함께 사용한다.
 */
 
<Suspense fallback={<Loader />}>
  <Route path="/first" element={<FirstPage />} />
  <Route path="/second" element={<SecondPage />} />
</Suspense>

 

 

위와 같이 적용하면 두 페이지에 대한 내용을 하나의 청크에 묶지 않고 각 페이지별로 청크 파일이 따로 생성되고, 실제로 해당 지면에 진입하지 않으면 다른 페이지에 대한 내용을 불러오지 않는다.

 

 

또 한가지 떠오르는 부분은 자주 볼 수 있는 에러 중 하나인 Chunk Load Error 인데, 이는 일반적으로 SPA환경에서 발견되며 빌드하여 생성된 청크를 유저에게 서빙하는 과정에서 새로 배포된 버전이 있음에도 불구하고 유저의 브라우저나 CDN에서 이전 버전의 청크를 캐싱할때 서버는 이를 찾을 수 없기 때문에 발생하는 에러이다. 이러한 경우는 보통 새로 진입하는 유저에게는 나타나지 않으나, 계속 서비스를 이용하고 있던 유저에게서 발생할 확률이 있으며 대부분의 경우에서는 자주 발생하지 않고 간헐적으로 발생하기에 새로고침을 통해 다시 새로 배포된 버전의 번들 파일을 요청할 수 있도록 하는 것이 좋다.

 

지금까지 했던 이야기 중에서 이번에 소개하고자 하는 내용과 관련이 있는 것은 첫번째로 이야기했던 Code Splitting을 통한 Lazy Load과 유사한 내용이다.

 

Vite로 마이그레이션하는 과정에서 실제로 특정 지면에서는 사용하지 않고있는 비교적 큰 규모의 외부 라이브러리를 메인 청크에서 분리하여 Lazy Load하고싶다는 생각이 들었다.

 

그러한 과정에서 Vite에 존재하는 manualChunks 옵션을 발견하게 되었다. (실제로는 내부적으로 번들러는 rollup을 사용하고있기에 rollup의 옵션이 맞다.)

 

따라서 이에 대해 적용하며 알아간 내용에 대해 적어보려고 한다.

 

 

본론

 

먼저 Vite에서는 import 구문을 감지하면 이에 대해서 head 태그에 modulepreload 속성을 가진 link 태그로 해당 라이브러리를 불러올 수 있도록 한다.

 

 

 

 

그렇다면 일반적으로 알고있는 preloadmodulepreload의 차이에 대해 먼저 알아보고 가자.

 

일단 공통점은 이름에서와 같이 둘 다 웹 성능 최적화를 위해 특정 자원을 미리 불러올 수 있도록 하는 데에 있다.

 

차이점은 다음과 같다.

 

preload

 

preload는 웹 페이지에서 필요로 하는 자원을 브라우저가 페이지의 나머지 부분을 처리하기 전에 미리 로드하도록 하는 것이다. 이때 preload를 통해 불러올 수 있는 자원이미지, 스크립트, 스타일시트, 폰트 등이 있다. 이렇게 사용할 때에는 여러가지 자원을 불러올 수 있는 태그이기때문에 as라는 속성을 통해 자원의 타입을 명시해 주어야한다. 이는 명시된 자원을 단일로 가지고온다.

 

 

modulepreload

 

modulepreload는 ES6 모듈 스크립트를 preload하려는 경우에 사용한다. 이는 모듈 스크립트와 해당 종속성을 미리 로드하여 모듈의 로드 시간을 단축시키고, 지연을 줄일 수 있다. 

 

 

그렇다면 어떠한 이유때문에 Direct import 구문에 대해 modulepreload로 처리하는 것일까 ?

 

그 이유는 아래와 같다.

 

너무 잘 설명되어있다.

 

 

위에도 너무 잘 정리되어있지만 간단하게 정리해보면 동적으로 불러오는 청크의 경우 처음부터 해당 청크에서 불러오려고 하는 청크에 대해서는 로드한 후에서야 알게된다. 이러한 흐름으로 인해 로딩에 딜레이가 생길 수 있기 때문에 이를 방지하기 위해 어차피 사용하게될 청크라면 해당 청크를 미리 불러와 위와 같은 지연을 제거하고자 함이다.

 

 

 

 

그렇다면 제목에서와 같이 Vite 프로젝트에서의 manualChunk는 어떤 역할을 하는 것일까 ?

 

처음 소개했던 청크 활용 케이스인 Page별 Code Splitting을 통한 Dynamic import 를 생각해보자.

 

일반적으로 페이지별로 Dynamic import를 적용하게되면 직전에 modulepreload의 도입 이유에서 본 안티 케이스와 같이 해당 페이지가 로드된 후에 이 모듈에서 호출하는 라이브러리를 호출하게 된다. 이렇게 될 경우 의도치 않게 로드하는 동안 지연시간에 생기게 된다.

 

그렇다면 이렇게 Dynamic import를 사용하면서도 미리 사용할 라이브러리를 preload해오는 방법은 무엇일까?

 

이때 사용하는 것이 manualChunk이다.

 

 

manualChunk는 이름 그대로 의도적으로 해당 라이브러리에 대한 청크를 나눌 수 있다. 

 

이렇게 청크를 나누게되면 자연스럽게 Direct import로 인지되어 이를 modulepreload로 불러오게되고 위와 같은 Dynamic import 케이스에서도 지연없이 로드할 수 있게된다.

 

다만 이런 경우 해당 라이브러리를 사용하지 않는 지면에서도 modulepreload가 동작하여 사전에 로드되어 정말 필요한 라이브러리만 manualChunk로 나누는 것이 좋다.

 

 

결론

 

처음 작업을 하는 과정에서 기존에 Dynamic Import를 하는 것과 동일하게 청크를 나누는 것이 지연 로딩의 역할을 해준다고 생각했었습니다. 하지만 적용하고나니 이를 사용하지 않는 지면에 접근하더라도 청크로 나눈 라이브러리를 불러오고있었고, 이로 인해 어떤 이유에서 이런 현상이 발생하는지 찾아보게 되었습니다.

 

혹시 잘못 알고있는 정보에 대한 지적이라면 언제든 환영입니다.

 

 

2024/4/17 추가 내용

 

해당 이슈는 다시 확인해보니 원래 chunk를 나누어 Lazy Load를 적용하고자 하는 것이 주목적이 맞았던 것으로 보인다.

 

다만 21년도부터 해당 이슈에 대한 이야기가 오가고있었고, 현재까지도 명확한 해결책이 나오지 않아 이러한 이슈를 마주한다면 manualChunks의 사용 자체를 고민해보길 바라는 이야기가 있어 남겨본다.

 

https://github.com/vitejs/vite/issues/5189

 

 

참조

 

https://ko.vitejs.dev/guide/features#preload-directives-generation

 

https://ko.vitejs.dev/guide/features#async-chunk-loading-optimization

 

https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/rel/modulepreload