배경
현재 개발 중인 서비스는 병원이라는 특수한 환경에서 운영되기 때문에 오래된 브라우저, 폐쇄망(오프라인) 설치 등 다양한 제약이 존재합니다. 다양한 제약을 극복할 만큼 최적화가 잘 되어 있다면 좋겠지만, 5년 전에 시작된 프로젝트라 기술 부채가 많고 최적화와는 거리가 있었습니다.
오래된 브라우저
병원과의 논의를 거쳐 꽤 낮은 브라우저 버전까지 지원하고 있습니다. 오래된 브라우저는 최신 브라우저에 비해 자바스크립트 실행 성능이 좋지 않고 렌더링 속도가 느리다는 단점이 있습니다. 그럼 무엇이 속도를 느리게 만들까요? 핵심은 자바스크립트에 있습니다. 자바스크립트 파일이 클수록 페이지가 로드되는 데 오래 걸립니다.

같은 파일 크기라고 해도 자바스크립트는 이미지에 비해 브라우저가 처리할 것이 많습니다. 자바스크립트 파일 다운로드 후 파싱, 컴파일, 실행과 같은 여러 단계를 거치기 때문입니다. 속도를 개선하기 위해서는 자바스크립트 파일 크기를 줄이는 것이 중요합니다.
폐쇄망(오프라인) 설치
병원에서는 보안을 위해 외부 인터넷 연결이 차단된 폐쇄망을 사용합니다. 프론트엔드 개발에 쓰이는 라이브러리를 패키지 저장소(npm, yarn)에서 가져와야 하는데, 폐쇄망에서는 패키지 저장소에 접근할 수 없습니다. 따라서 프로젝트에 필요한 패키지가 담긴 node_modules 디렉터리를 사전에 압축해 가야 합니다. 하지만 압축한 node_modules 사이즈도 너무 커서 최적화가 필요한 상태였습니다.
성능 최적화의 필요성
이런 제약들을 극복하기 위해 프론트엔드 성능 최적화가 필요했습니다. 올해 1월부터 7개월간 틈틈이 성능을 최적화한 과정을 정리해두고자 합니다.
도구를 활용해 현재 상태 파악하기
@next/bundle-analyzer
번들이란 JavaScript, CSS, 이미지 등 웹에서 필요한 모든 자원을 통합한 파일을 말합니다.
이 번들이 어떤 코드로 이루어져 있는지 트리맵으로 보여주는 도구가 @next/bundle-analyzer
입니다.
트리맵은 실제 크기와 비례하기 때문에 각 구성 요소의 비중을 쉽게 확인할 수 있습니다.
아니나 다를까 node_modules가 큰 비중을 차지하고 있었습니다.

depcheck
node_modules 사이즈를 줄이기 위해 사용하지 않는 패키지부터 제거하기로 결심했습니다.
개발자가 직접 package.json
을 보며 프로젝트에 설치된 패키지 목록을 쭉 나열하고 코드와 하나씩 비교하면 오래 걸리겠죠?
이런 걸 쉽게 도와주는 도구가 있어서 사용해 봤습니다.
바로 depcheck인데요, 원리부터 잠깐 설명드리겠습니다.
1. 프로젝트에서 package.json 파일 탐색
2. package.json 파싱해 의존성 추출
3. 코드 기반으로 AST(Abstract Syntax Tree) 생성
4. AST에서 ImportDeclaration 같은 노드 식별
5. 해당 노드에서 모듈 이름 수집
6. 모듈 이름과 의존성을 비교해 미사용 의존성 파악
가장 먼저 프로젝트에서 package.json
파일을 찾습니다.
이 파일을 해석해 어떤 패키지에 의존성을 가지고 있는지 추출합니다.
그 다음 전체 소스 코드 기반으로 Abstract Syntax Tree를 생성합니다. 줄여서 AST라고 해요.

이 그림은 https://astexplorer.net 에 import React from "react"
구문을 입력해 만든 AST입니다.
depcheck은 AST에서 ImportDeclaration
같은 노드를 식별합니다.
(식별하는 노드 종류는 링크를 참고해 주세요.)
해당 노드에서 모듈 이름을 수집하는데, 이 예시에서는 React
가 모듈 이름에 해당합니다.
모듈 이름과 의존성을 비교해 이름이 없으면 미사용으로 판단합니다.

