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

두번째 빌드

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

너무 쉽습니다. 그냥 webpack.config.js
파일에 위와 같이 cache
속성만 하나 추가하면 됩니다.
이렇게 좋은걸 왜 default로 하지 않은것인가?
언제 캐쉬를 무효화 해야하는지 알아서 판단하기가 어렵기 때문입니다.
예를들어
loader
나plugin
을 upgrade했을때- 웹팩 설정(
webpack.config.js
) 파일을 수정했을때 - 웹팩 설정 파일에서
import
하고 있는 다른 파일을 수정했을때 - 웹팩 설정 파일에서
import
하고 있는 다른 라이브러리를 upgrade 했을때 - build script에 다른 command-line arguments를 넣었을때
- custom build script를 가지고 있는데, 이 스크립트를 수정했을때
모든 캐쉬를 무효화 해야하는데, 위의 상황들을 웹팩이 알아서 처리하는것은 어렵다고 합니다. 즉, 어떤 파일이 바뀌었을때 캐쉬를 모두 무효화 할지를 저희가 일일이 지정을 해줘야 한다는 의미입니다.
(그런데 어떤 명확한 이유가 없이 그냥 안된다고만 나와 있어서 저도 잘 모르겠습니다.)
cache options

type
은 filesystem
으로 해줘야 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 코드를 살펴보니

타입도 그냥 key
를 string
으로 받고 있을 뿐입니다. 다시말해서,,config
건 config22
건 상관없다는 의미입니다. 저 key
는 그냥 보기 좋으라고 이름을 붙여준 정도라고 생각합니다.
실제로 key
를 아무렇게나 지어도 잘 작동 했습니다.
파일이 변경 되었을때?
웹팩은 파일이 변경되었는지 판단하기 위해서 2가지 전략을 사용합니다.
timestamp
어떤 파일이 변경되었을때 해당 파일의 meta data의 timestamp
가 변경됩니다. 예를들어 제가 맥북에서 abc.txt
파일을 열고 파일을 수정한뒤 저장 버튼을 누르면 timestamp
가 변경됩니다.


주의
timestamp는 파일 내용을 수정하지 않고 그냥 저장만 해도 변경됩니다.
content hash(= hash)
content hash는 파일의 내용물을 바탕으로 hashing알고리즘을 통해 생성된 문자열 입니다. 아래 이미지와 같이 생겼습니다.

timestamp vs content hash
속도
어떤 정보를 더 빠르게 얻을 수 있는가 하면, timestamp
가 content 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
입니다.

실제 코드

이런 저런 정보를 막 갖고 있는데, fileTimestamps
와 fileHashes
가 눈에 보입니다. 다른 정보는 아주 약간 짐작 가기는 하지만 정확히는 뭔지 모르겠습니다.
빌드할때 snapshot의 모습

보시면 여기도 timestamp
와 hash
가 있는것을 확인할 수 있습니다.
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가 생성됩니다

보시면 issuer
는 import
문을 사용한 파일이고, path
는 import
된 그 모듈이 있는 파일입니다.
그리고 ResolveCachePlugin
은 snapshot
을 하나 만들어서 이 resolve result와 함께 묶어서 캐싱합니다.
물론 나중에 이 캐시 데이타를 찾아야 함으로, identifier에 연결시켜 놓습니다.

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

이 identifier를 바탕으로 node_modules/.cache/webpack
폴더에서 캐싱된 데이터를찾습니다.
(identifier는 파일 경로로 얻는 동적인 데이타입니다)
그러면 아까 저장했던 resolve result와 snapshot을 가져올 수 있습니다


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

그래서 timestamp
를 비교 전략으로 택했다면 hello.js
의 timestamp
과 snapshot
의 timestamp
를 비교해서 다르다면 무효화 하고, 같다면 resolve result를 그대로 사용할것이고, content hash
도 동일하게 작동할것입니다.
snapshot.resolveBuildDependencies
build dependencies에 대한 resolve result 캐싱을 어떤 기준으로 무효화 할것인지에 대한 설정입니다.
snapshot.managedPaths
기본값이 잘 설정되어 있습니다만, 알고갈 필요가 있습니다. 퍼포먼스에 중요한 옵션이기 때문입니다.
webpack은 node_modules
의 모든 파일에 대해 snapshot
을 만들고 그것을 관리하지 않습니다. 왜냐하면 너무 비용이 크고 일반적으로 node_modules
안에 있는 라이브러리들의 코드를 직접 수정하는 일은 거의 없기 때문입니다.
그래서 persistent cache를 사용하면, node_modules
의 파일들은 timestamp
나 content 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 clone
은 timestamp
가 무조건 변할것이기 때문에 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 너무 좋다!
하지만 알고 써야 한다!