🐶 Husky 물론 멍뭉이얘기는 아닙니다. 하지만!
조내일 @tomorrowcho@hackers.pub
시작하며
사이드프로젝트를 시작하면서 Husky를 사용하게 되었습니다. 저는 이름만 들어봤을 뿐, 왜 필요하고 무엇을 할 수 있는지 잘 모르는 상태였습니다. 게다가 '왜 하필 멍뭉이 품종이 이름일까?'라는 호기심도 생겨, Husky의 배경부터 사용법까지 자세히 알아보았습니다.
Husky는 Git Hooks의 복잡한 설정 문제를 해결하기 위해 개발된 도구입니다.
배경
개발된 이유
Git은 기본적으로 .git/hooks 디렉토리에서 훅을 관리하지만, 이 설정 방식에는 몇 가지 문제가 있었습니다. Git hooks 설정이 까다롭고 번거로우며, 팀 협업 시 모든 팀원이 수동으로 사전 설정을 해야만 훅이 실행됩니다. .git 폴더는 Git에 의해 무시되기 때문에 훅 설정을 팀원들과 공유할 수 없었고, 누군가 실수로 설정을 하지 않으면 훅이 작동하지 않는 문제가 발생했습니다.
해결한 문제
Husky는 이러한 문제를 npm 패키지로 해결했습니다. npm install 과정에서 자동으로 Git hooks 설정을 적용하여, 팀원들이 별도의 설정 없이도 동일한 훅을 사용할 수 있게 만들었습니다. prepare 스크립트를 활용해 레포지토리를 클론받고 의존성을 설치하면 자동으로 .git/config에 hooksPath = .husky/_가 추가되어 모든 팀원에게 일관된 Git hooks 환경을 제공합니다.
실제 도입 배경 사례
개발팀들이 Husky를 도입하는 주요 이유는 배포 전 빌드 실패 방지입니다. GitHub Actions 등으로 배포할 때 린트 오류나 타입 에러로 빌드가 실패하는 경우가 많았는데, 개발자들이 로컬에서 사전 검증을 깜빡하고 푸시하는 일이 빈번했습니다. Husky를 사용하면 커밋 전에 자동으로 코드 검사를 실행하여 이런 실수를 방지할 수 있습니다.
그런데 왜 이름이 허스키일까요?
Husky라는 이름은 Git hooks의 "hook(갈고리)"과 개의 특성을 연결한 것으로 보입니다.
이름의 의미
공식 GitHub 저장소 설명이 "Git hooks made easy 🐶 woof!"로 되어 있고, 예제 코드에서도 "Hello Gabia, woof!"처럼 짖는 소리를 사용하는 것으로 보아 멍뭉이를 모티브로 한 것이 분명합니다. Hook은 "갈고리"라는 의미로, Git의 특정 시점에 코드를 "낚아채서" 검사하는 개념입니다.
허스키의 특징과 연관성
허스키는 충실하고 근면한 멍뭉이로 알려져 있습니다. 마치 허스키가 주인을 위해 열심히 일하듯, Husky 도구 역시 개발자를 위해 묵묵히 코드 품질을 지켜주는 '충실한 파수꾼' 역할을 합니다. 커밋이나 푸시 같은 Git 이벤트가 발생할 때마다 자동으로 코드를 검사하고, 문제가 있으면 (개가 짖듯이) 경고를 보내고 커밋 자체를 중단시켜 배포 전에 문제를 방지합니다.
허스키 개가 신뢰할 수 있는 동반자인 것처럼, Husky 도구도 개발자가 실수로 잘못된 코드를 커밋하지 않도록 항상 옆에서 지켜보는 역할을 한다는 점에서 적절한 이름 선택이라고 볼 수 있습니다.
Husky 적용 가이드
1. 필요 패키지 설치
가장 먼저 husky, lint-staged, 그리고 ESLint 설정에 필요한 패키지들을 개발 의존성(-D)으로 설치합니다.
npm install -D husky lint-staged eslint-plugin-filenames
레퍼런스로 참고한 ESLint 설정에 filenames/match-regex 규칙이 있어 eslint-plugin-filenames를 추가했습니다.
2. Husky 설정
아래 명령어를 실행하여 Husky를 초기화합니다. 이 명령어는 프로젝트 루트에 .husky 디렉토리를 생성하고 기본적인 설정을 추가합니다.
npx husky init
이제 **커밋을 시도하기 전(pre-commit)**에 lint-staged가 실행되도록 훅(hook)을 추가합니다.
npx husky set .husky/pre-commit "npx lint-staged"
이 명령어를 실행하면 .husky/pre-commit 파일이 생성되고, 그 안에 npx lint-staged 명령어가 자동으로 입력됩니다.
3. lint-staged 설정
lint-staged는 Git의 **스테이징 영역(staging area)**에 올라온 파일에 대해서만 특정 명령을 실행하게 해주는 아주 유용한 도구입니다. 프로젝트 루트에 .lintstagedrc.js 파일을 생성하고 아래 내용을 추가하세요.
// .lintstagedrc.js
module.exports = {
// TypeScript, JavaScript 파일에 대해 Prettier 포맷팅과 ESLint 검사를 실행
'**/*.{ts,tsx,js,jsx}': [
'prettier --write',
'eslint --fix'
],
// 기타 파일(JSON, Markdown 등)에 대해서는 Prettier 포맷팅만 실행
'**/*.{json,md,yml}': [
'prettier --write'
],
};
여기서 중요한 점
-
prettier --write: 먼저 Prettier가 코드를 예쁘게 정리합니다. -
eslint --fix: 그 다음에 ESLint가 코드의 잠재적인 오류를 찾아 수정합니다.
이 순서가 중요합니다. Prettier가 코드 서식을 먼저 맞춘 후, ESLint가 서식 외의 로직/스타일 오류를 검사하고 수정해야 서로 충돌하지 않습니다.
이렇게 두 단계를 거치면, 코드는 항상 일관된 스타일과 좋은 품질을 유지하게 됩니다.
4. ESLint 및 Prettier 설정 파일 생성
Prettier와 ESLint 설정은 프로젝트의 일관성을 위해 매우 중요합니다. 제가 사용한 전체 설정 파일을 공유합니다.
📂 .prettierrc.js (클릭하여 펼치기)
{
module.exports = {
parser: "@typescript-eslint/parser",
extends: [
"eslint:recommended",
"plugin:@typescript-eslint/recommended",
"plugin:react/recommended",
"plugin:react-hooks/recommended",
"plugin:jsx-a11y/recommended",
"plugin:@next/next/recommended", // Next.js 추천 규칙
"prettier", // ✨ Prettier와 충돌하는 ESLint 규칙을 비활성화하므로 반드시 마지막에 와야 합니다.
],
plugins: ["@typescript-eslint", "react", "react-hooks", "jsx-a11y", "import", "filenames"],
rules: {
// --- 파일 및 네이밍 규칙 ---
"filenames/match-regex": ["error", "^[a-z-.@]+$", true], // 파일명은 소문자, 하이픈(-), 마침표(.)만 허용
"react/jsx-pascal-case": "error", // 컴포넌트 이름은 PascalCase로 작성
camelcase: ["error", { properties: "never" }], // 변수명은 camelCase로 작성
"react/jsx-handler-names": [
// 이벤트 핸들러 네이밍 규칙
"error",
{
eventHandlerPrefix: "handle",
eventHandlerPropPrefix: "on",
},
],
"@typescript-eslint/naming-convention": [
// 변수 타입별 네이밍 규칙
"error",
{
selector: "variable",
types: ["boolean"],
format: ["PascalCase"],
prefix: ["is", "has", "should"],
},
{
selector: "interface",
format: ["PascalCase"],
prefix: ["I"],
},
{
selector: "typeAlias",
format: ["PascalCase"],
suffix: ["Type"],
},
],
// --- 코드 스타일 규칙 ---
"react/destructuring-assignment": ["error", "always"], // props는 항상 구조 분해 할당 사용
"arrow-body-style": ["error", "as-needed"], // 화살표 함수 본문은 필요할 때만 중괄호 사용
// --- TypeScript 관련 규칙 ---
"@typescript-eslint/no-explicit-any": "warn", // 'any' 타입 사용 시 경고
"@typescript-eslint/explicit-function-return-type": "off", // 함수의 반환 타입 명시를 강제하지 않음
"@typescript-eslint/no-unused-vars": ["warn", { argsIgnorePattern: "^_" }], // 사용하지 않는 변수 경고
// --- React/JSX 관련 규칙 ---
"react/function-component-definition": [
// 컴포넌트 정의는 화살표 함수만 사용
2,
{
namedComponents: "arrow-function",
unnamedComponents: "arrow-function",
},
],
"react/jsx-key": ["error", { checkFragmentShorthand: true }], // map 사용 시 key 속성 강제
"react/jsx-no-useless-fragment": "error", // 불필요한 Fragment 사용 금지
"react/react-in-jsx-scope": "off", // Next.js에서는 React를 import할 필요 없음
// --- 기타 규칙 ---
"no-console": ["warn", { allow: ["warn", "error"] }], // console.log 사용 시 경고
"react/prop-types": "off", // TypeScript를 사용하므로 prop-types는 필요 없음
},
settings: {
react: {
version: "detect", // 설치된 React 버전을 자동으로 감지
},
"import/resolver": {
typescript: {}, // TypeScript 경로 별칭(@/components 등)을 인식하도록 설정
},
},
}
};
📂 .eslintrc.js (클릭하여 펼치기)
// .eslintrc.js
module.exports = {
parser: '@typescript-eslint/parser',
extends: [
'eslint:recommended',
'plugin:@typescript-eslint/recommended',
'plugin:react/recommended',
'plugin:react-hooks/recommended',
'plugin:jsx-a11y/recommended',
'plugin:@next/next/recommended', // Next.js 추천 규칙
'prettier', // ✨ Prettier와 충돌하는 ESLint 규칙을 비활성화하므로 반드시 마지막에 와야 합니다.
],
plugins: ['@typescript-eslint', 'react', 'react-hooks', 'jsx-a11y', 'import', 'filenames'],
rules: {
// --- 파일 및 네이밍 규칙 ---
'filenames/match-regex': ['error', '^[a-z-.@]+$', true], // 파일명은 소문자, 하이픈(-), 마침표(.)만 허용
'react/jsx-pascal-case': 'error', // 컴포넌트 이름은 PascalCase로 작성
'camelcase': ['error', { properties: 'never' }], // 변수명은 camelCase로 작성
'react/jsx-handler-names': [
// 이벤트 핸들러 네이밍 규칙
'error',
{
eventHandlerPrefix: 'handle',
eventHandlerPropPrefix: 'on',
},
],
'@typescript-eslint/naming-convention': [
// 변수 타입별 네이밍 규칙
'error',
{
selector: 'variable',
types: ['boolean'],
format: ['PascalCase'],
prefix: ['is', 'has', 'should'],
},
{
selector: 'interface',
format: ['PascalCase'],
prefix: ['I'],
},
{
selector: 'typeAlias',
format: ['PascalCase'],
suffix: ['Type'],
},
],
// --- 코드 스타일 규칙 ---
'react/destructuring-assignment': ['error', 'always'], // props는 항상 구조 분해 할당 사용
'arrow-body-style': ['error', 'as-needed'], // 화살표 함수 본문은 필요할 때만 중괄호 사용
// --- TypeScript 관련 규칙 ---
'@typescript-eslint/no-explicit-any': 'warn', // 'any' 타입 사용 시 경고
'@typescript-eslint/explicit-function-return-type': 'off', // 함수의 반환 타입 명시를 강제하지 않음
'@typescript-eslint/no-unused-vars': ['warn', { argsIgnorePattern: '^_' }], // 사용하지 않는 변수 경고
// --- React/JSX 관련 규칙 ---
'react/function-component-definition': [
// 컴포넌트 정의는 화살표 함수만 사용
2,
{
namedComponents: 'arrow-function',
unnamedComponents: 'arrow-function',
},
],
'react/jsx-key': ['error', { checkFragmentShorthand: true }], // map 사용 시 key 속성 강제
'react/jsx-no-useless-fragment': 'error', // 불필요한 Fragment 사용 금지
'react/react-in-jsx-scope': 'off', // Next.js에서는 React를 import할 필요 없음
// --- 기타 규칙 ---
'no-console': ['warn', { allow: ['warn', 'error'] }], // console.log 사용 시 경고
'react/prop-types': 'off', // TypeScript를 사용하므로 prop-types는 필요 없음
},
settings: {
react: {
version: 'detect', // 설치된 React 버전을 자동으로 감지
},
'import/resolver': {
typescript: {}, // TypeScript 경로 별칭(@/components 등)을 인식하도록 설정
},
},
};
5. package.json 스크립트 확인 및 추가
npx husky init 명령을 실행하면 prepare 스크립트가 자동으로 추가됩니다. 이 스크립트는 다른 팀원이 npm install을 실행할 때마다 Husky가 자동으로 설치되도록 보장하는 필수 스크립트입니다.
추가로, 수동 검사를 위한 format 및 lint 스크립트(선택 사항)도 함께 구성하면 package.json의 scripts 객체는 다음과 같은 형태가 됩니다.
// package.json
"scripts": {
// ... 기존 스크립트들
"prepare": "husky install", // ⬅️ npx husky init으로 자동 추가됨 (필수)
"format": "prettier --write \"**/*.{ts,tsx,js,jsx,json,md}\"", // (선택 사항)
"lint": "eslint . --ext .ts,.tsx --fix" // (선택 사항)
},
6. 팀 협업을 위한 팁: 팀원들은 어떻게 적용하나요?
이 Husky 설정을 프로젝트에 도입하고 나면, 다른 팀원들은 어떻게 적용해야 할까요?
1. 🆕 프로젝트를 새로 클론(clone)받는 경우
매우 간단합니다. prepare 스크립트 덕분에, 터미널에 npm install 명령어 하나만 실행하면 Husky가 자동으로 설치되고 훅(hook)까지 모두 활성화됩니다.
2. 🔄 기존 프로젝트에서 이 설정을 풀(pull)받는 경우
이미 node_modules가 있는 기존 팀원들은 git pull로 변경사항을 받은 후, 다음 두 명령어를 순서대로 실행하는 것이 가장 안전합니다.
# 1. husky 등 새 패키지 설치
npm install
# 2. 훅(Hook) 활성화 보장
npm run prepare
npm install 시 prepare 스크립트가 자동으로 실행되는 것이 정상이지만, 로컬 환경에 따라 누락되는 경우가 간혹 있습니다. npm run prepare를 한 번 더 실행해 주면 만일의 경우에도 훅이 확실하게 적용됩니다.
전체 동작 흐름 요약
이제 모든 설정이 완료되었습니다! 앞으로의 개발 과정은 다음과 같이 진행됩니다.
-
개발자가 코드를 수정합니다.
-
git add <수정한 파일>명령어로 변경된 파일을 스테이징합니다. -
git commit -m "커밋 메시지"명령어로 커밋을 시도합니다. -
이때 Husky가
pre-commit훅을 실행시킵니다. -
pre-commit훅에 등록된 **lint-staged**가 실행됩니다. -
lint-staged는 스테이징된 파일들 중.lintstagedrc.js에 설정된 패턴과 일치하는 파일들을 찾아 Prettier와 ESLint를 순서대로 실행합니다. -
자동 수정 가능한 오류들은 모두 수정되고, 수정할 수 없는 오류가 없다면 커밋이 성공적으로 완료됩니다. 만약 오류가 남아있다면 커밋이 중단됩니다.
이제 팀원 모두가 일관된 코드 스타일을 유지하며 잠재적인 버그를 사전에 방지할 수 있게 되었습니다.
실제 테스트 시나리오
새로운 시스템이 어떻게 작동하는지 아래 시나리오를 통해 직접 테스트해 보았습니다.
시나리오 1. Prettier 자동 수정 (성공 케이스) ✅
코드 스타일이 잘못되었을 때, 커밋 시 자동으로 수정되는지 확인합니다.
액션(Action): test-husky.ts 파일에 일부러 세미콜론을 빼고 들여쓰기를 엉망으로 만든 아래 코드를 붙여넣습니다.
// test-husky.ts
function testPrettier ( name:string ) {
console.log("Hello, " + name)
}
커밋 실행: 터미널에서 아래 명령어를 실행합니다.
git add test-husky.ts
git commit -m "test: Prettier 자동 수정 테스트"
결과(Result): 커밋이 성공적으로 완료됩니다. 그리고 test-husky.ts 파일을 다시 열어보면, 아래와 같이 코드가 자동으로 깔끔하게 정리된 것을 확인할 수 있습니다.
시나리오 2. ESLint 커밋 방지 (실패 케이스) ❌
심각한 규칙 위반 시, 커밋을 막는지 확인합니다.
액션(Action): test-husky.tsx 파일에 .map()을 사용하면서 고의적으로 key prop을 누락시킨 아래 코드를 붙여넣습니다.
// test-husky.tsx
const TestComponent = () => {
const items = [1, 2, 3];
// ESLint 오류 발생 지점: .map() 사용 시 key가 없음
return <div>{items.map((item) => <span>{item}</span>)}</div>;
};
export default TestComponent;
커밋 실행: 터미널에서 아래 명령어를 실행합니다.
git add test-husky.tsx
git commit -m "test: ESLint 커밋 방지 테스트"
결과(Result): 커밋이 실패하며, 터미널에 아래와 같이 어떤 규칙을 위반했는지 알려주는 오류 메시지가 나타납니다.
이 테스트를 통해 Husky와 lint-staged가 사소한 스타일 실수는 자동으로 수정해 주고, 심각한 버그 가능성이 있는 코드는 커밋 자체를 막아주어 코드 품질을 효과적으로 지켜주는 것을 확인할 수 있습니다.
마무리
코드 컨벤션은 팀 프로젝트의 생산성과 직결되는 중요한 약속입니다. 하지만 이 약속을 매번 사람이 직접 기억하고 지키기란 쉽지 않습니다. Husky와 lint-staged를 이용한 자동화 시스템은 바로 이 문제를 해결해 주는 가장 확실한 방법입니다.
이제 Husky를 적용한 프로젝트는 누가 어떤 환경에서 코드를 작성하든, 커밋되는 순간 하나의 통일된 규칙을 따르게 됩니다. 이는 코드 리뷰의 본질(로직과 아키텍처)에 더 집중하게 만들어 줍니다. 잘 갖춰진 시스템 위에서 더 창의적인 결과물이 나온다고 믿으며, 이번 자동화 설정이 그 튼튼한 첫걸음이 되기를 바랍니다.
참고자료
https://github.com/typicode/husky
https://znagadeon.dev/post/automate-lint/
https://janggulu.tistory.com/14