지피지기 백전백퇴

Webpack 번들 거꾸로 뜯어보기


시작

어디선가 출처가 의심스러운 server.js 파일을 하나 얻게 되었다고 쳐 보자. 딱히 minified되어 있지는 않지만 한자릿수 메가바이트 사이즈의 부담스러운 크기.

이 서버를 public하게 열고 싶지 않은데 0.0.0.0으로 listen하고 있어서 부담스럽다. 홈 서버에서 ssh tunneling을 통해서만 접근하도록 하고 싶으니 bind address를 localhost로 변경하고 싶다면 어떻게 해야 하나?

여기서 잠깐, 사실은 node로 실행할 때 —require를 통해 hook을 붙일 수 있고, 그걸 통해서 localhost에서만 접속가능하도록 할 수 있습니다만, 이 글에서 원하는 주제가 아니기 때문에 다루지 않습니다.

여기서 잠깐, 사실 예전엔 docker 컨테이너에 넣고 bind하는 방식으로 localhost bind를 구축했던 적도 있습니다만, 이건 docker 컨테이너를 구동하는 비용이 있어서 다루지 않습니다.

여기서 잠깐, 그냥 claude에게 ‘해줘’하면 이 server.js를 적당히 주물러서 localhost bind를 구축할 수도 있겠습니다. 의외로 빠르게 찾을지도? 시도해보진 않았지만 어렵지 않게 가능할 것도 같습니다. 다만 다루지 않습니다.

그냥 webpack이 패키징한 개별 모듈을 모두 역산해서 원래 프로젝트의 형태를 더듬어보자는 게 이번 글의 목적이 되겠다.

대략의 요점정리

원래는 각 스텝의 프롬프트를 모두 정리하는 글로 작성하려고 했는데, 막상 작성하다 보니 글의 길이가 평소보다 아주 길어져서… 적당히 요약해본다.

  1. 모듈 단위로 펼치기 - webpack으로 번들링된 패키지의 경우 내부적으로 모듈이 있고 그것들을 __webpack_require__라는 자체 함수로 관리하는 방식이다. 이 모듈들을 각각 별도의 파일로 분리하도록 시킬 수 있다.
  2. npm package 찾기 - 몇몇 모듈은 사실 오픈소스 npm 패키지에서 패키징되었을 거라고 추측할 수 있다. 또 운좋게도 package.json으로 추측되는 파일을 모듈 중 하나에서 발견할 수 있었다. 거기서 npm 패키지 목록을 가져다 매칭을 시켜서 상당수의 모듈이 사실은 그냥 npm 패키지라는 것을 발견할 수 있었다.
  3. 매칭된 모듈들 치환 - module.exports = require(...); - 참 쉽죠?
  4. 안쓰는 모듈 제거 - 이를테면 mkdirp라는 npm 모듈을 하나 발견해서 치환했다고 치면 mkdirp의 라이브러리 코드는 이제 아무도 안쓰는 dangling code가 된다. 자체 require의 연관관계를 찾아 그런 파일들을 제거하도록 할 수 있다.
  5. transitive dependency 발견 - 남은 모듈이 너무 많아서 눈으로 좀 보니 몇몇은 여전히 오픈소스 패키지인데 app package에서 의존하는 모듈은 아닌 경우가 있었다. app package에서 또다른 private package를 의존하고, 그 private package에서 설정한 의존성인 것으로 판단, private package를 식별하는 방향으로 작업해 봤다.
  • 의존성 그래프 탐색: app package는 private package의 entrypoint 모듈만 의존하고, 그 뒤의 의존성들은 직접 접근하지 않을 거라고 가정했다. 이런 가정 하에서는 private package의 dependency graph는 entrypoint를 sub-root로 하는 connected component가 될 것이다.
  • 뭐 대단한 알고리즘을 쓰진 못하고, 오픈소스인 것 같은 패키지 - app package의 entrypoint로부터 해당 오픈소스까지의 dependency tree 탐색 - 대충 모듈인 것 같은;; 녀석을 손으로 몇개 분류해봄, 의 과정을 거쳐 private package 후보를 탐색했다.
  • 사실은 애매…한 녀석이 몇개 나와서, 그것들은 그냥 common이란 이름의 private package를 만들고 짬때렸다.
  1. 이와같이 여러개의 패키지가 공존해야 하므로, pnpm workspace로 변경하고 app package와 내부 의존성 패키지들로 정리했다.
  2. private package 내에서 여전히 webpack 시절의 가짜 require를 쓰는 코드들을 찾아 node의 require를 쓰도록 정리한다.

결론

여전히 구동하지만 더 알기쉬운 코드가 되어, 이제 app.listen을 찾아 127.0.0.1을 추가해줄 수 있었다. 만세