아키텍처 개요
관련 소스 파일
이 페이지의 내용은 다음 소스 파일을 기반으로 생성되었습니다:
- src/index.ts
- src/compat/index.ts
- src/compat/compat.ts
- tsconfig.json
- package.json
- src/compat/array/map.spec.ts
- src/compat/array/uniq.spec.ts
- src/compat/array/tail.spec.ts
- src/compat/array/last.spec.ts
- src/compat/array/head.spec.ts
- src/map/index.ts
- src/set/index.ts
- src/math/index.ts
- src/util/index.ts
- src/array/index.ts
- src/error/index.ts
- src/object/index.ts
- src/string/index.ts
- src/promise/index.ts
- src/function/index.ts
es-toolkit은 현대 JavaScript/TypeScript 생태계를 위한 고성능 유틸리티 라이브러리로, Lodash와의 호환성을 제공하면서도 더 작은 번들 크기와 더 나은 성능을 목표로 설계되었습니다. 이 문서에서는 es-toolkit의 아키텍처 구조, 핵심 모듈 설계, 데이터 흐름, 그리고 기술적 의사결정 과정을 상세히 분석합니다.
모듈 구조 및 진입점
es-toolkit의 아키텍처는 명확한 관심사의 분리(Separation of Concerns) 원칙을 따르며, 기능별로 독립적인 모듈로 구성되어 있습니다. 이러한 구조는 트리 쉐이킹(Tree Shaking)을 최적화하고, 사용자가 필요한 기능만 선택적으로 가져올 수 있게 합니다.
메인 진입점 구조
프로젝트의 메인 진입점인 src/index.ts:43-65는 9개의 핵심 기능 모듈을 re-export하는 구조로 되어 있습니다:
typescript1export * from './array/index.ts'; 2export * from './error/index.ts'; 3export * from './function/index.ts'; 4export * from './math/index.ts'; 5export * from './object/index.ts'; 6export * from './predicate/index.ts'; 7export * from './promise/index.ts'; 8export * from './string/index.ts'; 9export * from './util/index.ts';
각 모듈은 독립적인 네임스페이스를 형성하며, 다음과 같은 기능을 담당합니다:
| 모듈 | 담당 기능 | 주요 함수 예시 |
|---|---|---|
| array | 배열 조작 및 변환 | chunk, uniq, map, head, tail, last |
| function | 함수 유틸리티 | debounce, throttle, memoize |
| math | 수학 연산 | sum, mean, clamp |
| object | 객체 조작 | pick, omit, merge |
| predicate | 타입 가드 및 조건 | isNil, isEmpty, isPlainObject |
| promise | 비동기 처리 | debounce, throttle (async) |
| string | 문자열 처리 | trim, capitalize |
| error | 에러 처리 | 커스텀 에러 클래스 |
| util | 범용 유틸리티 | identity, noop |
TypeScript 컴파일러 설정
tsconfig.json:1-16은 프로젝트의 기술적 기반을 정의하며, 현대적인 TypeScript 개발 환경을 반영합니다:
json1{ 2 "compilerOptions": { 3 "lib": ["ESNext", "DOM"], 4 "target": "ESNext", 5 "module": "ESNext", 6 "moduleResolution": "Bundler", 7 "strict": true, 8 "allowImportingTsExtensions": true 9 } 10}
주요 설정의 의미:
- target: ESNext: 최신 JavaScript 기능을 활용하여 성능 최적화
- moduleResolution: Bundler: 모던 번들러(Vite, webpack 5+)에 최적화된 모듈 해석
- strict: true: 타입 안전성 보장으로 런타임 에러 사전 방지
- allowImportingTsExtensions:
.ts확장자 명시적 import 허용으로 명확한 모듈 경로 관리
Lodash 호환성 계층 (Compat Layer)
es-toolkit의 가장 독특한 아키텍처 특징은 이중 계층 구조입니다. 표준 API와 별도로 es-toolkit/compat 경로를 통해 Lodash와의 완전한 호환성을 제공합니다.
호환성 계층의 목적과 설계 철학
src/compat/index.ts:11-27에 명시된 바와 같이, compat 계층의 핵심 목표는 다음과 같습니다:
- Drop-in Replacement: 기존 Lodash 사용 코드를 수정 없이 교체 가능
- 100% 동작 일치: 실제 Lodash 테스트 케이스로 검증된 동등성
- 안전성 우선: 위험한 암시적 타입 변환은 의도적으로 제외
typescript1// compat 계층이 의도적으로 제외하는 안전하지 않은 기능 예시 2// - 빈 문자열 ''을 0이나 false로 암시적 변환 3// - 유사한 위험한 타입 강제 변환
호환성 검증 전략
src/compat/array/uniq.spec.ts:1-43은 Lodash 호환성 검증의 구체적인 사례를 보여줍니다:
typescript1describe('uniq', () => { 2 it('should perform an unsorted uniq when used as an iteratee', () => { 3 const array = [[2, 1, 2], [1, 2, 1]]; 4 const actual = array.map(uniq); 5 expect(actual).toEqual([[2, 1], [1, 2]]); 6 }); 7 8 it('should return an empty array when the collection is null', () => { 9 expect(uniq(null)).toEqual([]); 10 }); 11 12 it('should support array-like', () => { 13 expect(uniq({ 0: 1, 1: 2, 2: 1, length: 3 })).toEqual([1, 2]); 14 expect(uniq('112')).toEqual(['1', '2']); 15 }); 16 17 it('should match the type of lodash', () => { 18 expectTypeOf(uniq).toEqualTypeOf<typeof uniqLodash>(); 19 }); 20});
이 테스트는 다음을 검증합니다:
- 기능적 동등성: Lodash와 동일한 출력 생성
- 엣지 케이스 처리: null, undefined, 배열 유사 객체 지원
- 타입 호환성:
expectTypeOf를 통한 TypeScript 타입 일치 검증
시스템 아키텍처 다이어그램
es-toolkit의 전체 아키텍처는 다음 다이어그램으로 시각화할 수 있습니다:
正在加载图表渲染器...
아키텍처 핵심 포인트
-
이중 진입점 구조: src/index.ts:43-65의 Core 진입점과 src/compat/index.ts:11-27의 Compat 진입점이 분리되어 있어, 사용자는 필요에 따라 선택 가능
-
계층 간 의존성: Compat Layer는 Core Layer의 구현을 래핑하여 재사용하므로 코드 중복 최소화
-
테스트 기반 검증: Lodash의 실제 테스트 케이스를 사용하여 호환성을 보장하는 CI/CD 파이프라인
-
모듈 독립성: 각 기능 모듈(array, function, math 등)은 서로 독립적이어서 트리 쉐이킹 효율 극대화
핵심 모듈 상세 분석
모듈 1: Array 모듈
职责边界 (Responsibility Boundary):
- 배열 생성, 변환, 필터링, 검색 기능 담당
- 객체 순회나 문자열 처리는 각각 object, string 모듈에서 담당
- 순수 함수만 제공하며 원본 배열 변형 없음
진입점 및 주요 API:
- 진입점:
src/array/index.ts - 주요 함수:
chunk,uniq,head,tail,last,map
호환성 계층에서의 구현:
src/compat/array/head.spec.ts:1-51은 head(또는 first) 함수의 동작을 보여줍니다:
typescript1describe('head', () => { 2 it('should return the first element', () => { 3 expect(head([1, 2, 3, 4])).toBe(1); 4 }); 5 6 it('should work as an iteratee for methods like `map`', () => { 7 const array = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]; 8 const actual = array.map(head); 9 expect(actual).toEqual([1, 4, 7]); 10 }); 11 12 it('should be aliased', () => { 13 expect(first).toBe(head); // first와 head는 동일 함수 14 }); 15 16 it('should support array-like', () => { 17 expect(first({ 0: 1, 1: null, 2: 3, length: 3 })).toEqual(1); 18 expect(first('123')).toEqual('1'); 19 }); 20});
키 데이터 구조:
- 입력:
Array<T>또는 배열 유사 객체({ length: number, [key: number]: T }) - 출력:
T | undefined(head/last) 또는T[](tail/uniq)
에러 처리 및 경계 조건:
null/undefined입력 →undefined반환 (head/last) 또는[]반환 (tail)- 배열 유사 객체 지원:
{ 0: 'a', 1: 'b', length: 2 }→['a', 'b']로 처리 - 빈 배열 →
undefined또는[]
모듈 2: Compat Array 모듈
职责边界:
- Lodash API와 100% 호환되는 배열 함수 제공
- Core Array 모듈의 기능을 확장하여 Lodash 특유의 동작 구현
- 안전하지 않은 기능(위험한 타입 변환)은 의도적으로 제외
진입점 및 주요 API:
- 진입점:
src/compat/array/디렉토리 - 주요 함수:
map,uniq,head,tail,last
복잡한 호출 체인:
src/compat/array/map.spec.ts:29-189는 map 함수의 정교한 동작을 보여줍니다:
typescript1describe('map', () => { 2 it('should use `_.identity` when `iteratee` is nullish', () => { 3 const object = { a: 1, b: 2 }; 4 const values = [, null, undefined]; 5 const expected = values.map(constant([1, 2])); 6 7 each([array, object], collection => { 8 const actual = values.map((value, index) => 9 (index ? map(collection, value) : map(collection)) 10 ); 11 expect(actual).toEqual(expected); 12 }); 13 }); 14 15 it('should accept a falsey `collection`', () => { 16 const expected = falsey.map(stubArray); 17 const actual = falsey.map((collection, index) => { 18 return index ? map(collection as any) : map(); 19 }); 20 expect(actual).toEqual(expected); 21 }); 22 23 it('should use `isArrayLike` to determine array-like', () => { 24 const isIteratedAsObject = function (object: any) { 25 let result = false; 26 (func as any)(object, () => { result = true; }, 0); 27 return result; 28 }; 29 30 const values = [-1, '1', 1.1, Object(1), MAX_SAFE_INTEGER + 1]; 31 const expected = map(values, () => true); 32 const actual = map(values, length => isIteratedAsObject({ length })); 33 34 expect(actual).toEqual(expected); 35 }); 36});
키 데이터 구조:
- 입력:
Array<T> | Object | null | undefined | 배열 유사 객체 - iteratee:
function | object | string | null | undefined - 출력:
Array<U>(변환된 값들의 배열)
에러 처리 및 경계 조건:
null/undefinedcollection →[]반환- 숫자 collection →
[]반환 (배열이 아닌 것으로 간주) iteratee가 nullish → identity 함수 사용- 프로토타입 속성 무화:
Foo.prototype.b = 2는 순회에서 제외
모듈 3: Function 모듈
职责边界:
- 함수 조합, 커링, 메모이제이션 등 함수형 프로그래밍 유틸리티 제공
- debounce, throttle 등 실행 제어 함수 포함
- 비동기 함수 처리는 promise 모듈과 협력
진입점 및 주요 API:
- 진입점:
src/function/index.ts - 주요 함수:
debounce,throttle,memoize,once,partial
키 데이터 구조:
typescript1// debounce 함수의 개념적 타입 2function debounce<T extends (...args: any[]) => any>( 3 func: T, 4 wait?: number, 5 options?: { leading?: boolean; trailing?: boolean } 6): DebouncedFunction<T>; 7 8interface DebouncedFunction<T> { 9 (...args: Parameters<T>): void; 10 cancel(): void; 11 flush(): void; 12 pending(): boolean; 13}
에러 처리 및 경계 조건:
wait이 음수 → 0으로 처리func이 함수가 아님 → TypeError 발생- 취소된 debounce 호출 → 즉시 실행 중단
모듈 4: Predicate 모듈
职责边界:
- TypeScript 타입 가드 함수 제공
- 런타임 타입 검사 유틸리티
- Lodash의
is*함수들과 호환
진입점 및 주요 API:
- 진입점:
src/predicate/index.ts - 주요 함수:
isNil,isEmpty,isPlainObject,isArray,isObject
키 데이터 구조:
typescript1// 타입 가드 함수 예시 2function isNil(value: unknown): value is null | undefined; 3function isEmpty(value: unknown): boolean; 4function isPlainObject(value: unknown): value is object;
호환성 계층에서의 활용:
src/compat/array/map.spec.ts:164-188에서 isArrayLike predicate의 사용 예를 확인할 수 있습니다:
typescript1it(`should use \`isArrayLike\` to determine array-like`, () => { 2 const isIteratedAsObject = function (object: any) { 3 let result = false; 4 (func as any)(object, () => { result = true; }, 0); 5 return result; 6 }; 7 8 const values = [-1, '1', 1.1, Object(1), MAX_SAFE_INTEGER + 1]; 9 const expected = map(values, () => true); 10 const actual = map(values, length => isIteratedAsObject({ length: length })); 11 12 expect(actual).toEqual(expected); 13 expect(isIteratedAsObject({ length: 0 })).toBeFalsy(); 14});
데이터 흐름 및 호출 체인
함수 호출 시퀀스 다이어그램
다음은 es-toolkit/compat의 map 함수 호출 시 데이터 흐름을 보여줍니다:
正在加载图表渲染器...
호출 체인 핵심 포인트
-
입력 검증 단계: src/compat/array/map.spec.ts:54-68에서 확인할 수 있듯, falsey 값과 숫자 입력에 대한 방어 처리
-
배열 유사 객체 판별: src/compat/array/map.spec.ts:164-188의
isArrayLike로직을 통해{ length: number }형태의 객체도 순회 가능 -
iteratee 처리: nullish iteratee → identity, 함수 → 직접 호출, 문자열 → 속성 접근자
-
결과 수집: 변환된 값들을 새 배열로 수집하여 반환 (원본 불변)
모듈 의존 관계
正在加载图表渲染器...
의존 관계 핵심 포인트
-
단방향 의존: Compat Layer → Core Layer 방향으로만 의존하여 순환 참조 방지
-
Cross-cutting Concerns: predicate 모듈은 여러 compat 모듈에서 사용되는 타입 검사 기능 제공
-
독립적 Core 모듈: Core 모듈 간에는 직접 의존성을 최소화하여 개별 사용 가능
핵심 설계 결정 및 기술 선택
1. 이중 계층 아키텍처 채택
결정: Core API와 Compat API를 분리
이유:
- 명확한 사용 사례 구분: 새 프로젝트는 Core 사용, 마이그레이션은 Compat 사용
- 번들 크기 최적화: Core만 사용 시 Lodash 호환 코드가 번들에 포함되지 않음
- 점진적 마이그레이션: Lodash에서 es-toolkit으로 단계적 전환 가능
제약:
- 두 계층 간 코드 중복 가능성 (래핑으로 최소화)
- API 문서화 복잡도 증가
2. TypeScript Strict Mode 사용
결정: tsconfig.json:11에서 strict: true 설정
이유:
- 타입 안전성: 런타임 에러 사전 방지
- IDE 지원: 자동 완성 및 리팩토링 도구 향상
- 문서화: 타입 정의가 곧 API 문서
제약:
- TypeScript 학습 곡선
- 일부 복잡한 타입 추론 필요
3. Lodash 테스트 케이스 재사용
결정: src/compat/array/uniq.spec.ts:7에서 Lodash 테스트 케이스 직접 참조
이유:
- 검증된 테스트: 수년간 축적된 엣지 케이스 커버리지
- 신뢰성: Lodash와의 실질적 동등성 보장
- 유지보수 효율: 테스트 케이스 직접 작성 부담 감소
제약:
- Lodash 테스트 형식에 맞춘 구현 필요
- 일부 Lodash 특유의 동작이 es-toolkit 철학과 충돌 가능
4. ESNext 타겟 및 번들러 모듈 해석
결정: tsconfig.json:4-8에서 target: ESNext, moduleResolution: Bundler 설정
이유:
- 현대적 환경 최적화: 최신 JavaScript 엔진 기능 활용
- 트리 쉐이킹: 모던 번들러와의 완벽한 호환
- 개발자 경험:
.ts확장자 명시적 import 허용
제약:
- 구형 브라우저 지원을 위한 트랜스파일 필요
- Node.js 환경에서 실험적 기능 사용 가능성
5. 안전하지 않은 기능 의도적 제외
결정: src/compat/index.ts:18-22에서 위험한 암시적 변환 제외
이유:
- 예측 가능성:
'' == 0같은 혼란스러운 동작 방지 - 버그 예방: 암시적 변환으로 인한 미묘한 버그 차단
- 현대적 철학: TypeScript strict mode와 일관성
제약:
- 100% Lodash 호환성 달성 불가 (의도적)
- 일부 레거시 코드 수정 필요
기술 선택 비교표
| 기술 | 용도 | 선택 이유 | 대안 |
|---|---|---|---|
| TypeScript | 타입 시스템 | 타입 안전성, IDE 지원, 문서화 | JavaScript + JSDoc |
| Vitest | 테스트 프레임워크 | 빠른 실행, ESM 네이티브 지원, TypeScript 통합 | Jest, Mocha |
| ESNext Target | 컴파일 타겟 | 최신 기능 활용, 성능 최적화 | ES2020, ES2015 |
| Bundler ModuleResolution | 모듈 해석 | 모던 번들러 최적화, 트리 쉐이킹 | Node, Classic |
| Strict Mode | 타입 검사 강도 | 런타임 에러 사전 방지, 코드 품질 | Normal Mode |
| Lodash Test Cases | 호환성 검증 | 검증된 테스트 커버리지, 신뢰성 | 자체 테스트 작성 |
| Dual Layer Architecture | API 구조 | 사용 사례 분리, 번들 최적화 | 단일 API |
| Array-like Support | 입력 유연성 | Lodash 호환성, 다양한 입력 지원 | Array만 지원 |
| Pure Functions | 구현 패러다임 | 부작용 없음, 테스트 용이성 | Mutable 구현 |
| Tree Shaking | 번들 최적화 | 사용하지 않는 코드 제거 | 전체 번들 |
테스트 및 검증 전략
Vitest 기반 테스트 인프라
es-toolkit은 Vitest를 테스트 프레임워크로 사용하며, src/compat/array/head.spec.ts:1-51에서 그 패턴을 확인할 수 있습니다:
typescript1import { describe, expect, expectTypeOf, it } from 'vitest'; 2import type { head as headLodash } from 'lodash'; 3 4describe('head', () => { 5 // 기능 테스트 6 it('should return the first element', () => { 7 expect(head([1, 2, 3, 4])).toBe(1); 8 }); 9 10 // 타입 호환성 테스트 11 it('should match the type of lodash', () => { 12 expectTypeOf(head).toEqualTypeOf<typeof headLodash>(); 13 }); 14});
다층 검증 전략
- 기능적 동등성: Lodash와 동일한 출력 생성 검증
- 타입 호환성:
expectTypeOf를 통한 TypeScript 타입 일치 확인 - 엣지 케이스: null, undefined, 빈 배열, 배열 유사 객체 등 경계 조건 테스트
- iteratee 패턴: 함수가 다른 함수의 인자로 사용될 때의 동작 검증
엣지 케이스 처리 예시
src/compat/array/tail.spec.ts:35-44는 다양한 엣지 케이스를 보여줍니다:
typescript1it('should return an empty array when the collection is null or undefined', () => { 2 expect(tail(null)).toEqual([]); 3}); 4 5it('should return an empty array when the collection is not array-like', () => { 6 // @ts-expect-error - invalid argument 7 expect(tail(1)).toEqual([]); 8 // @ts-expect-error - invalid argument 9 expect(tail(true)).toEqual([]); 10}); 11 12it('should support array-like', () => { 13 expect(tail({ 0: 1, 1: null, 2: 3, length: 3 })).toEqual([null, 3]); 14 expect(tail('123')).toEqual(['2', '3']); 15});
결론
es-toolkit의 아키텍처는 성능, 호환성, 안전성이라는 세 가지 핵심 목표를 달성하기 위해 신중하게 설계되었습니다. 이중 계층 구조는 Lodash 사용자에게 원활한 마이그레이션 경로를 제공하면서도, 새로운 사용자에게는 최적화된 번들 크기와 성능을 제공합니다. TypeScript strict mode와 Lodash 테스트 케이스 재사용은 높은 코드 품질과 신뢰성을 보장합니다. 모듈화된 구조는 트리 쉐이킹을 최적화하고, 명확한 관심사 분리는 유지보수성을 향상시킵니다.
