pnpm install을 하고싶지 않은자는 나에게
디펜다봇을 물리쳤더니 갑자기 프로젝트가 켜지지 않았다.

메이저버전 업데이트는 없었는데 왜터지나 보니 rspack에서 마이너 업데이트 주제에 1.3.0 버전으로 올라가면서
ReactRefreshPlugin 이라는 class를 ReactRefreshRspackPlugin 으로 바꿔놨었다.
// v1.2.1
import { type Compiler } from '@rspack/core';
import type { NormalizedPluginOptions, PluginOptions } from './options';
export type { PluginOptions };
declare class ReactRefreshRspackPlugin {
options: NormalizedPluginOptions;
static deprecated_runtimePaths: string[];
static loader: string;
constructor(options?: PluginOptions);
apply(compiler: Compiler): void;
}
export = ReactRefreshRspackPlugin;
// v1.3.0
import type { Compiler } from '@rspack/core';
import type { NormalizedPluginOptions, PluginOptions } from './options';
export type { PluginOptions };
declare class ReactRefreshRspackPlugin {
options: NormalizedPluginOptions;
static deprecated_runtimePaths: string[];
static loader: string;
constructor(options?: PluginOptions);
apply(compiler: Compiler): void;
}
export default ReactRefreshRspackPlugin;
export { ReactRefreshRspackPlugin };
const ReactRefreshPlugin = require('@rspack/plugin-react-refresh'); // ❌
const { ReactRefreshRspackPlugin } = require('@rspack/plugin-react-refresh'); // ✅module export 도 만들어놨길래 export 문으로 config를 수정해 두었는데, 나중에 dev가 안켜진다는 팀원들의 아우성이 많았다.
원인은 두가지였는데,
pnpm install 을 안한사람흠.. 이거 어떻게 못하나?
사실 이 문제의 본질은 간단하다.
“팀원의 로컬 환경과 리모트의 의존성 상태가 어긋나는 것”
그걸 사람의 기억력에 의존하고 있었던 것이 원인이었다.
자동화할 수 있으면 자동화해야 한다. 그리고 이런 종류의 자동화에 가장 적합한 도구가 바로 Git Hook이다.
처음 떠오른 건 당연히 Husky였다. npm 다운로드 수 기준으로 사실상 Git Hook 도구의 표준이나 다름없고, 실제로 서버 레포에서도 이미 husky + lint-staged 조합을 쓰고 있었다.
그런데 막상 웹 프로젝트에 husky를 붙이려고 조금 걸리는 점이 있었다.
Husky v5의 변화로 인한 설정의 직관성이었다.
Husky v4까지는 package.json 에 설정을 넣으면 끝이었다. 그런데 v5부터 완전히 방향이 바뀌었다. .husky/ 디렉토리를 만들고, 훅마다 쉘 스크립트 파일을 하나씩 생성하고, chmod +x로 실행 권한까지 챙겨줘야 한다.
post-checkout, post-merge, post-rebase… 이 세 훅에 똑같은 스크립트를 연결하는 건 사실 아주 단순한 요구사항인데, husky로는 파일이 3개(.husky/post-checkout, .husky/post-merge, .husky/post-rebase) 필요했다. 이게 뭔가 DRY하지 않달까.
그러다 lefthook을 발견했다. Go 기반의 Git Hook 매니저인데, 가볍고 빠르며 무엇보다 설정이 간편하다.
husky 와 lefthook의 비교를 위한 포스팅은 아니므로 더이상 자세한 내용은 생략한다.
깃훅이란 쉽게 말해 깃에서 제공하는 어떤 이벤트가 생겼을때 자동으로 스크립트를 실행할 수 있는 도구이다.
기본적으로 .git/hooks 디렉토리에서 관리되며, husky는 메이저버전 v4 까지는 Node 환경에서 깃훅을 자체적으로 관리했지만 5버전부터 완전히 git에 종속시켰다.
북마크해둔 깃 공식문서에는 커밋 워크플로 훅, 서버 훅, 기타 훅으로 정리해두었는데, 현재 우리 상황에서 필요한 이벤트는 두가지 상황일 것이다.
이와 관련된 깃 훅은 세개다.
post-checkoutpost-merge / post-rebase이 세가지 이벤트가 트리거 될떄, 단순히 pnpm install을 시키면 된다.
하지만 브랜치를 바꿀때마다 pnpm install을 하는건 불필요하다.
이번 상황처럼 디펜다봇을 물리쳤거나, 누군가 작업에 필요한 패키지를 설치했을때만 설치해주는걸로 충분하다.
그리고 그 확인 방법은 pnpm-lock.yaml 이 수행해 줄 수 있다.
우리가 원하는 건 패키지 매니저의 Lock 파일(pnpm-lock.yaml)이 변경되었을 때만 설치를 다시 하는 것이다. 무턱대고 pnpm install을 돌리면 브랜치 바꿀 때마다 몇십초씩 기다려야 하니까.
Lefthook은 .yaml 파일 하나로 Git Hook을 아주 직관적으로 관리할 수 있다.
lefthook.yml 설정프로젝트 루트에 lefthook.yml 파일을 만들고, 아까 말한 3가지 Git Hook(post-checkout, post-merge, post-rebase)에 스크립트를 연결한다.
# lefthook.yml
post-checkout:
scripts:
"install-deps":
runner: bash .lefthook/scripts/install-if-needed.sh
post-merge:
scripts:
"install-deps":
runner: bash .lefthook/scripts/install-if-needed.sh
post-rebase:
scripts:
"install-deps":
runner: bash .lefthook/scripts/install-if-needed.sh
아주 심플하다.
어떤 깃 동작이 발생하든 install-if-needed.sh 라는 쉘 스크립트를 실행하도록 통일했다.
install-if-needed.sh 스크립트이제 핵심 로직을 쉘 스크립트로 짠다.
Git 명령어로 직전 상태와 현재 상태 사이에서 특정 파일이 바뀌었는지 알 수 있다.
#!/bin/bash
# .lefthook/scripts/install-if-needed.sh
# 1. diff-tree 명령어로 변경된 파일 목록을 가져온다.
# HEAD@{1} = 변경 전 커밋(이동 전 브랜치)
# HEAD = 현재 커밋(이동 후 브랜치)
changed_files="$(git diff-tree -r --name-only --no-commit-id HEAD@{1} HEAD)"
# 2. 변경된 파일 중에 'pnpm-lock.yaml'이 있는지 확인한다.
if echo "$changed_files" | grep --quiet "pnpm-lock.yaml"; then
echo "📦 pnpm-lock.yaml changed. Running pnpm install..."
# 3. 변경되었다면 설치 진행!
pnpm install
else
echo "✨ No dependency changes. Skipping install."
fi
이제 팀원들은 아무 생각 없이 git pull이나 git checkout을 해도 된다.
더 이상 “어? 왜 안 켜지지?" 하다가 "아 맞다 인스톨 안 했네" 하며 머리를 긁적일 필요가 없다.
Lefthook은 Husky보다 설정이 훨씬 직관적이고(yaml), 속도도 빠르다.
복잡한 모노레포 환경에서 깃 훅 관리가 고민된다면 강력하게 추천한다.