회사의 번들링 결과 파일에서 중복된 Object.spread 함수가 생성되고 있는것을 확인하였다. 같은 번들임에도 중복 함수가 생성되고 있었으며 번들의 크기가 큰 경우 100~200개 까지도 중복 함수가 이름만 바뀌어 생성되는 것을 확인하였으며 이는 파일의 크기를 불필요하게 키우는 문제를 야기하기 때문에 이를 해결하기 위한 원인을 찾아보았다.
문제 예시
아래 예시는 중복된 _object_spread 함수들이 각각의 이름만 달리하여 생성된 예시이다.
function _object_spread$3O(target) {
for (var i = 1; i < arguments.length; i++) {
var source = arguments[i] != null ? arguments[i] : {};
var ownKeys = Object.keys(source);
if (typeof Object.getOwnPropertySymbols === "function") {
ownKeys = ownKeys.concat(Object.getOwnPropertySymbols(source).filter(function(sym) {
return Object.getOwnPropertyDescriptor(source, sym).enumerable;
}));
}
ownKeys.forEach(function(key) {
_define_property$4z(target, key, source[key]);
});
}
return target;
}
function _object_spread$3N(target) {
for (var i = 1; i < arguments.length; i++) {
var source = arguments[i] != null ? arguments[i] : {};
var ownKeys = Object.keys(source);
if (typeof Object.getOwnPropertySymbols === "function") {
ownKeys = ownKeys.concat(Object.getOwnPropertySymbols(source).filter(function(sym) {
return Object.getOwnPropertyDescriptor(source, sym).enumerable;
}));
}
ownKeys.forEach(function(key) {
_define_property$4y(target, key, source[key]);
});
}
return target;
}
원인
중복된 _object_spread 함수의 생성 원인을 분석하기 위해 아래와 같은 코드를 작성하여 번들링을 진행해봤다.
jsxCopy code
const object_test2 = {
d: 4,
e: 5,
f: 6,
};
export const object_test3 = { ...object_test1, ...object_test2 };
export const object_test4 = { ...object_test2, ...object_test1 };
빌드 결과는 다음과 같다.
const object_test1 = {
a: 1,
b: 2,
c: 3
};
const object_test2 = {
d: 4,
e: 5,
f: 6
};
const object_test3 = _object_spread$w({}, object_test1, object_test2);
const object_test4 = _object_spread$w({}, object_test2, object_test1);
보이는 것처럼 _object_spread 감싸져서 번들링된 것을 확인할 수 있었다. 근데 이 경우엔 이름이 같으므로 문제가 되지 않는다. 그렇다면 같은 경로에 파일을 하나 더 추가해보고 다른 경로에도 파일을 추가한 뒤 번들링을 진행해보자.
const object_test_file2223 = _object_spread$y({}, object_test_file2221, object_test_file2222);
const object_test_file2224 = _object_spread$y({}, object_test_file2222, object_test_file2221);
const object_test_file3 = _object_spread$w({}, object_test_file1, object_test_file2);
const object_test_file4 = _object_spread$w({}, object_test_file2, object_test_file1);
const object_test3 = _object_spread$x({}, object_test1, object_test2);
const object_test4 = _object_spread$x({}, object_test2, object_test1);
확인 결과, 다른 디렉토리는 물론이고, 같은 디렉토리 안에 있더라도 파일이 다르면 무조건 _object_spread 함수를 새로 선언하는 것을 확인할 수 있었다.
현재 회사에서는 번들링을 위해 rollup을 사용하고 있으며 transpile을 위한 swc를 플러그인으로 추가하여 사용하고 있다.
현재 사용 중인 rollup-plugin-swc3 플러그인은 내부적으로 swc transpile 이후 해당 코드를 사용하여 Rollup 번들링을 진행한다. 일단 번들 결과값에서 보여지는 것은 object spread 문법을 transpile 하는 것이다.
이를 통해 이는 swc에서 transpile을 진행할 때 es2018 이전 버전으로 transpile을 진행하는 것을 알 수 있다. 이후 중복 함수가 생성 되는 것은 이를 rollup에서 번들링하는 과정에서 전체적으로 트리 쉐이킹이 제대로 이루어지지 않기 때문에 발생하는 것으로 추측된다.
이와 별개로 아래는 ecmaScript 버전에 따른 차이를 검증한 코드이다. 테스트는 swc playground에서 진행했다.
[코드]
const object_same_dir2_one = {
a: 1,
b: 2,
c: 3,
};
const object_same_dir2_two = {
d: 4,
e: 5,
f: 6,
};
export const object_same_dir2_res1 = { ...object_same_dir2_one, ...object_same_dir2_two };
export const object_same_dir2_res2 = { ...object_same_dir2_two, ...object_same_dir2_one };
[es2018 이전]
function _define_property(obj, key, value) {
if (key in obj) {
Object.defineProperty(obj, key, {
value: value,
enumerable: true,
configurable: true,
writable: true
});
} else {
obj[key] = value;
}
return obj;
}
function _object_spread(target) {
for(var i = 1; i < arguments.length; i++){
var source = arguments[i] != null ? arguments[i] : {};
var ownKeys = Object.keys(source);
if (typeof Object.getOwnPropertySymbols === "function") {
ownKeys = ownKeys.concat(Object.getOwnPropertySymbols(source).filter(function(sym) {
return Object.getOwnPropertyDescriptor(source, sym).enumerable;
}));
}
ownKeys.forEach(function(key) {
_define_property(target, key, source[key]);
});
}
return target;
}
const object_same_dir2_one = {
a: 1,
b: 2,
c: 3
};
const object_same_dir2_two = {
d: 4,
e: 5,
f: 6
};
export const object_same_dir2_res1 = _object_spread({}, object_same_dir2_one, object_same_dir2_two);
export const object_same_dir2_res2 = _object_spread({}, object_same_dir2_two, object_same_dir2_one);
[es2018 이후]
const object_same_dir2_one = {
a: 1,
b: 2,
c: 3
};
const object_same_dir2_two = {
d: 4,
e: 5,
f: 6
};
export const object_same_dir2_res1 = {
...object_same_dir2_one,
...object_same_dir2_two
};
export const object_same_dir2_res2 = {
...object_same_dir2_two,
...object_same_dir2_one
};
본론으로 돌아와 다음엔 번들링 설정을 확인해보자
//[코드]
minify: config.minify,
sourceMaps: config.minify !== true,
tsconfig: options.tsconfig,
jsc: {
minify: config.minify
? {
compress: true,
mangle: false,
}
: undefined,
//[결과] console.log('swc options', options)
swc options {
minify: undefined,
sourceMaps: true,
tsconfig: '[path]',
jsc: { minify: undefined }
}
위 코드는 swc plugin 설정 코드이다. 또한 번들링 설정에는 treeshake 옵션이 true로 잘 설정되어 있는것을 확인할 수 있었다.
콘솔을 찍어 확인해본 결과 개발 환경에서는 minify 설정이 undefined로 내려오고 있는 것을 확인할 수 있었다. 이는 프로덕션 환경에서는 true로 잘 설정되어 내려올것이다.
아마 그럴일은 없겠지만 정말 혹시 만약에라도 이로 인해 트리 쉐이킹이 제대로 이루어지지 않을 수 있으므로 검증을 위해 해당 옵션을 true로 변경하여 트리 쉐이킹이 잘 되는지 다시 확인해보았다.
물론 rollup을 직접 디버깅 해보는게 가장 확실하고 빠르겠지만 rollup 내부 코드를 잠깐 봐봤는데 너무 어지러웠어서 좀 더 테스트를 진행해보기로 결정했다.
확인 결과 minify된 코드에서도 object_spread가 여전히 중복 생성되는 것을 확인할 수 있었다
;function _object_spread$2(target){for(var i=1;i<arguments.length;i++){var source=null!=arguments[i]?arguments[i]:{},ownKeys=Object.keys(source);"function"==typeof Object.getOwnPropertySymbols&&(ownKeys=ownKeys.concat(Object.getOwnPropertySymbols(source).filter(function(sym){return Object.getOwnPropertyDescriptor(source,sym).enumerable}))),ownKeys.forEach(function(key){var value;value=source[key],key in target?Object.defineProperty(target,key,{value:value,enumerable:!0,configurable:!0,writable:!0}):target[key]=value})}return target}let object_same_dir1_one={a:1,b:2,c:3},object_same_dir1_two={d:4,e:5,f:6},object_same_dir1_res1=_object_spread$2({},object_same_dir1_one,object_same_dir1_two),object_same_dir1_res2=_object_spread$2({},object_same_dir1_two,object_same_dir1_one);function _object_spread$1(target){for(var i=1;i<arguments.length;i++){var source=null!=arguments[i]?arguments[i]:{},ownKeys=Object.keys(source);"function"==typeof Object.getOwnPropertySymbols&&(ownKeys=ownKeys.concat(Object.getOwnPropertySymbols(source).filter(function(sym){return Object.getOwnPropertyDescriptor(source,sym).enumerable}))),ownKeys.forEach(function(key){var value;value=source[key],key in target?Object.defineProperty(target,key,{value:value,enumerable:!0,configurable:!0,writable:!0}):target[key]=value})}return target}let object_same_dir2_one={a:1,b:2,c:3},object_same_dir2_two={d:4,e:5,f:6},object_same_dir2_res1=_object_spread$1({},object_same_dir2_one,object_same_dir2_two),object_same_dir2_res2=_object_spread$1({},object_same_dir2_two,object_same_dir2_one);var index$p=Object.freeze({__proto__:null,default_value:"default_value",
혹시 놓친 옵션이 있을수도 있으니 rollup-plugin-swc3 패키지의 문서를 확인해 보았으며 확인 결과 영향이 있을법한 옵션은 따로 찾지 못했다.
앞의 테스트들을 진행하다 보니 대충 원인은 감이 오기 시작했다. 아래는 처음 추측한 내용이다.
- swc가 es2018 이전 버전에서 object spread 문법을 transpile 하는 과정에서 object_spread를 생성한다. 이 때 object_spread함수는 모두 같은 이름을 가진다.
- rollup이 이를 번들링 할 때 동일한 이름, 동일한 함수에 대해 제대로 트리 쉐이킹을 해주지 못하거나 애초에 이를 지원해주지 않는다. → 이를 위해 번들링에 들어가는 계산을 생각해보면 지원해주지 않는다해도 이해가 간다.
대략 원인을 파악했으며 해당 키워드로 rollup github의 이슈 탭을 찾아 본 결과 비슷한 케이스를 찾을 수 있었다.
https://github.com/rollup/rollup/issues/3205
Don't duplicate identical functions · Issue #3205 · rollup/rollup
Expected Behavior / Situation Source files have a common function in them (e.g. __extends in OpenLayers 6: https://www.npmjs.com/package/ol) This common function should only be found once in the pa...
github.com
이 글에서 다루고 있는 문제 역시 위 이슈처럼 rollup에서 다른 파일에 있는 중복 함수들을 $[key]를 추가하여 여러개 선언하고 있으니 완벽하게 일치하는 이슈라고 말할 수 있다. 즉 이는 rollup을 버리거나 후처리 과정을 진행하지 않는이상 쉽게 해결할 수 없는 이슈라는 결론을 내릴 수 있다.
해결
원인을 명확하게 파악했으니 이제 이 문제를 해결해보자 해결 방법은 의외로 간단했다.
SWC의 transpile 과정에서 현재 없는 문법을 처리하기 위해 해당 함수를 생성하고 있는데, SWC의 target 버전을 현재 (ES2017)보다 상위 버전(ES2018)으로 올리면 문제를 간단하게 해결할 수 있다.
물론 object_spread뿐만 아니라 다른 문법의 경우도 이와 같은 문제가 발생할 수 있는데 ecmsScript 버전이 올라가면 올라갈수록 이런 문제가 사라질 것이다.
단 주의해야 할 점이 있다. 이는 현재 서비스중인 코드의 버전을 바꾸는 일이기에 이 수정으로 인해 이전 버전 브라우저에서 해당 버전을 지원하지 않는 경우 에러가 발생할 수 있다. 따라서 사전에 고객 데이터를 기반으로 호환성 문제 가능성을 파악하는 과정이 필요하다.
나의 경우에는 호환성 검증 이전에 벤치마킹을 진행해 이 수정이 유의미한지를 먼저 검증해보기로 결정하였다.
아래 예시 코드처럼 rollup의 swc plugin 설정쪽에 명시적으로 es2018을 추가해준뒤 벤치마킹을 진행했다.
jsc: {
target: 'es2018',
minify: config.minify
? {
compress: true,
mangle: false,
}
: undefined,
}
벤치마킹 및 테스트 결과
수정을 적용한 후 번들 사이즈와 번들링 시간을 테스트해보았다. 적당히 큰 프로젝트에서 빌드를 해 본 결과, 번들링 시간은 동일하지만 번들 사이즈가 약 5% 감소한 것을 확인할 수 있었다.
이는 유의미한 결과값이기에 추후 호환성 검증 문제가 없다고 결론이 나면 프로젝트에 해당 수정 내용을 반영할 것 같다.
'builder' 카테고리의 다른 글
unused-import 제거를 통한 빌더 성능 개선 (0) | 2024.07.31 |
---|---|
번들 사이즈 이슈 해결(eslint,tsconfig) (2) | 2024.07.24 |