하나의 서비스를 오랫동안 운영하다보면 프로젝트의 리소스들에서 자연스럽게 불필요한 이미지, 코드, 미사용 라이브러리들이 누적되게 된다.
물론 바로 정리를 하는 습관이 되어있다면 좋겠지만 다양한 사람들이 지나쳐간 프로젝트에서 그런 기대를 하기는 어렵다. (본인 포함)
더군다나 여러 개의 프로덕트가 운영되고 있는 상태에서 직접 하나하나씩 파일들을 뒤져가며 불필요한 리소스들을 제거하는 작업을 하는 것은 너무 수고스럽다. 그러나, 자바스크립트 기반 프로젝트라는 점에서 봤을 때, 컴파일러를 이용해 ReactNative 앱, React 웹 모두에 적용할 수도 있겠다.
웹, 앱에서 정기적으로 정리를 해야할 필요가 있었기 때문에 자동으로 불필요 리소스를 정리하는 프로세스를 만드는 것을 목표로 삼았다.
정리를 했을 때 기대되는 점은 앱에서는 다운로드 용량이 줄어들어 다운로드 시간도 비례해 줄어들 것이고, 웹 환경에서는 SSR이든 CSR이든 리소스를 다운받는데 필요한 로딩시간을 줄이는 장점이 있다.
프로젝트 내의 각 리소스들 중 어떤 것이 큰 용량을 차지하는지 확인하기위해
react-native-bundle-visualizer
를 사용했다.
설치한 뒤
npx react-native-bundle-visualizer
를 커맨드라인에 입력해 실행시키면
브라우저가 실행되며 아래 사진처럼 보여진다.
어느 부분이 많이 차지하고있는지 알 수 있다.
물론, 모노레포 환경이거나 엔트리포인트를 index파을 사용하지 않는다면 --entry-file
옵션을 사용해 지정해주면 된다.
미사용 라이브러리들을 출력해보기위해 depcheck을 설치한다.
yarn add -D depcheck
, npx depcheck
아래사진 처럼 출력된다.
외부모듈 중에서 의존성으로 갖고 있는 경우에도 Unused로 출력되기 때문에 무조건 삭제하면 안되고 직접 확인해야 한다.
삭제 후에는 반드시 테스트가 필요하다.
일반적으로 ESLint가 unused variables는 출력해준다.
VSC 에서 Command + Shift + O
를 누르면 자동으로 삭제해주지만, 모듈시스템에서 모두 제거되진 않는다.
모듈시스템 내에서 참조하지 않는 변수, 함수를 출력해보려 한다.
먼저 설치를 한다. yarn add -D ts-prune
설치를 한 뒤, npx ts-prune
로 실행하면 모듈 내에서 사용중인 것들까지 출력되기 때문에 너무 많다.
used in module
인 부분들을 제외하고 출력한다.
npx ts-prune | grep -v '(used in module)'
default
로 출력되는 것은 ESM 기준으로 default export로 사용된 부분이기 때문에 이 것도 제외해본다.
npx ts-prune | grep -E -v '(used in module)|default'
이제 ESM내에서 사용하지 않는 변수, 함수들을 확인할 수 있다.
필요하다면 packages.json에 스크립트로 추가해서 사용하면 좋다.
이번엔 미사용 이미지를 출력해본다. 이미지를 모듈화해서 사용하는 방식에따라 2번에서 모두 찾아낼 수 도 있지만, 아래 코드처럼 하나의 이미지 객체에 property로 관리하는 경우에는 2번의 방법으로 찾아낼 수 없다.
설치되어있지 않다면 babel
, tsc
를 미리 설치한다.
// images.ts
const IMAGES = {
// ...
};
아래 순서로 진행해봤다.
(약간의 꼼수..?)
#!/bin/sh
npx babel src/modules/images.ts --out-file images.bundle.js
npx babel src/modules/icons.ts --out-file icons.bundle.js
mv src/modules/images.ts ../images2.ts
mv src/modules/icons.ts ../icons.ts
tsc --p tsconfig.json | grep -v ''
node ./bin/checkImage.js
mv ../images2.ts src/modules/images.ts
mv ../icons.ts src/modules/icons.ts
rm -rf index.bundle.js
rm -rf images.bundle.js
rm -rf icons.bundle.js
// checkImage.js
// nodejs 11 이상
const fs = require("fs");
const path = require("path");
const buffer = fs.readFileSync("index.bundle.js");
const bundleTxt = buffer.toString();
const imagesCode = fs.readFileSync("images.bundle.js").toString().split("\n").map(v => v.trim()).filter(v => v.includes(":") && !v.includes("{") && !v.includes("}") && !v.includes("//"));
const iconsCode = fs.readFileSync("icons.bundle.js").toString().split("\n").map(v => v.trim()).filter(v => v.includes(":") && !v.includes("{") && !v.includes("}") && !v.includes("//"));
const blankRegex = new RegExp(/((\r\n|\n|\r)$)|(^(\r\n|\n|\r))|^\s*$/gm);
const deepReadDir = (dirPath) => {
const dirents = fs.readdirSync(dirPath, {withFileTypes: true});
return dirents.map(dirent => {
const pwd = path.join(dirPath, dirent.name);
return dirent.isDirectory() ? deepReadDir(pwd) : pwd;
})
}
const imagefiles = deepReadDir("assets/images").flat();
const iconfiles = deepReadDir("assets/icons").flat();
let unusedImagesCount = 0;
let unusedIconsCount = 0;
console.log("=============== UNUSED IMAGES =============== (코드에서 사용하지 않는 이미지객체)");
imagesCode.map(v => v.split(":")[0]).forEach(v => {
if (!bundleTxt.includes(v)) {
console.log(v);
unusedImagesCount = unusedImagesCount + 1;
}
})
console.log("UNUSED IMAGES count ===> ", unusedImagesCount, "/ total", imagesCode.length);
console.log("\n\n\n");
console.log("=============== UNUSED ICONS =============== (코드에서 사용하지 않는 아이콘객체)");
iconsCode.map(v => v.split(":")[0]).forEach(v => {
if (!bundleTxt.includes(v)) {
console.log(v);
unusedIconsCount = unusedIconsCount + 1;
}
})
console.log("UNUSED ICONS count ===> ",unusedIconsCount, "/ total", iconsCode.length);
const imageRegex = new RegExp(/(.*?)\.(jpg|jpeg|png|gif|JPG|JPEG|PNG|GIF|svg|SVG|PSD|psd)$/);
let unregisteredImagesCount = 0;
let unregisteredIconsCount = 0;
console.log("\n\n\n");
console.log("=============== UNREGISTERED IMAGE FILES =============== (IMAGES에 등록되지 않은 이미지파일)");
const codes = [...imagesCode, ...iconsCode].join("");
imagefiles.filter(name => imageRegex.test(name)).forEach(name => {
if (!codes.includes(name)) {
console.log(name);
// 자동삭제
// if (fs.existsSync(name)) {
// fs.unlinkSync(name)
// }
unregisteredImagesCount = unregisteredImagesCount + 1;
}
})
console.log("UNREGISTERED IMAGE FILES count ===> ", unregisteredImagesCount, "/ total", imagefiles.length);
console.log("\n\n\n");
console.log("=============== UNREGISTERED ICON FILES =============== (ICONS에 등록되지 않은 이미지파일)");
iconfiles.filter(name => imageRegex.test(name)).forEach(name => {
if (!codes.includes(name)) {
console.log(name);
// 자동삭제
// if (fs.existsSync(name)) {
// fs.unlinkSync(name)
// }
unregisteredIconsCount = unregisteredIconsCount + 1;
}
})
console.log("UNREGISTERED ICON FILES count ===> ", unregisteredIconsCount, "/ total", iconfiles.length);
console.log("\n\n\n");
checkImage.sh
을 실행시키면 아래와 같은 결과를 얻을 수 있었다.
=============== UNUSED IMAGES =============== (코드에서 사용하지 않는 이미지객체)
imgReview
imageLogoLongXLarge
imgPaypal
imgPriceBannerNewKr
... 생략
imgBlackFridayEn1
imgScretPromotionKr
UNUSED IMAGES count ===> 48 / total 220
=============== UNUSED ICONS =============== (코드에서 사용하지 않는 아이콘객체)
iconChevronRightWhiteBold
iconChevronRightSuccess
iconChevronRightInfo
iconWarningYellow
iconSettingsGray
... 생략
icSuccessStamp_60
UNUSED ICONS count ===> / total 225
=============== UNREGISTERED IMAGE FILES =============== (IMAGES에 등록되지 않은 이미지파일)
imageLogoCircle.png
... 생략
imgWebinarHome.png
UNREGISTERED IMAGE FILES count ===> 6 / total 144
=============== UNREGISTERED ICON FILES =============== (ICONS에 등록되지 않은 이미지파일)
groupPrimary.png
groupYellow.png
hamburger_menu_black.png
horizontal_dots.png
... 생략
triangle.png
UNREGISTERED ICON FILES count ===> 61 / total 276
✨ Done in 7.84s.
확인해보았을 때, 삭제해도 괜찮다면 자동삭제 부분의 주석을 해제한 뒤 다시 한 번 실행시켜 모두 삭제한다.
결과적으로 수백개의 이미지파일을 삭제할 수 있었는데, 남아 있는 파일들을 이미지 최적화 API를 이용해 사이즈 최적화까지 마치고나니
안드로이드 기준으로 앱 다운로드 사이즈가 125mb->103mb까지 줄어들었다.
빌드 파이프라인에 추가해서 프로세스화 한다면 주기적으로 관리할 수 있는 환경을 구축할 수 있을 것이다.
눈으로 직접 확인하지않고 삭제했기 때문에 불안할 수 있다. 실제로 코드에서 사용중인데 삭제한 건 아닌지? tsc를 이용해 확인해본다.
tsconfig.json을 아래와 같이수정한다
"noEmit": true,
"compilerOptions": {
...otherOptions,
"noUnusedLocals": false,
"noUnusedParameters": false,
}
npx tsc
아래처럼 참조할 수 없는 값들을
아래처럼 출력해준다.
참조 에러가 발생하는 경우 ReactNative에서는 Crash등 심각한 버그가 발생할 수 있어 반드시 디버깅이 필요하다. 마찬가지로 빌드 파이프라인에 추가해보는 것이 좋겠다.
추가로, ESM내에서 사용하지 않는 모듈을 알아보자
yarn add -D unimported
npx unimported
ESM내에서 사용되고 있지 않은 모듈들을 보여준다.
좋은 글 감사합니다~!