원문 : https://madelinemiller.dev/blog/reduce-webapp-bundle-size/
웹팩과 같은 웹 번들러는 웹 앱을 배포하는 데 유용한 도구이지만, 더 복잡한 프로그램의 경우 번들 크기가 문제가 될 수 있습니다. 근본적인 문제와 솔루션은 사이트마다 다르지만 웹팩 및 다른 번들러가 가능한 작업을 잘 수행할 수 있도록 구현할 수 있는 몇 가지 변경 사항이 있습니다. 경우에 따라 동일한 문제가 다시 발생하지 않도록 방지하기 위한 린트(lint) 규칙 및 다른 방법들을 사용할 수도 있습니다. 이러한 변경 사항은 주로 리엑트, 타입스크립트 및 웹팩5를 염두에 두고 작성되었습니다. 그러나 문법과 이름은 다를 수 있지만, 일반적으로 모든 번들러를 거치는 모든 애플리케이션에 적용됩니다.
개선을 시작하기 전에 실제로 어떻게 동작하는지 이해하는 것이 중요합니다. 웹팩과 같은 웹 번들러는 코드와 코드에서 사용되는 다양한 종속성을 단일 파일로 번들하고 가능한 가장 적합하게 분할하려고 시도합니다. 이를 분할하는 프로세스를 번들 분할이라고 합니다. 웹팩은 또한 코드를 최소화하고 최적화하기 위해 터서(Terser)라는 도구를 사용합니다. 터서는 변수와 함수의 이름을 가능한 작게 변경하고 트리 쉐이킹으로 알려진 프로세스를 수행하여 응용프로그램에서 사용되지 않는 코드를 제거합니다.
트리 쉐이킹은 복잡한 프로세스이므로 기능이 손상되지 않도록 신중하게 수행되어야 합니다. 코드가 앱의 동작에 영향을 줄 조금의 가능성이라도 있는 경우 터서는 코드를 제거하지 않습니다. 파일을 가져오기만 하고 함수를 호출하지 않아도 실행되는 동작들이 있는 경우가 있습니다. 이것들을 사이드 이펙트라고 부릅니다. 사이드 이펙트 감지는 가져오기 코드를 무시해도 안전한지 판단하기 위해 번들러와 같은 도구가 수행해야 하는 또 다른 복잡한 프로세스입니다. 가져온 파일을 사용하지 않지만 터서가 사이드 이펙트가 있을 수 있다고 신중하게 판단하는 경우 트리 쉐이킹이 발생하지 않습니다. 자바스크립트와 같은 동적 언어의 복잡성으로 인해 사이드 이펙트 감지는 거짓 양성(실제로는 음성인데, 양성으로 판단하는 경우)을 만들 수도 있습니다.
또한 ESModule(또는 줄여서 ESM)과 같은 새로운 표준과 달리 CommonJS와 같은 오래된 모듈 표준은 트리 쉐이킹이 더 적게 일어납니다. 대부분의 번들러는 이러한 모듈 형식을 트리 쉐이킹하려고 시도하지 않거나, 시도할 경우 훨씬 더 신중하게 판단합니다.
프로젝트 최적화를 수행할 때는 먼저 몇 가지 지표을 얻는 것이 좋습니다. 이 경우 번들의 크기와 번들에 포함된 내용을 이해해야 합니다. 일반적으로 번들의 진입점이 되는 부분은 가장 검토할 가치가 있습니다. 응용 프로그램의 구조에 따라 main
으로 시작하는 이름, 페이지 경로 이름 또는 템플릿 파일 이름이 있을 수 있습니다.
만약 웹팩을 사용하는 경우에는 Webpack Bundle Analyzer 도구가 매우 유용할 수 있습니다. 이 플러그인 설정 및 활성화된 상태에서 웹팩 빌드를 실행하면 출력 디렉터리에 각 번들과 번들에 포함된 다양한 파일 및 종속성을 시각화하여 확인할 수 있는 HTML 파일이 생성됩니다.
Bundlewatch와 같은 CI 파이프라인에 통합되는 툴도 있지만, 이러한 툴은 현재 문제에 대한 통찰력을 제공하는 것 보다는 번들 크기의 추가 증가를 방지하는 데 더 유용합니다.
번들 크기 증가의 가장 일반적인 원인 중 하나는 트리쉐이킹할 수 없는 CommonJS와 같은 모듈들입니다. 번들 분석을 통해 발견된 큰 크기의 종속성이 ESM 변형 패키지를 제공하는지 조사하는 것이 좋습니다. 이를 확인하는 빠른 방법은 node_modules
의 종속성 package.json
파일 내에 module
항목이 있는지 확인하는 것입니다.
ESM 형식으로 제공되지 않는 모듈을 확인했으면 해당 모듈을 제공하는 업데이트가 있는지 확인하는 것이 좋습니다. NPM 페이지 또는 Github를 확인해보면 좋습니다. 종속성이 NPM 패키지 이름을 변경하는 경우가 있습니다. 예를 들어, react-query
는 현재 ESM 릴리스를 포함하여 @tanstack/react-query
로 변경되었습니다. 만약 사용 가능한 새 업데이트가 없는 경우 Github 이슈를 확인하면 이와 관련된 시간 범위 또는 토론이 있을 수 있습니다.
ESM 업데이트를 사용할 수 없는 경우에는 몇 가지 옵션이 있습니다. 대부분의 경우, 잠재적인 번들 크기 절감이 새로운 종속성으로 전환하는데 드는 개발 비용의 가치가 있는지 아니면 ESM 지원에 직접 기여할 가치가 있는지 결정하는 것이 중요합니다. ESM 형식으로 패키지를 재컴파일을 제공하는 esm-bundle과 같은 일부 프로젝트가 있지만 빈번하게 종속성으로 사용되는 모듈에 대해 이러한 프로젝트로 구성하는 것은 문제가 될 수 있습니다.
번들러와 함께 타입스크립트를 사용하는 경우 많은 최적화를 비활성화 시키는 쉬운 실수는 TSConfig 파일의 "module"
속성을 CommonJS
으로 설정하는 것입니다. 이것은 번들러의 실제 출력을 설정하는 것이 아니라 컴파일된 타입스크립트를 받는 중간 형식을 변경합니다. 대부분의 경우 이것은 "es2020"
과 같은 ESM 형식으로 쉽게 변경할 수 있어야 합니다. Jest를 사용하는 경우에도 CommonJS 출력이 필요할 수 있습니다. 이 경우 첫 번째 파일(TSConfig)을 확장하지만 모듈을 다시 CommonJS로 설정하는 두 번째 tsconfig.spec.json
파일을 설정할 수 있습니다. 이를 통해 번들러는 ESM을 받을 수 있으며, Jest는 변경된 TSConfig를 사용하여 CommonJS를 유지하도록 설정할 수 있습니다.
웹팩과 같은 일부 번들러에서는 사이드 이펙트 감지 프로세스에 대한 힌트를 제공할 수 있습니다. 코드에 사이드 이펙트가 없는 경우 package.json
에 "sideEffects"
속성 false
로 설정할 수 있습니다. 이것은 라이브러리에 사이드 이펙트가 없기 때문에 훨씬 더 깊이 있는 트리 쉐이킹이 가능하다는 것을 번들러에게 알려줍니다. 리액트와 같은 라이브러리를 사용할 때 고차 컴포넌트와 같은 다양한 리액트 패턴이 사이드 이펙트 감지를 잘못 트리거 할 수 있기 때문에 이 효과는 훨씬 더 두드러질 수 있습니다.
사이드 이펙트가 있지만, 어디에 있는지 정확하게 알고 있다면 "sideEffects"
속성에 사이드 이펙트가 있는 패턴의 목록을 전달할 수 있습니다. 구체적인 구문은 웹팩 설명서에 나와 있습니다. 이를 통해 CSS 가져오기와 같은 상황에서 사이드 이펙트를 유지하면서 패키지를 대부분 사이드 이펙트가 없는 것으로 표시할 수 있습니다.
더 복잡한 경우에는 웹팩 규칙을 통해 sideEffects
속성을 설정할 수도 있습니다. 이 기능은 보다 동적인 표시를 수행하거나 종속성에서 파일을 표시하는데 유용할 수 있습니다. 다음은 모든 index.ts
파일은 사이드 이펙트가 없는 것을 표시하는 예시입니다.
rules: [
{
test: /\/index.ts$/,
sideEffects: false,
},
];
타입스크립트를 사용하는 경우 파일에서 타입을 가져오기 위해 파일을 가져오는 경우가 많습니다. 경우에 따라 출력 코드에 영향을 미치지 않는 타입에도 불구하고 실제로 해당 파일이 트리 쉐이킹 되지 않을 수 있습니다. 웹팩이 파일을 가져오고 사이드 이펙트가 있다고 판단할 경우, 유일하게 가져오는 것이 타입이더라도 "used exports" 최적화를 사용하지 않도록 설정하고 파일을 유지합니다.
이 문제를 해결하는 한 가지 방법은 응용 프로그램에서 타입 가져오기 및 내보내기에 type
한정자를 사용하는 것입니다. 타입스크립트 문서에서 다루며, 기본적으로 가져오기와 내보내기 키워드 뒤에 배치하여 명명된 모든 가져오기/내보내기가 타입이며 컴파일러에 의해 지워질 수 있음을 명시하는 수식어입니다. 이러한 모든 명령문이 삭제되면 번들러는 import
또는 export
를 볼 수 없으므로 파일에 사이드 이펙트가 있는지 확인하지 않습니다. 최신 타입스크립트 버전(4.5+)에서는 전체 문이 아닌 개별 가져오기에서도 타입을 표시할 수 있습니다.
예시입니다.
// TS 3.8+ syntax
import type { A, B, C } from "./letters";
// TS 4.5+ syntax
import { type D, E } from "./moreLetters";
// TS 3.8+ syntax
export type { F, G } from "./letters";
// TS 4.5+ syntax
export { type H, I } from "./moreLetters";
@typescript-eslint/eslint-plugin
패키지는 코드에서 자동으로 적용할 수 있는 두 가지 ESLint 규칙을 auto-fixer와 함께 제공합니다. consistent-type-imports
는 가져오기 문의 타입 한정자를 처리하고, consistent-type-exports
는 내보내기 문의 타입 한정자를 처리합니다. 이는 이러한 문제가 시간이 지남에 따라 코드로 다시 유입되는 것을 방지하고 신규 엔지니어가 프로세스에 투입되는데 도움이 됩니다.
배럴(Barrel) 파일은 자바스크립트 앱에서 일반적인 패턴이지만 예상치 못한 실수로 파일을 번들로 가져오기 쉽습니다. 배럴 파일은 다른 파일에서 내보내기를 다시 내보내는 파일입니다. 일반적으로 다음과 같은 와일드카드 내보내기를 사용합니다.
export * from "./someFile";
export * from "./someOtherFile";
이렇게 하면 가져오기를 깔끔하게 유지하는 데 편리할 수 있지만, 다시 내보내는 파일 중 하나에 사이드 이펙트가 있을 경우 번들로 제공됩니다. 이 문제는 사용중인 가져오기가 다시 내보낸 파일에도 없는 경우에도 발생합니다.
이 문제를 해결하는 한 가지 방법은 모든 배럴 파일에 index.js
또는 index.ts
와 같이 쉽게 식별할 수 있는 이름을 지정하는 것입니다. 그런 다음 이 글의 앞부분과 같은 웹팩 규칙을 사용하여 이러한 규칙에 대한 사이드 이펙트 탐지를 비활성화 할 수 있습니다. 타입스크립트 사용자의 경우 타입 한정자를 내보내기에 적용할 수 있으므로 와일드카드 내보내기 대신 명명된 내보내기를 사용하는 방법도 있습니다. 다음과 같은 방법입니다.
export type { A, B, C } from "./someFile";
export { type D, E, F } from "./someOtherFile";
또한 이런한 방식은 배럴의 와일드카드 내보내기를 사용하면 실수로 중복 내보내기가 발생할 수 있는 것에 비해 정확성이 약간 향상됩니다. 명명된 내보내기를 명시적으로 내보내면 내부 파일 간에 사용되는 내보내기 대신 다른 패키지에 노출할 항목만 내보내므로 캡슐화가 개선됩니다.
지연 로딩(Lazy loading)은 이 페이지에서 가장 효과적인 최적화가 될 가능성이 있지만, 제가 마지막으로 둔 이유가 있습니다. 지연 로딩은 훌륭한 도구이지만, 이 문서 전반에 걸쳐 언급된 다른 문제들로 가득 찬 코드에 의해 억제될 것입니다. 지연 로딩을 최대한 활용하려면 코드를 번들러에서 쉽게 분석하고 트리 쉐이킹 할 수 있어야 합니다.
또한 지연 로딩은 잘 생각하지 않으면 오히려 더 많은 문제를 일으킬 수 있습니다. 프로세스가 시작될 때 사용자가 기다리고 있는 파일을 구문 분석하기 위해 코드를 로드하면 사전에 로드하는 것보다 사용자 환경이 나빠질 수 있습니다. 사용자 경험을 저해하지 않고 번들을 적절하게 분할하는 방식으로 앱을 분할할 수 있도록, 지연 로드가 일어날 위치를 생각하는 것이 중요합니다. 앞서 말한 단계를 수행하기 전에 지연 로딩을 설정한 경우, 이러한 부분이 지연 로딩에 적합한지 확인하는 것도 중요할 수 있습니다. 웹팩이 번들을 분할하도록 하기 위해 엔지니어가 코드의 많은 작은 세션들을 지연 로드하는 것이 일반적입니다. 위의 문제가 해결된 후에는 많은 수의 매우 작은 번들을 생성하게 됩니다.
동적 가져오기 문 import('./Module').then(module => {});
을 사용하여 자바스크립트 코드를 지연 로드할 수 있습니다. 이것은 프런트엔드 응용 프로그램의 라우터 수준에서 가장 잘 사용할 수 있습니다. 이 문은 프로미스 구문이므로 await을 사용하여 응답을 기다렸다가 일반적으로 모듈을 가져오는 것처럼 사용하거나 .then()
의 콜백을 이용하여 가져온 모듈에 접근하여 사용할 수 있습니다.
리액트 앱에서는 React.lazy
호출과 Suspense
컴포넌트를 함께 사용하여 컴포넌트의 지연 로딩을 쉽게 설정할 수 있습니다. 동적 가져오기 호출은 React.lazy
로 묶은 다음 Suspense
컴포넌트 내에서 일반 컴포넌트로 사용할 수 있습니다. 예를 들면 다음과 같습니다.
const LazyViewer = React.lazy(() => import("./Viewer"));
const App = () => (
<Suspense fallback={<Spinner />}>
<LazyViewer />
</Suspense>
);
이러한 기능은 사용자 입력이 필요한 상황에서만 이상적으로 사용해야 하며, 그렇지 않으면 응용프로그램 전체의 많은 로드 상태로 인해 사용자 경험이 저하될 수 있습니다. 좋은 예시 중 하나는 모달 또는 대화 상자 내부입니다. 기본 컴포넌트는 초기에 로드되지만 React.lazy
컴포넌트는 모달/대화 상자가 열 때까지 렌더링 되지 않습니다. React.lazy
에 대한 자세한 내용은 React Docs에서 확인할 수 있습니다.
웹팩을 사용하면 상위 번들이 로드된 후 지연 로드된 번들을 미리 가져올 수 있는 힌트를 제공할 수 있습니다. 이 작업은 동적 가져오기 문의 경로 앞에 /* webpackPrefetch: true */
를 추가하여 수행할 수 있습니다. 상위 번들이 로드된 이후가 아닌 함께 로드하기 위해 /* webpackPreload: true */
를 추가할 수 있습니다. 그러나 이 작업은 브라우저가 유휴 상태가 아닌 중간 우선 순위로 다운로드 되므로 과도하게 사용할 경우 프리페치 대신 프리로드을 사용하는 것이 좋지 않을 수 있습니다. 이것은 페이지 로드에 필요한 다른 자산 또는 번들보다 우선처리 되기 때문입니다.
프리페치와 프리로드를 모두 사용하는 가져오기의 예시는 import(/* webpackPrefetch: true */ /* webpackPreload: true */ './Module').then(module => {});
입니다. 이러한 힌트에 대한 자세한 내용은 웹팩 설명서에서 확인할 수 있습니다.
웹 앱의 번들 크기를 개선하기 위해 할 수 있는 일은 매우 많습니다. 이 글은 여러분에게 한번에 많은 것을 던져주는 것처럼 보일 수 있지만, 제 목표는 여러분이 번들러와 옵티마이저가 실제로 무엇을 하는지, 일반적인 함정이 어떻게 발생하는지 더 잘 이해할 수 있도록 도움을 주는 것입니다. 이러한 제안은 대부분 사이트에서 강력하게 도움이 될 것이지만, 이 글에서 트리 쉐이킹이 실패할 수 있는 방법에 대한 학습은 프로젝트에서 추가적인 개선점을 찾는 데 도움이 될 것입니다.
저는 이 사이트, 몇 개의 다른 개인 프로젝트 사이트 및 작업 중인 프로젝트를 크게 최적화하기 위해 이러한 방법을 성공적으로 사용했습니다. 제가 자바스크립트 생태계에서 새로운 것을 발견하거나 변화가 생긴다면, 이 글을 업데이트 하겠습니다.
잘 읽었습니다!