모노레포의 node_modules 한번에 지우기
첫번째 포스트를쓰고 언 6개월.. 그사이 인생에있어서 큰이벤트인 결혼과 집관련 문제로 여러 잔스트레스가 쌓여 블로깅에 소홀했다.. 그동안 포스팅할 주제는 생각해왔지만 오랜만에 쓰는 글이기에 좀 가벼운 내용으로 시작해보려 한다!! <HeaderLink text="모노레포로 프로젝트 전환 이후 node_modules 관리" /> vite로 번들러를 변경함과 동시에 모노레포로 전환했기에 시기는 좀 되었다. `client`, `widget`, `util` 총 3개의 레포를 관리하고 있는데, 패키지 재설치할 일이 많지 않다보니, 별 생각없이 node_modules 제거를 각 레포마다 `rm -rf`를 하고있었다. 하지만 최근 패키지 정리 및 버전관리를 하면서 node_modules를 지웠다 설치했다 할 일이 많았었는데 정말 여간 귀찮은게 아니어서 간단한 node script를 만들었다. ```javascript const fs = require('fs'); const deleteNodeModules = () => { const NODE_MODULES = 'node_modules'; const paths = { root: './' + NODE_MODULES, client: './packages/client/' + NODE_MODULES, chat_widget: './packages/chat_widget/' + NODE_MODULES, }; for (const name in paths) { try { const path = paths[name]; fs.accessSync(path); fs.rm(path, { recursive: true, force: true }, (err) => { if (err) { console.log(`${name}의 node_modules를 지우는데 문제가 생겼습니다.`, err); return; } console.log(`DELETE - [${name}] node_modules`); }); } catch (err) { console.log(`${name}의 node_modules이 존재하지 않습니다.`); } } }; ``` 각 모노레포 경로를 객체로 만들어 순회한다, 레포가 3개밖에 없으니 동기식으로 작동하는 `accessSync` 메서드를 써주고, 파일이 있냐없냐만 체크하면 되니 두번째 매개변수의 권한은 `fs.constants.F_OK` 지만 기본값이므로 생략해 주었다. 파일이 존재하지 않다면 catch문으로 이동해 로그를 띄우게 되고, 존재한다면 `rm -rf`와 유사한 동작을 하는 `rm` + `{ recursive: true, force: true }`을 이용해 제거해준다. 해당 스크립트의 실행은 아래와 같이 한다. ```JSON "delete" : "node -e \'require('./scripts.js').deleteNodeModules()\'" ``` 모노레포 root디렉토리안에 scripts.js라는 파일이 존재한다. 프로젝트 세팅과 관련된(호스트 파일 세팅 등) 유틸 함수들도 별도로 스크립트로 만들었기때문에 `-e` 옵션으로 script.js를 실행해서 해당함수만 가져와서 실행해준다. 하지만 window환경에서 해당 스크립트가 실행이 안됐는데, 이유는 작은따옴표에 있었다. window와 unix 기반 시스템간 작은따옴표 처리방식이 달라서 그럴 수 있다고 해 아래와 같이 바꾼후 정상 동작됐다. ```JSON "delete" : "node -e \"require('./scripts.js').deleteNodeModules()\"" ``` 부족한 내용이지만, 팀원들의 인식하지 못하는 불편함을 하나 제거했다는 생각에 나름 뿌듯하다 😂
CRA에서 VITE로
3년전 CRA로 시작한 우리 프로젝트는 현재까지도 CI/CD가 구축되지 않았다. TDD를 하지 않기때문에 기본적으로 테스트코드가 존재하지 않았고, 무엇보다 바라만봐도 마음아픈 미니타워 사이즈의 개발서버가 CI/CD를 구축했을시 잘 버텨줄 수 있을지 의문이었기 때문이다. (실제로 배포만이라도 자동화 해보자는 마음으로 구축후 빌드해봤으나 빌드만으로 RAM 사용률이 45%를 넘어서면서 개발서버가 먹통이되는 현상이 나타났다..) 다가오는 출시압박에 요구사항이 커짐에 따라 기능개발에만 급급한 프로젝트 빌드시간은 평균 90s를 넘고야 말았고, CD없이 개발-스테이징-운영서버의 3중배포는 나를 피곤하게 하기에 충분했고 미뤄왔던 번들러 교체를 진행하게된 계기가됐다. <HeaderLink text="esbuild-loader & vite 선택의 순간" /> 오로지 빌드타임을 줄이는 것이 목표였기때문에 현재 상황에서 <OutLink href="https://github.com/privatenumber/esbuild-loader" text="esbuild-loader" />를 적용해 간편한 세팅으로 빠른 적용을 하고싶었다. 하지만 프로젝트가 CRA 기본 + craco로 typescript-path 설정만 되어있었기 때문에, CRA 기본 세팅이 필요로 하는 것 보다 무겁게 설정되어있지는 않을까 하는 마음에 eject를 진행해서 확인해본 결과, 생각보다 많은 세팅이 들어가있었고 이는 불필요하다는 판단 하에 <a href="https://ko.vitejs.dev/guide/why.html" target="_blank">vite</a>를 선택하게 됐다. <HeaderLink text="설치 및 세팅" /> 기존설정을 동일하게 가져가기 위한 몇가지 플러그인을 설치후 react-scripts와 craco는 제거후 package.json의 scripts를 변경해줬다. ```bash pnpm add -D vite@latest vite-tsconfig-paths vite-plugin-svgr pnpm remove react-scripts craco ``` - <OutLink href="https://github.com/aleclarson/vite-tsconfig-paths#readme" text="vite-tsconfig-path" />: Typescript path mapping으로 모듈을 가져오기 위한 플러그인 - <OutLink href="https://github.com/pd4d10/vite-plugin-svgr#readme" text="vite-plugin-svgr" />: svg를 react-component로 변환하기 위한 플러그인 ```JSON "start" : "vite" "build" : "vite build" ``` 현재 프로젝트는 로컬서버에서도 개발서버와 동일한 환경을 위해 host 설정 및 https + 443포트를 사용중이다 ^^b 그렇기에 별도의 설정이 필요했다. ```javascript // vite.config.ts import * as fs from 'fs'; import tsconfigPaths from 'vite-tsconfig-paths'; import svgr from 'vite-plugin-svgr'; import react from '@vitejs/plugin-react'; import { defineConfig, loadEnv } from 'vite'; export default defineConfig(({ mode }) => { const env = loadEnv(mode, process.cwd(), ''); return { server: { port: 443, strictPort: true, host: env.HOST, https: { key: fs.readFileSync(env.SSL_KEY_FILE), cert: fs.readFileSync(env.SSL_CRT_FILE), }, }, plugins: [ react({ jsxImportSource: '@emotion/react', babel: { plugins: ['@emotion/babel-plugin'], }, }), tsconfigPaths(), svgr(), ], envPrefix: 'REACT_APP_', build: { outDir: 'build', }, }; }); ``` https를 쓰기위한 인증서 설정 + 별도의 plugin설정까지 추가했다. vite는 클라이언트에서 환경변수에 접근하기 위한 prefix가 `VITE_`지만, 개발,스테이징,운영 3가지 서버의 .env를 바꾸기 귀찮아 일단 prefix를 기존과 맞춰서 바꿔주었다. 추가로 svgr 플러그인은 사용하려면 import 하는 경로 마지막에 `?react` suffix를 붙여야한다. ```javascript import SvgCallIn from '@assets/assets-svg/interaction_call_in.svg?react'; ``` <HeaderLink text="process.env -> import.meta" /> 기존에 환경변수 참조 코드에 문제가 생겨서 확인해보니 vite는 import.meta.env로 환경변수를 참조한다고 한다. 그래서 `process.env` 에서 `import.meta`로 변경해 주었다 <HeaderLink text="assets파일 핸들링" /> 로컬서버에서는 문제가 없었는데 production에서 문제가 생겼다. 기존 css 내부에 `background: url()`로 인라이닝한 스타일이 적용이 안되는 것이었다. 해당부분이 문제가 되는건 아래에 나오지만 vite는 로컬서버는 번들링하지 않는다 그렇기때문에 production과 assets-path가 예상과 다를 수 있다는 것이다. url 변수에 `Double-quote("")`를 붙여줘야 적용된다고 한다. 하지만 prettier설정의 `Single-quote`가 켜져있었기때문에 `.styles.ts` 모듈에는 `Double-quote("")`가 적용되도록 추가 수정해주었다. ```javascript // .styles.ts import Svg from "@assets/svgs/svg"; const Container = css` background-image: url("${Svg}"); ` // .prettierrc.js module.export = { overrides: [ { files: '*.styles.ts', options: { singleQuote: false, }, }, ], } ``` <HeaderLink text="Native ESM의 자동 strict mode" /> vite는 <u>**라이브러리와 같이 그 내용이 바뀌지 않을 소스코드는 esbuild를 이용해 사전번들링**</u>해 빠른 속도를 취하고, <u>**source-code와 같이 수정이 잦은 코드는 Native ESM을 이용**</u>해 본질적으로 브라우저에게도 번들링 작업 일부를 위임한다. Native ESM을 사용하는 것 자체가 문제가 아니다. 프로젝트 내부에서 `sheetjs-style`라는 오래된 엑셀 라이브러리를 사용중이 었는데. 그 라이브러리가 제공하는 코드에 문제가 있었다. ```javascript var APOS = "'"; QUOTE = '"' ``` 이유는 **Native ESM의 모듈은 내부에서 자동적으로 strict mode를 사용**하기 때문에 문제가됐다. 라이브러리 자체에서 수정되면 참 고맙겠지만 (확인해보니 라이브러리 이슈에도 올라와 있더라..) 업데이트도 없고 이슈pr도 무시되는 상황이라 라이브러리를 `exceljs`로 교체해줌으로써 문제를 해결했다. <HeaderLink text="개발서버는 번들링되지 않는다" /> 위의 설정까지 마친후 처음 로컬서버를 실행했을때 당황스러웠다. 초기 로딩이 느려도 너~무 느렸다. 확인해 보니 엄청난 수의 모듈을 request하고 있었고 좀만 찾아보니 vite는 개발서버를 번들링하지 않는다는 것을 알 수 있었다. ![GATSBY_EMPTY_ALT](./images/lazy_before.png) 모듈이 너무 많다.. 어쩌면 CRA가 개발서버를 번들한탓에 이러한 문제에 대해 생각하지 않았던게 아닐까 싶다. 하지만 지금 수많은 모듈을 개발한 사람은 남아있지도않고, 곧 출시를 앞두고 모듈 정리를하기엔 리스크가 크기에 당장 생각해낸 해결방안은 다음과 같았다. - 배럴파일 정리 - 동적분할로 인해 필요할때만 모듈 불러오기 정말 사용해야 할 곳에서만 배럴파일을 사용하고, 순환참조가 일어나는 부분을 모두 제거한 후 동적분할을 통해 분리해주면 어느정도 잡힐 것이라 판단했다. 가장 큰 원인이 됐던 모듈은 메인 라우터 모듈과 전역 상태를 import해 메인 reducer를 생성하는 모듈이었다. 그도 그럴것이 메인라우터는 모든 페이지를 불러오기 때문에 모든 페이지에 관한 로직을 import할 것이고, 전역상태 역시 마찬가지기 때문이다. 특히 라우터 모듈은 동적분할이 돼있지 않아 메인 로직이 담겨있지 않은 페이지에서도 메인에 관한 모듈을 불러오는 쓸데없는 시간을 소비하고 있었다. ![GATSBY_EMPTY_ALT](./images/lazy_after.png) WoW! 여전히 모듈은 많지만 눈에띄는 성과가 나왔고 덤으로 메인로직을 사용하지 않는 페이지의 초기 로딩속도 역시 눈에띄게 좋아진 것을 발견했다. ![GATSBY_EMPTY_ALT](./images/register_lazy_before.png) ![GATSBY_EMPTY_ALT](./images/register_lazy_after.png) load가 1.53s -> 201ms로 나름의 성과를 거뒀다. 하지만 기뻐하는것도 잠시였다. 동적분할을 적용하니 Suspense로 인한 fallback뷰가 라우팅마다 발생하는 꺼림직한 문제가 발생했다. <HeaderLink text="라우터 Suspense의 fallback뷰가 UX를 떨어트린다.." /> 모듈을 불러오는 동안 fallback뷰가 라우팅마다 보이는건 무시할 수 없는 문제였다.. startTransition을 사용하면 fallback뷰가 보이는걸 막을 수 있지만 사용하고 있는 react-router-dom에서 해당 기능을 지원하냐가 관건이었다. 공식문서에서 찾기는힘들었지만 분명 있을거라 생각해 모든 changelog를 뒤져본 결과 실험적인 기능으로 개발되어 있었고 적용 후 테스트해보니 원하는 결과를 봐서 다행이었다. ```javascript <HistoryRouter future={{ v7_startTransition: true, v7_relativeSplatPath: false }}> <Routes> // 라우터들 </Routes> </HistoryRouter> ``` <HeaderLink text="배포시 route를 동적 import했을때 문제 feat.React.lazy" /> 현재 프로젝트는 haproxy를 이용한 무중단 배포를 진행하고 있다. 하지만 배포환경에서 동적import에 문제가생겼고 이는 꽤 여러사람에게 <OutLink href="https://github.com/vitejs/vite/issues/11804" text="문제" />가 되고있는듯 했다. build시 분리된 chunk가 생성되고 이 chunk에 **코드 내용과 관련된 hash값**이 붙는다. 즉 소스코드가 변하지 않는 모듈은 hash값이 동일하기 때문에 이는 캐싱에 유리한데, 이 바뀐 hash가 약간의 문제를 일으킨다. 예를들어 **/home 경로가 Home.hash.js**를 load할때, 소스코드 변경후 build시 hash값이 변경되고, 더이상 Home.hash.js는 없지만 이를 import하기 때문에 문제가 생긴다. error이벤트에 강제로 새로고침해주는 로직으로 임시로 해결할 수 있다고는 하지만, 프로젝트 특성상 그럴 수 없기때문에 모든 chunk에서 hash를 빼주기로 결정했다. > 정적파일과 entry모듈은 hash를 유지했지만 entry의 hash가 바뀌는경우도 생겨서 안전하게 모든 chunk에 hash를 제거해줬다. ```javascript export default defineConfig(({ mode }) => { return { envPrefix: 'REACT_APP_', build: { rollupOptions: { output: { entryFileNames: '[name].js', chunkFileNames: '[name].js', assetFileNames: 'assets/[name][extname]', }, }, }, }; }); ``` <HeaderLink text="결과" /> > chunk에 hash를 제거하는 찝찝함은 남아있다... ![GATSBY_EMPTY_ALT](./images/build_before_vite.png) ![GATSBY_EMPTY_ALT](./images/build_after_vite.png) 하지만 빌드시간은 3배가량 단축되었고, 기존 CRA는 로컬서버도 번들링되기 때문에 HRM과 서버실행속도가 느렸는데 (특히 node_modules를 지우고 새로 시작했을때는 정~~말 느리다) 지금은 거의 즉시 reload된다.. 아직 멀었지만 그래도 조금이나마 DX가 좋아져서 다행이다 🥲