[Next.js] 패키지 최적화

최선 버전의 Next.js 에서는 패키지 가져오기를 최적화 했습니다.

 

여러 라이브러리를 사용하거나 패키지를 이용하는 경우 불필요한 함수와 컴포넌트까지 불러와져서

번들 파일이 커지는 현상이 발생하는 경우가 생기기 시작했습니다. 따라서 Next.js 는 이러한 문제점을 인식하고 개선하였습니다.

 

이 글에서 이러한 변경이 필요했던 이유와 현재 해결 방법을 찾기까지의 과정, 그리고 개선된 성능에 대해 설명하겠습니다. 

Barrel file 이란?

자바스크립트에서 배럴 파일은 단일 파일에서 여러 모듈을 그룹화하여 내보내는 방법입니다. 

그룹화된 모듈에 접근할 수 있는 중앙화된 위치를 제공함으로써 그룹화된 모듈을 더 쉽게 가져올 수 있습니다.

 

예를 들어, components/index.ts 내에 2개의 모듈 Button.tsx, Input.tsx 가 있다고 가정해 봅시다.

 

index.ts 파일에는 다음과 같이 컴포넌트를 내보낼 수 있습니다.

// components/index.ts
export { default as Button } from './Button';
export { default as Input } from './Input';

 

이제 다른 파일에서 이렇게 임포트 할 수 있습니다.

import { Button, Input } from './components'; // 더 간단하게 가져올 수 있음

 

이렇게 배럴 파일을 사용하면, 내부 구조에 대해 알 필요 없이 모든 모듈을 일괄적으로 가져올 수 있습니다.

배럴 파일은 관련 모듈에 쉽게 접근할 수 있는 인터페이스를 제공하여 코드 구성과 유지보수성을 향상시킬 수 있습니다.

이 때문에 자바스크립트 패키지, 특히 아이콘 및 컴포넌트 라이브러리에서 널리 사용되는 이유이기도 합니다.

일부 인기 있는 아이콘 및 컴포넌트 라이브러리에는 배럴 파일에 최대 10,000 개의 내보내기가 있습니다.

 

또한 컴포넌트 뿐 만 아니라 함수도 내보낼 수 있습니다.

// 배럴 파일 (index.js)
export * from './module1';
export * from './module2';
export * from './module3';

// 다른 파일에서 사용
import { func1, func2 } from './path/to/barrel'; // 한 번의 임포트로 모든 모듈을 가져옴

 

하지만 베럴 파일의 문제점??

이렇게 모든 함수나 컴포넌트를 불어오는 것은 엄청난 비용을 야기합니다.

수천 개의 다른 항목들을 가져오는 배럴 파일에서 단일 내보내기를 사용하려는 경우, 불필요한 다른 모듈을 가져오는 대가를 지불하고 있는 것입니다.

 

그러면 이러한 문제점을 어떻게 해결해야 할까요??

트리 쉐이킹(Tree Shaking) 을 이용하면 되지 않나요?

첫번 째 방법으로는 트리 쉐이킹이라는 방법이 있습니다. 

트리 쉐이킹(Tree Shaking)은 코드에서 사용되지 않는 불필요한 부분을 제거하는 기술입니다.

이는 특히 JavaScript 번들링 도구에서 사용되며, 코드의 크기를 줄이고 성능을 개선하는 데 도움을 줍니다.

트리 쉐이킹을 자동으로 도와주는 유용한 번들링 도구들이 있는데 가장 많이 사용되는 도구는 Webpack 과 Rollup 입니다.
즉 따로 설정하지 않아도 번들링 도구들이 트리 쉐이킹을 자동으로 적용해 줍니다.

 

만약 여러분이 JavaScript 라이브러리에서 여러 함수를 가져왔는데, 실제로 사용하는 것은 그 중 하나라고 가정해 봅시다.

import { funcA, funcB, funcC } from 'my-library'; 

funcA();

 

이 코드에서 funcB와 funcC는 사용되지 않으므로 필요 없습니다.

트리 쉐이킹을 통해 funcB와 funcC를 자동으로 제거하여 번들 파일에서 불필요한 코드를 없앨 수 있습니다.

마치 나무(tree)의 가지(branch)를 흔들어(shake) 떨어뜨리는 것처럼 사용되지 않는 코드를 제거하는 것이죠.

 

하지만 트리 쉐이킹은 next.js 에서 패키지를 최적화 하는데 사용할 수 없습니다.

기본적으로 트리 쉐이킹은 ES6 모듈(ESM)을 사용하여 "사용되지 않은 코드" 를 제거하는 최적화 기법인데, 많은 패키지들은 여전히 CommonJS 방식(require, module.exports)을 사용합니다.

