React Native, React 프로젝트에서 컴파일러 이용해 사용하지 않는 코드, 라이브러리, 이미지 정리하기 (unused variables, libraries, dead code)

권준혁·2023년 4월 23일
3

유지보수

목록 보기
1/1

배경

하나의 서비스를 오랫동안 운영하다보면 프로젝트의 리소스들에서 자연스럽게 불필요한 이미지, 코드, 미사용 라이브러리들이 누적되게 된다.
물론 바로 정리를 하는 습관이 되어있다면 좋겠지만 다양한 사람들이 지나쳐간 프로젝트에서 그런 기대를 하기는 어렵다. (본인 포함)
더군다나 여러 개의 프로덕트가 운영되고 있는 상태에서 직접 하나하나씩 파일들을 뒤져가며 불필요한 리소스들을 제거하는 작업을 하는 것은 너무 수고스럽다. 그러나, 자바스크립트 기반 프로젝트라는 점에서 봤을 때, 컴파일러를 이용해 ReactNative 앱, React 웹 모두에 적용할 수도 있겠다.
웹, 앱에서 정기적으로 정리를 해야할 필요가 있었기 때문에 자동으로 불필요 리소스를 정리하는 프로세스를 만드는 것을 목표로 삼았다.

이점

정리를 했을 때 기대되는 점은 앱에서는 다운로드 용량이 줄어들어 다운로드 시간도 비례해 줄어들 것이고, 웹 환경에서는 SSR이든 CSR이든 리소스를 다운받는데 필요한 로딩시간을 줄이는 장점이 있다.

프로젝트 파악하기 (react-native-bundle-visualizer)

프로젝트 내의 각 리소스들 중 어떤 것이 큰 용량을 차지하는지 확인하기위해
react-native-bundle-visualizer를 사용했다.

설치한 뒤
npx react-native-bundle-visualizer를 커맨드라인에 입력해 실행시키면
브라우저가 실행되며 아래 사진처럼 보여진다.
어느 부분이 많이 차지하고있는지 알 수 있다.
물론, 모노레포 환경이거나 엔트리포인트를 index파을 사용하지 않는다면 --entry-file 옵션을 사용해 지정해주면 된다.

사용하지 않는 코드, 라이브러리, 이미지 정리하기

1. 미사용 외부모듈 확인 (depcheck)

미사용 라이브러리들을 출력해보기위해 depcheck을 설치한다.
yarn add -D depcheck, npx depcheck

아래사진 처럼 출력된다.
외부모듈 중에서 의존성으로 갖고 있는 경우에도 Unused로 출력되기 때문에 무조건 삭제하면 안되고 직접 확인해야 한다.
삭제 후에는 반드시 테스트가 필요하다.

2. 미사용 코드 찾기 (ts-prune)

일반적으로 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에 스크립트로 추가해서 사용하면 좋다.

3. 미사용 이미지 찾기

이번엔 미사용 이미지를 출력해본다. 이미지를 모듈화해서 사용하는 방식에따라 2번에서 모두 찾아낼 수 도 있지만, 아래 코드처럼 하나의 이미지 객체에 property로 관리하는 경우에는 2번의 방법으로 찾아낼 수 없다.
설치되어있지 않다면 babel, tsc를 미리 설치한다.

// images.ts
const IMAGES = {
	// ...
};

아래 순서로 진행해봤다.
(약간의 꼼수..?)

  1. babel을 이용해 ts파일을 js로 변경
  2. node fs모듈을 이용해 IMAGES의 key값들을 추출
  3. tsc를 이용해 번들파일 추출 (난독화 X)
  4. 모노번들파일을 fs모듈로 읽고 IMAGES의 속성 중 번들파일에서 사용하지 않는 것들을 출력
  5. (optional) 삭제

shell script 파일

#!/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

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까지 줄어들었다.
빌드 파이프라인에 추가해서 프로세스화 한다면 주기적으로 관리할 수 있는 환경을 구축할 수 있을 것이다.

4. 재확인

눈으로 직접 확인하지않고 삭제했기 때문에 불안할 수 있다. 실제로 코드에서 사용중인데 삭제한 건 아닌지? tsc를 이용해 확인해본다.

tsconfig.json을 아래와 같이수정한다

"noEmit": true,
  "compilerOptions": {
    ...otherOptions,
    "noUnusedLocals": false,
    "noUnusedParameters": false,
  }
npx tsc

아래처럼 참조할 수 없는 값들을

아래처럼 출력해준다.

참조 에러가 발생하는 경우 ReactNative에서는 Crash등 심각한 버그가 발생할 수 있어 반드시 디버깅이 필요하다. 마찬가지로 빌드 파이프라인에 추가해보는 것이 좋겠다.

5. (추가) 미사용 내부모듈 찾기 (unimported)

추가로, ESM내에서 사용하지 않는 모듈을 알아보자

yarn add -D unimported
npx unimported

ESM내에서 사용되고 있지 않은 모듈들을 보여준다.

profile
웹 프론트엔드, RN앱 개발자입니다.

1개의 댓글

comment-user-thumbnail
2024년 1월 22일

좋은 글 감사합니다~!

답글 달기