guksulog
포스트 목록
#JavaScript#번들링

왜 lodash-es를 사용할까

2026년 1월 20일 4분 읽기

개발을 하다 보면 lodash는 자주 쓰는 라이브러리 중 하나예요. 그런데 프론트엔드 실무 팁 중에 자주 등장하는 게 lodash 대신 lodash-es를 쓰라는 얘기죠.

이유는 간단해요. lodash는 트리쉐이킹이 되지 않아서 번들 크기가 커지거든요. 그럼 왜 lodash는 트리쉐이킹이 안 될까요?

CommonJS

이유는 lodash가 CommonJS로 만들어진 라이브러리이기 때문이에요. CommonJS는 Node.js가 등장하고 모듈 시스템이 필요해지면서 같이 등장했어요.

javascript
// math.js
const add = (a, b) => a + b;
module.exports = { add };

// app.js
const { add } = require("./math");

이런 식으로 모듈을 만들고 다른 파일에서 쓸 수 있어요.

동적 사용

CommonJS의 핵심 특징 중 하나는 동적 사용이 가능하다는 점이에요. 보통 파일 최상단에서 require를 쓰지만, 코드 중간에서도 호출할 수 있어요.

javascript
if (condition) {
  const moduleA = require("./moduleA");
} else {
  const moduleB = require("./moduleB");
}

require는 런타임에 실행돼요. 즉, 어떤 모듈이 필요한지가 코드가 실제로 실행되기 전까지 결정되지 않는 거죠. 번들러 입장에선 빌드 시점에 "이 require가 어떤 모듈을 불러올지" 확신할 수 없다는 뜻이에요.

ES Modules

lodash-es는 ES Modules로 만들어졌어요. 아마 CommonJS보다 ES Modules이 친숙한 개발자가 더 많을 거예요.

javascript
// math.js
export const add = (a, b) => a + b;

// app.js
import { add } from "./math";

ES Modules의 import는 반드시 파일 최상단에 위치해야 하고, 조건문이나 함수 안에 넣을 수 없어요. 이 제약 덕분에 번들러는 코드를 실행하지 않고도 AST(추상 구문 트리)를 분석하는 것만으로 어떤 모듈이 어디서 쓰이는지 파악할 수 있죠. 이 정적 분석이 가능하니까 트리쉐이킹도 가능해지는 거예요.

트리쉐이킹

트리쉐이킹은 번들링 과정에서 불필요한 코드를 식별하고 제거하는 기법이에요. lodash-es를 예로 들면, lodash-es에 수많은 함수가 있어도 아래처럼 하나만 import하면 나머지 함수들은 번들에 안 들어가요.

javascript
// 트리쉐이킹이 되면 debounce만 번들에 포함된다

import { debounce } from "lodash-es";

왜 CommonJS는 트리쉐이킹이 안 될까

번들러가 트리쉐이킹을 하려면 빌드 타임에 어떤 코드가 사용되는지 정적으로 판단해야 해요. 그런데 CommonJS의 require는 런타임에 평가되기 때문에, 번들러가 빌드 시점에 "이 require가 실제로 호출될 것인가"를 알 수가 없어요. 결국 번들러는 동적으로 사용될 수 있다는 가정 아래 모든 파일을 번들에 포함하게 되죠. 이게 트리쉐이킹이 불가능해지고 번들 크기가 커지는 근본 이유예요.

반면 ESM의 import는 컴파일 타임에 파싱돼서 의존 관계가 정적으로 확정돼요. 번들러는 이 정보를 토대로 실제 사용되지 않는 코드를 안전하게 제거할 수 있고요.

그럼 lodash-es의 이점이 많은데 왜 아직도 lodash의 사용량이 많을까요? ES Modules은 CommonJS보다 늦게 등장했고, Node.js에서 ESM을 지원한 것도 비교적 최근이거든요. 그래서 레거시 코드에서는 lodash를 그대로 쓰는 경우가 많아요.

번들 크기가 성능에 직접적인 영향을 미치는 프론트엔드 환경에서는 lodash 대신 lodash-es를 선택하는 게 좋아요. 새로 시작하는 프로젝트라면 특히 그렇고요.


실제 번들 크기 차이

말로만 "번들이 커진다"고 하면 체감이 안 될 수 있어서, 실제 수치를 확인해봤어요.

방식gzip 크기비고
import _ from 'lodash'~24 kB전체 lodash 포함
import { debounce } from 'lodash'~24 kB트리쉐이킹 불가 (CJS)
import { debounce } from 'lodash-es'~2 kB트리쉐이킹으로 debounce만 포함
import debounce from 'lodash/debounce'~2 kB단일 함수 직접 import

debounce 하나 쓰자고 lodash 전체를 끌어오면 24kB가 번들에 들어가요. lodash-es로 바꾸면 2kB 남짓으로 줄어들어서 10배 이상 차이가 나죠.

lodash-es 외의 대안들

1. 단일 함수 직접 import (lodash/*)

lodash-es로 마이그레이션하기 어려운 레거시 환경이라면, 개별 패키지 경로로 가져오는 방법도 있어요.

javascript
// lodash 전체를 가져오는 대신
import { debounce } from 'lodash'; // ❌ 24kB

// 해당 함수만 직접 가져오기
import debounce from 'lodash/debounce'; // ✅ ~2kB

CommonJS지만 경로를 직접 지정하면 번들러가 해당 파일만 포함해요. Node.js 환경이나 레거시 CJS 코드베이스에서도 쓸 수 있고요.

2. 브라우저 내장 API 직접 활용

사실 lodash에서 자주 쓰는 기능 중 상당수는 이제 자바스크립트 자체에서 지원해요.

javascript
// lodash.cloneDeep → structuredClone (Node 17+, 모던 브라우저)
const copy = structuredClone(obj);

// _.flatten → Array.prototype.flat
const flat = arr.flat();

// _.uniq → Set
const unique = [...new Set(arr)];

// _.isEmpty
const isEmpty = (val) =>
  val == null || (typeof val === 'object' && Object.keys(val).length === 0);

라이브러리 없이 해결되면 번들 크기 걱정 자체가 없어져요. 단, 레거시 브라우저 지원이 필요한 프로젝트라면 폴리필 여부를 먼저 확인해야 해요.

마치며

"lodash-es를 써라"는 말을 처음 들었을 때는 "트리쉐이킹이 뭔지" 정도만 알고 넘어갔어요. 직접 번들 크기를 비교해보고 나서야 왜 이게 중요한지 실감했죠. 24kB와 2kB는 작은 차이처럼 보이지만, 첫 로딩에서 JavaScript 파싱 비용도 함께 올라가니까 실제 체감 성능 차이로 이어져요.

모듈 시스템의 차이(CJS vs ESM)가 왜 번들 최적화에 중요한지 이해하면, lodash 하나를 넘어 다른 라이브러리를 선택할 때도 같은 기준을 적용할 수 있어요. "이 라이브러리는 ESM을 지원하는가"가 의존성 선택의 중요한 기준이 되는 거죠.