Webpack Persistent Cache

webpack5의 persistent cache에 대해 알아봅시다!

webpack5에 새로운 캐시 기능이 나왔습니다. 파일시스템 즉 하드디스크에 빌드된 결과물을 저장하고, 나중에 다시 빌드할때 캐싱된 빌드 결과물을 재사용 함으로써 빌드 속도를 많이 높여줍니다.

맛보기

첫번째 빌드

두번째 빌드

무려! 10배나 빨라졌습니다! 그리고 모듈에 변한것이 없어서 loader는 어떤 모듈도 처리하지 않았습니다.

persistent cache 적용하기

너무 쉽습니다. 그냥 webpack.config.js 파일에 위와 같이 cache 속성만 하나 추가하면 됩니다.

이렇게 좋은걸 왜 default로 하지 않은것인가?

언제 캐쉬를 무효화 해야하는지 알아서 판단하기가 어렵기 때문입니다.

예를들어

  • loaderplugin을 upgrade했을때
  • 웹팩 설정(webpack.config.js) 파일을 수정했을때
  • 웹팩 설정 파일에서 import하고 있는 다른 파일을 수정했을때
  • 웹팩 설정 파일에서 import하고 있는 다른 라이브러리를 upgrade 했을때
  • build script에 다른 command-line arguments를 넣었을때
  • custom build script를 가지고 있는데, 이 스크립트를 수정했을때

모든 캐쉬를 무효화 해야하는데, 위의 상황들을 웹팩이 알아서 처리하는것은 어렵다고 합니다. 즉, 어떤 파일이 바뀌었을때 캐쉬를 모두 무효화 할지를 저희가 일일이 지정을 해줘야 한다는 의미입니다.

(그런데 어떤 명확한 이유가 없이 그냥 안된다고만 나와 있어서 저도 잘 모르겠습니다.)

cache options

typefilesystem으로 해줘야 persistent caching이 적용 됩니다.

buildDependencies는 webpack이 빌드할때 사용하는 의존성들(loader, plugin, config file…)을 의미하며 동시에 이런 의존성들이 변경되었을때 모든 cache를 무효화 해줍니다.

buildDependencies.defaultWebpack

buildDependencies.defaultWebpack["./node_modules/webpack/lib/"]을 넣어줬습니다. 이것은 ./node_modules/webpack/lib/ 디렉토리 안에 있는 모든 lodaer, plugin, 기타 파일이 변경되었을때 모든 캐시를 무효화 하라는 의미입니다.

defaultWebpack은 기본으로 웹팩에서 설정해 놨기 때문에 따로 이렇게 저희가 기입하지 않아도 됩니다

buildDependencies.config

buildDependencies.config[__filename]을 넣었는데, 이것은 현재 파일을 의미합니다.

다시말해서, 현재 이 config file이 변경되었을때 모든 캐시를 무효화 하라는 의미입니다.

잠깐!! webpackDefault, config는 정해져 있는게 아닙니다!

저도 의문이 들어서 webpack 코드를 살펴보니

타입도 그냥 keystring으로 받고 있을 뿐입니다. 다시말해서,,configconfig22건 상관없다는 의미입니다. 저 key는 그냥 보기 좋으라고 이름을 붙여준 정도라고 생각합니다.

실제로 key를 아무렇게나 지어도 잘 작동 했습니다.

파일이 변경 되었을때?

웹팩은 파일이 변경되었는지 판단하기 위해서 2가지 전략을 사용합니다.

timestamp

어떤 파일이 변경되었을때 해당 파일의 meta data의 timestamp가 변경됩니다. 예를들어 제가 맥북에서 abc.txt파일을 열고 파일을 수정한뒤 저장 버튼을 누르면 timestamp가 변경됩니다.

수정’전’ timestamp
수정’후’ timestamp

주의

timestamp는 파일 내용을 수정하지 않고 그냥 저장만 해도 변경됩니다.

content hash(= hash)

content hash는 파일의 내용물을 바탕으로 hashing알고리즘을 통해 생성된 문자열 입니다. 아래 이미지와 같이 생겼습니다.

timestamp vs content hash

속도

어떤 정보를 더 빠르게 얻을 수 있는가 하면, timestampcontent hash보다 더 빠르게 취득할 수 있는 정보입니다. 왜냐하면 timestamp는 파일의 meta data에서 바로 가져올 수 있는데, content hash는 파일의 내용물을 불러오고 그것을 hashing해야만 얻을 수 있기 때문입니다. 따라서 timestamp가 더 빠릅니다.

