3 분 소요

순환참조 알잘딱!

[ AI_Todo 프로젝트 개발기 - DevOps #2] ESLint와 Husky로 나만의 품질 파이프라인 구축하기

1. 속도는 잡았지만, 실수는 잡지 못했다

지난 글에서 pnpmTurborepo를 도입해 지루했던 설치와 빌드 시간을 극적으로 단축했습니다.
개발 경험이 쾌적해졌다고 생각했지만, 그건 착각이었습니다.
CI/CD 파이프라인은 여전히 이런 사소한 실수들로 붉은 등을 켜기 일쑤였습니다.

  • 제각각인 코드 포맷팅과 import 순서
  • 깜빡하고 지우지 않은 console.log()
  • ESLintPrettier의 미세한 충돌로 인한 예상치 못한 오류
  • 가장 치명적인, 패키지 간 순환 참조 발생

속도의 파이프라인 뒤에는, 품질의 파이프라인이 반드시 필요.

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-stagedGit에 스테이징(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라는 거대한 벽

CPUIntel 에서 Ryzen 으로 바꾸면서 Windows 를 포맷해야 했는데 어찌 된 일인지, 전보다 개발 환경이 더 느려진 기분이 들었습니다. “차라리 Linux 로 갈까?” 고민도 했지만, 예를 들어 카카오톡 같은 프로그램을 wine으로 설치하며 시간을 쓰고 싶진 않았습니다.

그러다 문득, 존재를 잊고 있던 WSL(Windows Subsystem for Linux) 을 떠올렸고 반신반의하며 프로젝트를 WSL 환경으로 옮겼는데, 결과는 충격적….

  • Spring 프로젝트의 컴파일 시간
    • Windows 네이티브에서 14 초 -> WSL에서는 단 7 초

엄청난 성능 향상에 환호했지만, 기쁨은 잠시였을뿐…. OS 환경이 바뀌어도 백엔드는 별 문제 없었지만 문제는 프론트에서 애써 구축해 둔 Husky 훅이 온갖 권한 오류를 뿜으며 동작하지 않았습니다…
결국, 성능향상을 노리다가 또 다른 문제가 나타났고 이런 자잘한 문제해결에 시간을 쓰는게 아까웠습니다.
다시 이전환경으로 되돌려야 되나 생각했지만 저의 게으름을 컴퓨터에 자동화 시키는 게 인생 모토이기 때문에

“어떻게 하면 여러 OS 환경에서 고생 안하고 한 번에 일관되게 관리할 수 있을까?”

를 고민했습니다.
이 문제를 해결하는 과정에서, 저는 mise라는 새로운 도구를 만나게 되었고, 그 이야기는 다음 편에서 계속됩니다.

댓글남기기