depcheck을 활용해 파악한 미사용 패키지는 총 18개였는데요, 사용 예정인 패키지 하나만 남기고 전부 제거했습니다🥳
기술 변화에 따른 최적화 기회 찾기
우선 도구의 도움을 받았고, 제가 추가로 기여할 수 있는 부분은 없을지 찾아봤습니다. 단지 기술 변화를 잘 파악하고 있다는 이유만으로 개선할 수 있는 부분이 2가지 있었습니다✌️
첫 번째는 라이브러리의 변화를 파악하고 있어 가능했던 최적화입니다. TypeScript 공식 문서를 보면 5.5 버전에서 패키지 크기가 약 33% 감소했다는 것을 알 수 있습니다. 패키지 버전 업그레이드만으로 패키지 크기를 줄일 수 있었습니다.
Further leveraging our transition to modules in 5.0, we've significantly reduced TypeScript's overall package size by making
tsserver.js
andtypingsInstaller.js
import from a common API library instead of having each of them produce standalone bundles. This reduces TypeScript's size on disk from 30.2 MB to 20.4 MB, and reduces its packed size from 5.5 MB to 3.7 MB!
두 번째는 프레임워크의 변화를 파악하고 있어 가능했던 최적화입니다. Next.js 공식 문서에 따르면 12 버전부터 SWC가 내장되어 있어 @swc/core를 직접 설치할 필요가 없습니다. Next.js 버전만 올리고 미처 제거하지 않았던 불필요한 패키지를 제거해 node_modules 크기가 40.8MB 감소할 수 있었습니다.
The Next.js Compiler, written in Rust using SWC, allows Next.js to transform and minify your JavaScript code for production. This replaces Babel for individual files and Terser for minifying output bundles. Compilation using the Next.js Compiler is 17x faster than Babel and enabled by default since Next.js version 12.
Tree shaking으로 자바스크립트 사이즈 줄이기

Tree shaking이란 사용하지 않는 코드를 제거하고 필요한 코드만 가져와 번들 크기를 줄이는 기법을 말합니다. 번들 분석 결과에서 lodash가 3번째로 큰 비중을 차지한 이유는 tree shaking이 적용되지 않았기 때문입니다. 이 문제를 해결하기 위해서는 cherry picking을 통해 필요한 함수만 불러오거나, lodash-es로 교체해야 합니다. 점진적인 개선을 위해 cherry picking 방식을 선택했고, 보일 때마다 수정하고 있습니다.
입사 전까지 lodash를 사용해 본 적도 없고, 현재 회사에서도 lodash의 필요성을 느끼지 못해 속도가 더 빠른 ES6 문법으로 lodash를 대체하는 작업도 진행하고 있습니다. You-Dont-Need-Lodash-Underscore와 같은 참고 자료도 팀에 공유하며 lodash 사용을 지양하고자 노력했습니다. 토스 기술 블로그의 'ESLint와 AST로 코드 퀄리티 높이기' 아티클에 따르면 토스의 ESLint 규칙 중에도 ban-lodash라는 것이 있다고 합니다.
아직도 lodash가 많이 남아있어 꾸준히 작업하며 자바스크립트 사이즈를 줄여보려고 합니다.
변화

위와 같은 과정을 통해 node_modules 사이즈는 28%나 감소했습니다. 단순히 패키지를 제거한 것이 아니라 중간중간 TanStack Query, Vitest 등 여러 패키지가 추가되었음에도 사이즈가 감소한 것을 감안하면 유의미한 변화라고 생각합니다.

node_modules뿐만 아니라 static/chunks/pages/_app.js
를 의미하는 노란색 영역의 비중도 줄어들었습니다.

static/chunks/pages/_app.js
의 사이즈 변화를 정리하면 위와 같습니다.
원본 파일의 크기를 의미하는 Stat size가 약 26% 감소했습니다.
브라우저가 해석할 코드의 크기를 의미하는 Parsed size는 약 23% 감소했습니다.
Parsed size는 로딩 성능에 직접적인 영향을 미치는 요소로, @next/bundle-analyzer
에서 기본으로 체크하는 사이즈입니다.
압축 알고리즘 gzip으로 압축한 파일의 크기를 의미하는 Gzipped size는 약 22% 감소했습니다.
Gzipped size는 Parsed size를 압축한 것이기 때문에 Parsed size가 줄면 Gzipped size도 따라서 줄어듭니다.

First Load JS 측정 결과도 비교해 봤습니다. 자바스크립트 코드나 의존성이 추가되면 First Load JS 값이 같이 증가하는데, 신규 기능들이 추가되었음에도 불구하고 First Load JS 값이 줄어들었다는 점이 의미있었습니다. First Load JS가 줄어드니, 네트워크 탭에서 리소스 크기와 로드 시간이 약 10% 줄어든 것을 확인할 수 있었습니다.
앞으로 개선할 점
번들 분석 결과, 가장 큰 비중을 차지하는 것은 유지보수가 중단된 사내 디자인 시스템이었습니다. 두 개의 레거시 디자인 시스템을 제거하면 번들 사이즈 감소가 크게 감소할 것으로 예상됩니다. 디자인 시스템 교체가 최적화에 중요한 기여를 할 것으로 기대하며 새로운 디자인 시스템 개발에 열심히 참여하고 있습니다.