안정성

timestamp내용물의 수정 없이 [파일 저장만 해도 바뀌고, git pull할 경우]에도 바뀔 여지가 있습니다. 반면 content hash파일의 내용물을 바탕으로 변경 여부를 확인하기 때문에 더 안정적입니다. 불필요한 재빌드를 하지 않는것이죠.

선택

이렇게 두가지 기준으로 파일의 변경 여부를 파악할 수 있습니다. 그리고 저희는 이 기준을 선택할 수 있습니다. timestamp만 변경 여부의 기준으로 할지, content hash만 할지, 아니면 둘다 기준으로 채택할지 선택할 수 있습니다. 이 설정은 snapshot옵션에서 설정 가능합니다.

snapshot ?

스냅샷하면 사진이 생각 나기도 하고, 또 cloud service(aws, vultr…)를 사용해본 분이시라면 백업도 떠오를 것입니다. persistent cache의 snapshot도 비슷한 의미를 가지고 있습니다. 어떤 파일을 빌드하고 그 빌드된 결과를 캐싱하는데, 그 캐싱된 데이타에 붙어있는 부가 정보가 바로 snapshot입니다.

실제 코드

이런 저런 정보를 막 갖고 있는데, fileTimestampsfileHashes가 눈에 보입니다. 다른 정보는 아주 약간 짐작 가기는 하지만 정확히는 뭔지 모르겠습니다.

빌드할때 snapshot의 모습

보시면 여기도 timestamphash가 있는것을 확인할 수 있습니다.

snapshot option

snapshot.buildDependencies

build dependencies(loader, plugin, config..)에 대한 cache data를 어떤 기준으로 무효화 할것인지 설정합니다.

파일이 변했다고 판단되면, 모든 cache를 무효화 합니다.

timestamp : true

timestamp를 기준으로 파일의 변경 여부를 판단합니다.

hash: true

content hash를 기준으로 파일의 변경 여부를 판단합니다.

timestamp: true & hash: true

둘다 true인 경우는 timestamp먼저 검사하고 이후에 content hash를 검사합니다.

snapshot.module

모듈에 대한 cache data를 어떤 기준으로 무효화 할것인지 설정합니다.

모듈(파일)이 변했다고 판단되면, 해당 모듈에 대한 cache data를 무효화합니다.

snapshot.resolve

resolve라 함은 “webpack이 빌드시에 import문을 만났을때 import하려는 파일의 정확한 위치를 찾는일”을 의미합니다.

resolve랑 캐싱이랑,,,무슨 상관이지?” 라고 생각하실 수 있습니다.

resolve는 webpack 내장 플러그인인 ResolveCachePlugin이 담당을 하는데, 이 플러그인은 webpack5의 persistance cache를 활용해서 파일을 resolve한 결과를 snapshot과 함께 cache에 저장합니다.

먼저 resolve에 성공하면 다음과 같은 resolve result가 생성됩니다

보시면 issuerimport문을 사용한 파일이고, pathimport된 그 모듈이 있는 파일입니다.

그리고 ResolveCachePluginsnapshot을 하나 만들어서 이 resolve result와 함께 묶어서 캐싱합니다.

물론 나중에 이 캐시 데이타를 찾아야 함으로, identifier에 연결시켜 놓습니다.

identifier

이제 re-build를 할때, hello.jsresolve해야될 일이 생기면

이 identifier를 바탕으로 node_modules/.cache/webpack 폴더에서 캐싱된 데이터를찾습니다.

(identifier는 파일 경로로 얻는 동적인 데이타입니다)

그러면 아까 저장했던 resolve result와 snapshot을 가져올 수 있습니다

resolve result
snapshot

이제 이 resolve result(cache data)가 아직 써도 되는 값인지(유효한지) 확인하기 위해서 snapshot을 바탕으로 유효성 검사를 합니다.

그래서 timestamp비교 전략으로 택했다면 hello.jstimestampsnapshottimestamp를 비교해서 다르다면 무효화 하고, 같다면 resolve result를 그대로 사용할것이고, content hash도 동일하게 작동할것입니다.

snapshot.resolveBuildDependencies

build dependencies에 대한 resolve result 캐싱을 어떤 기준으로 무효화 할것인지에 대한 설정입니다.

snapshot.managedPaths

기본값이 잘 설정되어 있습니다만, 알고갈 필요가 있습니다. 퍼포먼스에 중요한 옵션이기 때문입니다.

