당신의 TreeShaking은 정말로 안녕하신가요?
프로젝트를 진행하면서 어느 순간 번들 사이즈가 대폭 커지면서 의아함을 감출 수가 없었다.
지금까지 진행한 프로젝트에 비해 약 2배 가까이 되는 번들 사이즈를 보고 내가 어딘가를 놓쳤다는 의구심이 들 수 밖에 없었다.
번들 사이즈의 원인으로 의심되는 부분은 아래 4개였다.
- Dynamic Import를 놓쳤다.
- 사이즈 큰 모듈을 Lazy하게 가져오도록 하지 않았다.
- TreeShaking이 제대로 되지 않았다.
Dynamic Import 문제였는지 체크
가장 상단의 컴포넌트를 주석 처리를 하고 빌드를 해보니 First Load Bundle Size가 약 10kb 정도 줄어들긴 했지만, 여전히 300kb나 되는 굉장히 큰 것을 보고 이것이 큰 원인은 아니라는 것을 깨달았다.
사이즈 큰 모듈 Lazy하게 가져오도록 하지 않았었는지 체크
전체적으로 import해서 가져올 때, `import('{moduleName}')` 형식으로 처리하면서 Lazy하게 가져오게 하는 것을 체크하면 이 부분이 문제가 아닌 것을 알 수 있었다.
TreeShaking이 제대로 되지 않았는지 체크
우선 제일 최상위 컴포넌트 파일에서 컴포넌트 호출하는 부분을 주석 후, 모든 import해오는 요소들을 주석하니 당연히 번들 사이즈가 모두 줄어들었다.
그 상태에서 import해오는 요소들을 하나씩 주석을 풀면서 확인을 해보니, utils.ts 파일에서 isServer인지 판단하는 변수를 import하니 급격히 번들 사이즈가 늘어나는 것을 확인했다.
// ./utils.ts
import jwt from 'jsonwebtoken'
export const isServer = typeof window === 'undefined';
export const getJsonWebToken = () => {
const token = jwt.decode("~~");
return token;
}
// ./index.tsx
import { isServer } from './utils';
...
나는 Next.js13 환경이고 Webpack5, SWC를 돌리고 있기 때문에, 별도의 환경 설정을 해주지 않아도 정상적으로 TreeShaking이 될 것이라 믿었다.
우선 범인은 상위에 import해서 사용하는 아주 무거운 모듈인 jwt였다.
jwt가 CJS용 모듈이여서 문제원인의 가능성이 있을 수도 있다고 인지하기까지도 시간이 꽤 걸렸고, jwt import하는 곳과 관련 로직들을 주석을 하니 번들 사이즈가 줄어든 것을 보고 이녀석이 문제라는 것을 확신을 했다.
근데 jwt는 import만 할 뿐, 실제로 isServer 변수를 import하는데는 관련이 없으니 당연히 TreeShaking이 되어야 하지 않나 라고 생각을 했었다.
혹시나 import하는 것 자체가 해당 파일 내 컨텍스트와 연관이 되어버려서인가 싶어, 해당 함수 내에서 dynamic하게 호출하는 식으로 수정을 해보았다.
// ./utils.ts
export const isServer = typeof window === 'undefined';
export const getJsonWebToken = async () => {
const jwt = await import('jsonwebtoken');
const token = jwt.decode("~~");
return token;
}
// ./index.tsx
import { isServer } from './utils';
...
완전히 isServer랑 분리된 형태이기 때문에 문제가 없을 것이라 생각했지만, TreeShaking은 여전히 되지 않았다. 그래서 SWC 자체 문제라고 판단하고 'TreeShaking is not working in SWC' 관련 구글링을 열심히 해보았지만 해답은 찾을 수 없었다.
SWC에서 해결하는 방법 찾는건 포기하고, 내가 이전에 Babel을 이용한 TreeShaking 환경 세팅 관련해서 블로그를 잘 정리했던게 생각나서 Babel로 한번 해볼까 해서 들어갔다가, 정리한 글 중 아래 내용이 눈에 띄었다.
"cjs(CommonJS)형식의 코드는 TreeShaking이 되지 않으니, export.module 형식의 코드는 가져오지 않도록 한다."
CJS형식의 모듈은 TreeShaking이 되지 않는다라... 분명 저 JWT가 Node환경에서 사용하기 때문에 CJS인 가능성이 높았고, 확인해보니 역시나 였다.
비록 지금도 jwt를 동적으로 가져오도록 해서 완벽히 분리한 상태이긴 했지만, 혹시나 해서 jwt관련 로직들만 따로 다른 파일로 분리를 했더니, TreeShaking이 동작하면서 번들사이즈가 대폭 줄어들었다.
Before TreeShaking
After TreeShaking
너무 기뻤다.
같은 파일에만 있으면 CJS용 모듈을 직접 사용하지 않든, Dynamic하게 가져오도록 하든간에 무조건 해당 파일 내 코드들이 CJS로 엉켜서 전체적으로 TreeShaking이 되지 않는다는 것을 처음으로 깨달았다.
앞으로는 유틸성 코드라고해서 모두 한 파일에 몰아넣는게 아니라, CJS용, ESM용 로직을 잘 분리해서 사용해야겠다.