취미로 하는 개발

HMR의 작은 비밀

개발자의 생산성은 무엇에 달려있을까?

바로 input과 output 간의 이어지는 사이클을 극단적으로 줄여 빠른 피드백을 반복적으로 얻을 수 있는 환경이다.

 

빌드에 2분이 걸리는 프로젝트가 있고, 코드 변경의 결과물을 확인하기 위해서는 꼭 다시 빌드를 해야 한다고 생각해 보자.

내 코드의 변경사항이 어떤 결과로 나타내는지를 알기 위해서는 2분이라는 시간이 필요하다. 만약 실수했다면? 다시 수정 후 빌드를 하는 2분을 기다려야 한다.

 

이 시간들은 당신을 좀 더 여유롭게 살 수 있게 해 줄 수 있을지도 모르지만, 적어도 생산적인 사람으로 만들진 못할 것이다.

 

모든 개발 환경은 이러한 잉여 시간을 줄이고 빠른 피드백을 주기 위해 발전해 왔다. 테스트 코드, HMR(Hot Moudle Replacement), JetPack과 Flutter의 Hot Reload 등등...

 

특히 프론트엔드 개발 생태계에서는 웹팩에서 주도했던 HMR가 큰 영향을 미쳤다.

 

HMR의 기능을 한 문장으로 요약하면 '로컬 파일의 변경사항이 생기면 해당 모듈을 빠르게 교체' 하는 것이다. 

 

내 오픈소스에서도 해당 기능을 아주 살짝? 비슷... 하게 구현하고 있는데, 특정 파일만을 교체하는 것이 아니라 전체 파일을 다시 빌드 후 익스텐션을 새로 로드하는 방식을 사용한다. (그래서 이름도 Hot Rebuild Reload/Refresh인 HRR로 지었다)

 

로컬 파일의 아웃풋이 필요한 크롬 익스텐션의 특성상 이러한 방식으로 구현이 되었는데, vite 크롬 익스텐션 플러그인의 경우 빌트인 개발서버를 후킹 해서 개발서버에서 서빙되는 모듈을 로컬 파일에 그때그때 쓰고 reload 하는 방식을 사용하고 있다. 후덜덜...

 

어쨌거나 이런 이유로 반쪽짜리인 HRR만을 운영하던 내 오픈소스 레포에 이런 이슈가 올라왔다.

https://github.com/Jonghakseo/chrome-extension-boilerplate-react-vite/issues/261

 

manifest.json file listens for compilation issues · Issue #261 · Jonghakseo/chrome-extension-boilerplate-react-vite

The manifest.ts file has added listening, but the contents modified after starting local development are not updated in the compiled dist file It is hoped that the contents of the manifest.ts file ...

github.com

 

내 보일러 플레이트에서는 manifest.ts 파일의 내용을 바탕으로 maifest.json을 생성해 주는데, manifest.ts 파일의 변경사항이 rebuild는 발생시키지만 정작 생성된 manifest.json을 보면 최초의 내용을 그대로 가지고 있어서 아무것도 업데이트되지 않는 문제가 있었다.

 

즉, manifest 파일을 수정하고 그 내용을 반영하려면 pnpm dev를 다시 하고 익스텐션도 수동으로 reload를 해줘야 하는 불편함이 있었다.

 

대체 왜 manifest.ts 파일의 변경사항은 빌드 시에 반영되지 않는 건지 궁금해서 알아보니, 아니 글쎄 한 번 가져온 모듈은 리소스 낭비를 위해 캐싱을 한다는 게 아닌가?

 

NodeJS 공식 문서를 보니 require를 통해 가져온 모듈의 경우 require.cache에 캐싱된 내용이 저장되고, delete require.cache['<moduleName>']을 통해 캐시 무효화를 할 수 있었다.

 

ESM을 사용하는 나의 경우에는 어떤 식으로 해결할 수 있을지 찾아보니 일단 import()를 통한 동적 모듈 로딩은 수동으로 캐시 무효화가 불가능했다.

 

하지만 방법은 있었으니... 바로 모듈 뒤에 쿼리 URL을 붙이는 것이다. 이게 무슨 뜬금없는 소리지?

 

https://ar.al/2021/02/22/cache-busting-in-node.js-dynamic-esm-imports/

 

Cache busting in Node.js dynamic ESM imports

I’m porting JSDB to EcmaScript Modules (ESM) and one of the issues I had to look into was module cache invalidation. JSDB is my little in-memory native JavaScript Database that writes JavaScript operations to append-only JavaScript logs that have UMD hea

ar.al

 

뜬금없다고 생각했으나 공식문서를 살펴보니 정말로 그러한 내용이 있었다.

Modules are loaded multiple times if the import specifier used to resolve them has a different query or fragment.

 

vite에서도 해당 내용이 반영된 소스코드를 찾을 수 있었는데, `t=${timeStamp}` 형태로 cache busting과 해당 모듈이 어떤 시점에 로드되었는지 식별이 가능한 형태로 구현하고 있다는 것을 알게 되었다.

https://github.com/vitejs/vite/blob/main/packages/vite/src/node/server/middlewares/indexHtml.ts#L132

 

결론적으로는 나도 동일한 방식을 통해 구현했는데, manifest 파일의 변경 시 rebuild를 하게 되면, 캐시가 무효화된 manifest.js를 가져와 그때그때 새로 manifest.json을 생성하도록 했다.

 

결과적으로 위 이슈를 해결하면서 배운 것들은 다음과 같다.

 

1. require, import와 같이 JS에서의 모듈 로드 시스템은 최적화를 위해서 기존 모듈을 캐싱한다.

2. require의 경우 직접 캐시 객체를 수정하여 cache bursting을 달성한다.

3. import의 경우는 직접 캐시 객체를 비울 수 없어 모듈 이름 뒤의 인자를 수정해줘야 한다. 이 경우 모듈을 다시 로드한다.

4. windows 환경에서는 import() 인자에 file schema가 강제된다. 따라서 url.pathToFileURL 사용이 강제된다.

 

사실 이슈를 봤을 때 이렇게 재밌는 사실을 많이 알게 될 줄은 몰랐는데, 참 이런 경험이 새로운 것들을 재미있고 자연스럽게 익힐 수 있는 계기인 것 같아 기록용으로 남겨준다.

 

 

 

 

enhance: Modify to let the plugin do the build completion detection not file system. by Jonghakseo · Pull Request #265 · Jongh

Fixed #184

github.com

 

 

fix: windows dynamic import protocol issue by Jonghakseo · Pull Request #268 · Jonghakseo/chrome-extension-boilerplate-react-v

Fixed #266 (comment)

github.com