webpack은 node_modules의 모든 파일에 대해 snapshot을 만들고 그것을 관리하지 않습니다. 왜냐하면 너무 비용이 크고 일반적으로 node_modules안에 있는 라이브러리들의 코드를 직접 수정하는 일은 거의 없기 때문입니다.

그래서 persistent cache를 사용하면, node_modules의 파일들은 timestampcontent hash를 기준으로 무효화 할지 말지 판단 하는것이 아니라, 파일이 아닌 해당 패키지의 버전이나 이름이 변경되었을때 해당 패키지에 대한 캐시를 무효화 합니다.

immutablePaths

immutablePaths는 저희가 yarn을 쓴다면 기본적으로 yarn cache 폴더를 가리키고 있습니다.

yarn berry (pnp version 3)는 라이브러리를 압축파일로 갖고있습니다. 그리고 저희는 이 압축파일을 수정할 일이 없습니다. node_modules는 저희가 접근해서 폴더를 타고 들어가서 파일 내용을 수정할수야 있지만, 이렇게 압축된 파일은 저희가 접근해서 내용물을 수정할 일이 없습니다(불가능은 아니겠지만요!).

그래서 webpack은 yarn cache 폴더의 내용물에 snapshot을 붙이거나, version / name을 전후 비교해서 캐시를 무효화 하지 않습니다. 안바뀌는 파일에 굳이 이런 tracking을 할 필요가 없다는 것이지요.

빌드할때 yarn cache의 압축 파일을 풀어서 라이브러리를 빌드하고 -> 그 빌드된 결과물을 캐싱한뒤, 이후에 쭈우우욱 그 캐싱 데이터를 사용할 뿐입니다.

만약에 라이브러리를 업데이트 했다면?

잘 모르겠습니다만,,,압축 파일이 사라졌고 새로운 압축 파일이 생겼으니까, 전꺼는 무효화 해버리고 새로운 압축 파일을 빌드한뒤 캐싱하지 않을까 싶습니다! 그러니까 snapshot / name / version으로 ‘변했는지’ 판단 하는게 아니라, 그냥 그 파일의 존재유무만 보고 없으면 캐쉬 지우고 있으면 캐싱된거 있는지 확인해서 있으면 쓸것이라 추측됩니다.

상황별 추천 설정

로컬 개발 환경

snapshot.buildDependencies

기본적으로는 timestamp: true + hash: true 이렇게 둘다 하는것이 좋습니다. 아주 가끔 내용물이 안바뀌고 timestamp만 바뀌는 경우를 대비한것입니다. 이 옵션은 모든 캐쉬를 무효화 하기 때문에 이렇게 hash가 오래 걸려도 추가 해주는것이 좋습니다.

snapshot.module

timestamp: true가 빨라서 좋습니다.

간혹 내용물이 안바뀌고 timestamp만 바뀐다 하더라도 큰 비용이 안들기 때문에 지속적으로 느린 content hash를 비교하는것보다 결과적으로 더 효율적일 확률이 높습니다.

snapshot.resolve

timestamp: true로 하는게 좋습니다. resolve도 위와 같은 이유입니다. 여차 하면 캐시 무효화 해도 됩니다. 별로 비용이 안들어 갑니다.

CI Build (with git clone ..)

모든 옵션을 hash: true만 해줍니다. 어차피 git clone해서 새로운 코드들을 싹 가져온다 치면 timestamp는 무조건 바뀔것이기 때문입니다. 그래서 timestamp는 비교할 의미가 없으니까 content hash만 비교 하도록 합니다!

CI Build (with git pull ..)

모두 timestamp: true + hash: true 이렇게 둘다 해줍니다.

git clonetimestamp가 무조건 변할것이기 때문에 content hash만 사용하는게 맞지만, git pull하는 경우에는 그래도 timestamp가 같은 파일이 있을테니까 timestamp 비교를 먼저 해주고, timestamp가 달라도 실제 내용물은 같을수도 있기 때문에 content hash도 추가를 해줍니다.

branch와 cache 무효화 간단 실험!

// main.js (A branch)

import { a, b, c } from "./utils";

a();
b();
c();
// main.js (B branch)

import { a, b } from "./utils";

a();
b();

이런 경우에 branch를 왔다 갔다 하면 main.js파일만 변화가 있다고 판단해서 main.js파일만 빌드를 다시 합니다.

자세한 테스트 내용은 여기서 확인 가능합니다.

결론

persistent cache 너무 좋다!

하지만 알고 써야 한다!

참고 자료

Leave a Reply

Your email address will not be published.