Skip to content

코드 스플리팅

코드 스플리팅(Code-splitting)은 하나의 큰 자바스크립트 파일을 여러 개의 작은 파일로 나누고, 필요한 시점에 필요한 코드만 로드할 수 있도록 하는 최적화 기술이에요.

모든 페이지의 코드를 한꺼번에 로드하면 사용하지 않는 리소스까지 불러와 성능이 저하될 수 있어요. 하지만 코드 스플리팅을 적용하면 사용자가 방문한 페이지에 필요한 코드만 동적으로 로드할 수 있어서, 초기 로딩 속도가 빨라지고 불필요한 리소스 낭비도 줄일 수 있어요.

이제 코드 스플리팅을 활용하는 다양한 기법과 웹팩 설정 방법을 구체적으로 살펴볼게요.

동적으로 함수 로드하기

import()는 특정 모듈을 필요한 시점에 비동기로 불러오는 방식이에요. 이를 활용하면 초기 로딩을 최소화하고, 사용자가 요청한 기능만 동적으로 로드할 수 있어 성능을 최적화할 수 있어요.

  • 기존 방식: 모든 코드를 한 번에 로드

    js
    import { myFunction } from "./module.js";
    
    myFunction();
  • 코드 스플리팅 방식: 필요한 시점에 로드

    js
    async function loadModule() {
      const { myFunction } = await import("./module.js");
      myFunction();
    }

    import()는 Promise를 반환하기 때문에, await 키워드를 사용하여 비동기적으로 처리할 수 있어요.

동적으로 리액트 컴포넌트 로드하기

리액트에서는 React.lazySuspense를 활용해 컴포넌트를 동적으로 불러올 수 있어요.

jsx
import React, { Suspense } from "react";

const LazyComponent = React.lazy(() => import("./LazyComponent"));

function App() {
  return (
    <Suspense fallback={<div>Loading...</div>}>
      <LazyComponent />
    </Suspense>
  );
}

이 방식은 초기 렌더링 시 필요한 컴포넌트만 로드하고, 나머지 컴포넌트는 실제로 필요한 순간에 로드하게 만들어서 퍼포먼스를 개선할 수 있어요.

뿐만 아니라 라우팅을 설정할 때도 유용해요. 페이지 단위로 청크를 나눠 필요한 페이지를 비동기로 로드할 수 있어요.

jsx
import React, { Suspense } from "react";
import { BrowserRouter, Routes, Route } from "react-router-dom";

const Home = React.lazy(() => import("./pages/Home"));
const About = React.lazy(() => import("./pages/About"));

function App() {
  return (
    <BrowserRouter>
      <Suspense fallback={<div>Loading...</div>}>
        <Routes>
          <Route path="/" element={<Home />} />
          <Route path="/about" element={<About />} />
        </Routes>
      </Suspense>
    </BrowserRouter>
  );
}

빌드 도구별 코드 스플리팅 설정

코드 스플리팅은 애플리케이션의 초기 로딩 속도를 개선하고, 필요한 파일만 로드하여 성능을 최적화하는 중요한 기술이에요.

  • false: 청크 이름을 자동으로 생성하지 않고, Webpack이 내부적으로 관리해요. production 환경일 때 권장하는 값이에요.
javascript
module.exports = {
  optimization: {
    splitChunks: {
      chunks: "all",
      minSize: 20000,
      maxSize: 50000,
      minChunks: 1,
      maxAsyncRequests: 30,
      maxInitialRequests: 30,
      automaticNameDelimiter: "~", // 자동으로 생성된 청크 이름을 구분할 때 사용하는 문자열
      name: false, // ex) 1234.bundle.js
    },
  },
};
  • string: 청크 이름을 명시적으로 지정할 수 있어요. (ex vendor.js, chunk.js)
javascript
module.exports = {
  optimization: {
    splitChunks: {
      chunks: "all",
      minSize: 20000,
      maxSize: 50000,
      minChunks: 1,
      maxAsyncRequests: 30,
      maxInitialRequests: 30,
      automaticNameDelimiter: "~",
      name: 'chunk', // ex) chunk~abcd1234.bundle.js
    }
  }
};
  • function: 청크 이름을 동적으로 생성할 수 있어요. 그룹 이름, 청크 이름, 모듈명 등을 포함해서 가독성 있게 만들고 싶을 때 권장해요.
javascript
module.exports = {
  optimization: {
    splitChunks: {
      chunks: "all",
      minSize: 20000,
      maxSize: 50000,
      minChunks: 1,
      maxAsyncRequests: 30,
      maxInitialRequests: 30,
      automaticNameDelimiter: "~",
      name(module, chunks, cacheGroupKey) {
        const moduleFileName = module
          .identifier()
          .split("/")
          .reduceRight((item) => item);
        const allChunksNames = chunks.map((item) => item.name).join("~");
        return `${cacheGroupKey}-${allChunksNames}-${moduleFileName}`; 
        // ex) defaultVendors-main-formatters.js.bundle.js
      },
    },
  },
};

== Vite

기본적으로 Rollup을 내부적으로 사용하기 때문에 코드 스플리팅이 자동으로 적용돼요. 다만, 외부 라이브러리를 별도로 분리하거나 청크 전략을 직접 정의하고 싶다면 build.rollupOptions.output.manualChunks 옵션을 설정할 수 있어요.

변경 가능성이 적은 외부 라이브러리를 vendor.js로 분리하면 브라우저 캐시를 효율적으로 사용할 수 있어 초기 로딩 속도가 개선돼요.

javascript
import { defineConfig } from "vite";

export default defineConfig({
  build: {
    rollupOptions: {
      output: {
        manualChunks(id) {
          if (id.includes("node_modules")) {
            return "vendor";
          }
        },
      },
    },
  },
});

:::