이 외에도 다양한 문제가 있습니다. 패키지 구조가 트리 쉐이킹을 방해하거나 사이드 이펙트가 있는 패키지, 동적 임포트로 인한 트리 쉐이킹의 방해 등이 있습니다. 

 

Next.js는 기본적으로 트리 쉐이킹을 지원하지만, 사용된 패키지나 코드 스타일에 따라 트리 쉐이킹이 불가능할 수도 있습니다.

그러면 Next.js 에서 어떤 방법을 사용해야 할까요?

Next.js에서 modularizeImports 기능의 도입

Next.js 에서 처음으로 도입한 트리 쉐이킹 방법은 modularizeImports 방법이였습니다.

modularizeImports는 Next.js에서 모듈을 효율적으로 임포트하고 번들 크기를 줄이는 기능입니다.

이 기능은 트리 쉐이킹과 비슷한 목적을 가지고 있으며, 각 모듈이나 라이브러리의 특정 부분만 임포트하게 도와줍니다.

이를 통해 불필요한 코드가 포함되지 않도록 최적화하는 것이죠.

 

만약 여러분이 lodash라는 유명한 라이브러리를 사용한다고 가정해봅시다.

lodash는 굉장히 많은 유틸리티 함수들을 포함하고 있는데, 여러분이 그 중 한 두 가지만 사용한다면 모든 함수들을 한꺼번에 가져올 필요가 없습니다.

// 이 코드는 lodash의 전체 내용을 가져옴 (불필요한 코드가 많아질 수 있음) 
import _ from 'lodash';

 

하지만 modularizeImports를 사용하면 이렇게 특정 부분만 가져올 수 있습니다.

// 필요한 함수들만 임포트 
import cloneDeep from 'lodash/cloneDeep'; 
import isEmpty from 'lodash/isEmpty';

 

이렇게 하면 불필요한 코드를 제외하고 필요한 함수만 번들에 포함됩니다.

어떻게 설정하나요?

Next.js에서 modularizeImports를 설정하려면 next.config.js 파일을 수정해야 합니다.

module.exports = { 
  modularizeImports: { 
    'lodash': { 
      transform: 'lodash/{{member}}', // 특정 함수만 임포트 
    }, 
    'date-fns': { 
      transform: 'date-fns/{{member}}', 
    }, 
  }, 
};
 

이 설정을 통해 lodash나 date-fns 라이브러리에서 특정 함수만 임포트하게 하여 코드의 크기를 줄이고 성능을 최적화할 수 있습니다.

하지만 이러한 방법은 오래 가지 못했습니다.
내가 사용하는 라이브러리의 내부 디렉토리 구조를 기반으로 하며, 대부분 수작업으로 구성되기 때문이였습니다.

 

Next.js의 새로운 솔루션 : optimizePackageImports

Next.js 13.5 version 부터는 이러한 어려움을 해결하기 위해서 이 작업을 자동으로 수행하는 새로운 optimizePackageImports 옵션을 도입했습니다.

 

사용법도 아주 간단하게 바뀌었습니다.

아래의 next.config.js 파일에서 간단하게 설정만 해주시면 됩니다.

// next.config.js

module.exports = {
  experimental: {
    optimizePackageImports: ["lodash"],
  },
};

 

이렇게 설정을 해주시고 아래와 같은 코드처럼 사용하시면 됩니다.

import _ from "lodash";

const App = () => {
  const changeEvent = _.throttle(() => {}, 500)
  
  ...
}

 

optimizePackageImports 를 사용하면 import _ from "lodash" 처럼 라이브러리 전체를 임포트하는 방식이라도,

실제로 사용하는 부분만 번들에 포함시킬 수 있습니다. 위에 코드처럼 _.throttle 만 사용한다면, optimizePackageImports 가 lodash 의 다른 불필요한 부분들을 자동으로 제거하고, throttle 함수만 번들에 포함시켜 줍니다.

개발 환경에서는 번들 파일을 확인해도 lodash 의 모든 부분이 번들 파일에 포함되어 있는 모습을 보실 수 있습니다.
하지만 배포 환경에서는 throttle 함수만 사용하는 경우 해당 throttle 함수에 필요한 번들 파일만 포함하여 배포하게 됩니다.

 

공부를 하면서 프로젝트에 적용해본 결과

@material-ui/icons 같은 경우 10.1초 (11738개의 모듈 포함) 에서 2.8초 (632개의 모듈 포함) 으로 단축하였습니다.