[DevOps] 컴퓨터야, 순환 참조 좀 알아서 찾아주면 안 되겠니?
순환참조 알잘딱!
[ AI_Todo 프로젝트 개발기 - DevOps #2] ESLint와 Husky로 나만의 품질 파이프라인 구축하기
1. 속도는 잡았지만, 실수는 잡지 못했다
지난 글에서 pnpm
과 Turborepo
를 도입해 지루했던 설치와 빌드 시간을 극적으로 단축했습니다.
개발 경험이 쾌적해졌다고 생각했지만, 그건 착각이었습니다.
CI/CD 파이프라인은 여전히 이런 사소한 실수들로 붉은 등을 켜기 일쑤였습니다.
- 제각각인 코드 포맷팅과
import
순서 - 깜빡하고 지우지 않은
console.log()
ESLint
와Prettier
의 미세한 충돌로 인한 예상치 못한 오류- 가장 치명적인, 패키지 간 순환 참조 발생
속도의 파이프라인 뒤에는, 품질의 파이프라인이 반드시 필요.
2. 문제 정의: 버그는 늦게 잡을수록 비싸다
오류는 어느 단계에서 발견되느냐에 따라 처리 비용이 기하급수적으로 달라질 수 있다는 글을 봤습니다.
오류 발견 단계 | 소요 시간 (평균) | 결과 및 비용 |
---|---|---|
커밋 (Commit) 이전 | - | (이상적인 상황) |
CI 파이프라인 | 6 ~ 10분 | 푸시 후 한참 뒤에 인지, 컨텍스트 스위칭 비용 발생 |
배포 (Deploy) 이후 | 수 시간 ~ 수일 | Hotfix, 롤백, 사용자 신뢰도 하락 등 최악의 비용 |
혼자 개발하기에 팀원 리뷰 지연은 없었지만, 문제는 다른 곳에 있었습니다.
정신 없을 때 미처 확인하지 못한 커밋 하나가 훗날 거대한 버그로 돌아왔을 때, 원인이 된 커밋을 찾아 헤매는 시간은 상상 이상의 비용이었습니다.
이를 해결하기 위한 가장 좋은 가성비는 변경점이 있어 저장하려고 commit을 할 때 바로 그 순간에 오류를 발견하는 것!!
3. 품질 파이프라인 구축: ESLint와 Husky라는 두 개의 기둥
이 목표를 달성하기 위해, 저는 두 가지 도구를 선택했습니다.
프로젝트의 ‘법전’을 만들 ESLint 와, 그 법을 집행할 ‘문지기’ Husky 였습니다.
1단계: ESLint로 프로젝트의 ‘법전’ 만들기
단순한 린터를 넘어, 프로젝트의 아키텍처 규칙까지 강제하는 것이 목표!
- Flat Config (
eslint.config.js
) 도입:- 여러
.eslintrc.*
파일로 흩어져 있던 설정을 단일 ESM 파일로 통합했고 이는 설정의 일관성과 재사용성을 높이는 최신 방식!
- 여러
- 아키텍처 순수성 강제:
no-restricted-imports
규칙은 이 법전의 핵심 조항!, 예를 들어, 공용 로직인core
패키지가 특정 앱인apps/web
을 참조하는 것은 설계 원칙에 어긋남 -> 이런 실수를 원천 차단하기!
// eslint.config.js 일부
// ...
{
rules: {
"no-restricted-imports": [
"error",
{
"name": "@apps/web",
"message": "Core 패키지는 특정 App을 직접 참조할 수 없습니다."
}
]
}
}
- 성능 최적화:
Turborepo
의 캐싱 기능을 활용 -> ESLint 설정 파일이나 플러그인 버전이 바뀔 때만 캐시를 무효화하고, 변경된 패키지만 린트를 실행하도록 구성!
2단계: Husky와 lint-staged로 ‘첫 번째 검문소’ 세우기
아무리 훌륭한 법전도, 아무도 읽지 않으면 무용지물…
Husky
는 Git의 각 이벤트(commit, push 등)에 갈고리를 걸어, 이 법전을 강제로 실행시키는 역할을 만들었습니다.
# .husky/pre-commit
#!/bin/sh
# git commit 실행 직전에, 아래 명령어를 실행한다.
pnpm exec lint-staged
여기서 핵심은 lint-staged
입니다.
pre-commit
단계에서 프로젝트 전체를 린트하면 커밋 한 번에 몇 분씩 걸릴 수도 있지만 lint-staged
는 Git에 스테이징(Staging)된 파일만 을 대상으로 린트와 포맷팅을 실행할 수 있습니다.
// package.json 일부
"lint-staged": {
"**/*.{ts,tsx}": [
"eslint --fix",
"prettier --write"
]
}
덕분에 커밋에 걸리는 시간은 1~2초 내외로 유지되면서도, 모든 코드가 일관된 품질을 유지할 수 있었습니다. (1~2초가 생각보다 좀 답답하지만 버그방지 적금이라 생각하면 나름 합리적이라고 생각합니다)
여기서 더 나아가, 다른 Git 단계에도 검문소를 추가로 설치했습니다.
Git 훅 | 실행 작업 | 목 표 |
---|---|---|
pre-push | pnpm turbo run test | 테스트를 통과하지 못한 코드가 원격 저장소로 올라가는 것을 방지 |
commit-msg | pnpm exec commitlint | feat:, fix: 와 같은 Conventional Commit 규칙을 강제하여 커밋 히스토리 가독성 확보 |
4. 자동화된 시스템을 향한 여정
- ESLint는 프로젝트의 ‘법전’을 정의
- Husky는 그 법을 커밋과 푸시라는 ‘관문’에서 집행하는 경찰
- lint-staged는 모든 사람을 검문하는 대신, ‘용의자(변경된 파일)’만 심문하여 속도와 안전을 모두 잡는 현명한 수사관
이제 CI 파이프라인의 빨간불을 보기 전에, 내 컴퓨터가 먼저 실수를 자동으로 잡도록 만들어 혼자 개발하면서 할 수 있는 실수에 대해 단순히 시간을 아끼는 것을 넘어, 리듬을 깨지 않고 온전히 개발에만 집중할 수 있게 해주는 강력한 무기를 만들었습니다.
하지만 이 평화는 길지 않았습니다… 하드웨어를 교체하며 OS를 재설치하는 과정에서, 전혀 예상치 못한 ‘진짜 최종 보스’ 가 나타났기 때문입니다…
5. 진짜 최종 보스는 따로 있었다: OS라는 거대한 벽
CPU 를 Intel 에서 Ryzen 으로 바꾸면서 Windows 를 포맷해야 했는데 어찌 된 일인지, 전보다 개발 환경이 더 느려진 기분이 들었습니다. “차라리 Linux 로 갈까?” 고민도 했지만, 예를 들어 카카오톡 같은 프로그램을 wine
으로 설치하며 시간을 쓰고 싶진 않았습니다.
그러다 문득, 존재를 잊고 있던 WSL(Windows Subsystem for Linux) 을 떠올렸고 반신반의하며 프로젝트를 WSL 환경으로 옮겼는데, 결과는 충격적….
- Spring 프로젝트의 컴파일 시간
- Windows 네이티브에서 14 초 -> WSL에서는 단 7 초
엄청난 성능 향상에 환호했지만, 기쁨은 잠시였을뿐…. OS 환경이 바뀌어도 백엔드는 별 문제 없었지만 문제는 프론트에서 애써 구축해 둔 Husky
훅이 온갖 권한 오류를 뿜으며 동작하지 않았습니다…
결국, 성능향상을 노리다가 또 다른 문제가 나타났고 이런 자잘한 문제해결에 시간을 쓰는게 아까웠습니다.
다시 이전환경으로 되돌려야 되나 생각했지만 저의 게으름을 컴퓨터에 자동화 시키는 게 인생 모토이기 때문에
“어떻게 하면 여러 OS 환경에서 고생 안하고 한 번에 일관되게 관리할 수 있을까?”
를 고민했습니다.
이 문제를 해결하는 과정에서, 저는 mise
라는 새로운 도구를 만나게 되었고, 그 이야기는 다음 편에서 계속됩니다.
댓글남기기