AI 코딩 시대에 ESLint와 자동 검사가 더 중요해진 이유

글 썸네일

나는 나이 계산기, 연봉 실수령액 계산기 같은 웹 도구들을 모아 둔 사이트를 운영하고 있다.

광고 수익 기반의 사이트인데, 요즘은 코드 대부분을 AI 에이전트에게 맡겨 개발하고 있다.

얼마 전 에이전트에게 로또 계산기 페이지의 광고 오류를 수정하라고 지시했다.
광고 관련 테스트와 빌드, 배포 전 검사까지 전부 통과했기에 평소처럼 그대로 배포했다.

그런데 사이트 점검 차 실서버를 둘러보던 중 이상한 점을 발견했다.

pSEO(프로그래매틱 SEO)로 구축한 대량 생성 페이지 1만여 장에서
본문 광고가 전혀 노출되지 않고 있었다.

원인은 에이전트가 추가한 조건문 딱 한 줄이었다.
해당 계산기에 맞춰 바꾼 광고 삽입 조건이,
대량 생성 페이지에서는 광고가 들어갈 자리를 아예 없애 버린 것이다.

AI에게 코드를 맡긴 뒤로 이런 식의 조용한 실패를 겪은 건 이번이 처음이 아니었다.

이 글에서는 AI가 짠 코드에서 되풀이되는 실수를, 사람의 리뷰 대신 린트와 CI 같은 자동 검사로 막는 방법을 다룬다.


조용한 실패가 반복되는 구조

이런 일이 반복되는 이유는 단순하다.

AI에게 기능 하나를 맡기면 몇 분 만에 수백 줄의 코드가 돌아오지만,
그걸 꼼꼼히 읽을 내 시간은 늘어나지 않는다.
diff가 길어질수록 정독 대신 훑어보게 되고, 승인 버튼도 습관처럼 누르게 된다.

생성량과 리뷰 용량 그래프
AI가 쓰는 양과 사람이 읽는 양

나만 그런 건 아니다.
Stack Overflow의 2025년 개발자 설문을 보면 응답자의 84%가 AI 도구를 쓰고 있거나 쓸 계획이라고 답했다.

그런데 AI 결과의 정확도를 믿는다는 사람은 33%에 그쳤고, 가장 큰 불만으로 66%가 "거의 맞지만 완전히 맞지는 않은 결과"를 꼽았다.

AI 도구 신뢰도 그래프
AI 도구 사용률과 신뢰도

'그럴듯하게 돌아가는 코드'는 런타임 에러를 뿜지 않는다.

화면도 정상적으로 뜨고 버튼도 잘 눌린다.
크래시가 나지 않으니 에러 추적 도구(Sentry)에도 잡히지 않는다.

결함은 에러 로그가 아니라, 엉뚱한 조건문 뒤에 숨어 조용히 비즈니스 로직만 무력화한다. 이번 사고가 딱 그랬다.


규칙 문서의 한계

처음에는 에이전트의 메모리, 즉 규칙 문서로 막아 보려고 했다.
Claude Code의 CLAUDE.md, Cursor의 .cursorrules처럼 에이전트가 세션을 열 때 가장 먼저 읽는 설정 파일에 사고가 날 때마다 규칙을 한 줄씩 덧붙였다.

오래 걸리는 작업은 백그라운드로 실행할 것, 배포 브랜치에는 직접 커밋하지 말 것, 새 페이지에는 공유 이미지를 반드시 삽입할 것.
그렇게 쌓인 '주의사항'이 어느새 300줄을 넘어섰다.

하지만 적어 둔다고 지켜지지는 않았다.
몇 분짜리 빌드를 포그라운드로 띄워 세션을 먹통으로 만드는 일도, master 브랜치에 그대로 푸시해 버리는 일도 계속 반복됐다.

결국 되돌릴 수 없는 사고가 터졌다.

에이전트는 명령 실패에 대비한다며 습관적으로 명령어 끝에 || git checkout -- .를 붙이곤 했다.
앞선 빌드가 실패하자마자, 아직 커밋하지 않은 작업 내역을 통째로 롤백해 버린 것이다.
이 한 줄 때문에 작성 중이던 글 17편이 로컬에서 완전히 증발했다.

고속도로 출구 짤
규칙 문서의 수명

규칙 문서 맨 위에 느낌표와 경고 문구를 덕지덕지 붙이고 사고 경위까지 박아 넣었지만, 비슷한 실수는 또 나왔다.

프롬프트 규칙 문서는 어디까지나 '권고'일 뿐 시스템 레벨의 '강제'가 아니기 때문이다.
컨텍스트가 길어지고 토큰이 밀려나면,
에이전트의 주의력(Attention)은 문서의 경고를 가장 먼저 흘려보낸다.

반대로 단 한 번도 어겨진 채 배포되지 않은 규칙도 있었다.
새 페이지에 공유 이미지를 누락하지 말라는 규칙이었다.

이 규칙은 문서가 아니라 빌드 스크립트가 지키고 있었다.
이미지가 비어 있는 페이지가 하나라도 있으면 빌드 파이프라인 자체가 에러를 뿜으며 중단되니, AI가 규칙을 까먹든 말든 배포까지 도달할 수가 없었던 것이다.

메모리 문서는 에이전트가 읽어야 효과가 있지만, 기계적 규칙은 강제로 지키게 할 수 있다.


반복 실수의 자동 검사 전환

사고가 났던 규칙부터 하나씩 문서에서 꺼내 기계적인 검사로 옮겼다.
가장 먼저 손댄 것은 에이전트 훅(Hook)이다.

훅은 에이전트가 셸 명령이나 파일 수정 같은 도구를 실행하기 직전이나 직후에 개입하는 스크립트다.
위험한 명령을 실행 단계에서 물리적으로 가로챌 수 있다.

Claude Code의 동작 방식을 보면
PreToolUse 훅이 종료 코드 2(exit code 2)를 반환할 때 해당 도구 호출이 즉시 차단된다.
그리고 표준 오류(stderr)로 출력한 텍스트는 그대로 에이전트의 다음 턴 컨텍스트로 주입된다.

이 두 가지 원리만 활용해도 치명적인 사고의 대부분을 원천 봉쇄할 수 있다.


오래 걸리는 빌드를 포그라운드로 띄우는 것과 배포 브랜치에 직접 커밋하는 패턴부터 훅으로 막아 세웠다.
앞서 작성 중이던 글을 날려버렸던 위험한 롤백 명령도 같은 방식으로 차단할 수 있다.

설정은 두 부분으로 나뉜다.

1. 훅 등록

.claude/settings.json에 에이전트가 셸 명령(Bash)을 실행하기 직전마다 차단 스크립트가 먼저 돌도록 등록한다.

jsonc
// .claude/settings.json
{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          { "type": "command", "command": "node .claude/hooks/block-git-reset.mjs" }
        ]
      }
    ]
  }
}

2. 차단 스크립트

에이전트가 실행하려는 명령을 받아, 작업 폴더 전체를 되돌리는 명령이나 커밋 전 검사를 건너뛰는 명령이면 종료 코드 2로 막는다.

js
// .claude/hooks/block-git-reset.mjs
import { readFileSync } from "node:fs";

const { tool_input } = JSON.parse(readFileSync(0, "utf8"));
const cmd = String(tool_input?.command ?? "");

const BLOCKED = [
  /\bgit\s+(checkout|restore)\s+(--\s+)?\.(\s|$)/, // 작업 폴더 전체 되돌리기
  /\bgit\s+commit\b.*--no-verify/,                 // 커밋 전 검사 건너뛰기
];

if (BLOCKED.some((re) => re.test(cmd))) {
  console.error("차단된 명령이다. 되돌릴 파일은 경로를 하나씩 지정하고, 검사 실패는 원인을 고친다.");
  process.exit(2);
}

차단 메시지(stderr)를 작성할 때는
"명령이 차단된 이유"와 함께 "대신 사용할 안전한 대안"을 명시해 주는 것이 핵심이다.


에이전트는 에러 메시지를 읽고 곧바로 안전한 명령어로 우회하여 작업을 이어가기 때문이다.

Hook이 위험한 명령을 차단하는 GIF
훅에 막힌 에이전트가 안전한 명령으로 다시 시도하는 동작

lint와 CI의 이중 차단

다만 에이전트 훅은 에이전트에게만 걸린다. 내가 터미널에서 직접 친 명령은 그대로 통과한다.
그래서 커밋할 때 한 번, 코드를 올렸을 때 CI에서 한 번 더 검사를 건다.
뒤로 갈수록 빠져나가기는 어렵지만, 그만큼 문제를 늦게 알게 된다.

검사 단계 그림
검사 단계별 특징

lint: 프로젝트만의 금지 패턴

린트(lint)는 코드를 실행하지 않고 읽기만 해서 규칙 위반을 찾아 주는 도구다.
자바스크립트에서는 보통 ESLint를 쓴다.
그런데 기본 규칙은 일반적인 실수만 알지, 우리 프로젝트에서 반복되는 실수는 모른다.

이럴 때 쓰는 게 ESLint의 no-restricted-syntax 규칙이다.
금지할 코드 모양과, 걸렸을 때 띄울 메시지를 직접 정할 수 있다.

js
// eslint.config.js
import js from "@eslint/js";
import { defineConfig } from "eslint/config";

export default defineConfig([
  {
    files: ["src/**/*.{js,jsx}"],
    plugins: { js },
    extends: ["js/recommended"],
    rules: {
      "no-restricted-syntax": [
        "error",
        {
          selector: "CallExpression[callee.name='gtag']",
          message: "gtag 를 직접 부르지 않는다. src/lib/analytics.js 의 track() 을 쓴다.",
        },
        {
          selector: "MemberExpression[object.name='localStorage']",
          message: "localStorage 를 직접 쓰지 않는다. src/lib/storage.js 의 safeGet()·safeSet() 을 쓴다.",
        },
      ],
    },
  },
  {
    // 감싼 함수 자체는 예외
    files: ["src/lib/analytics.js", "src/lib/storage.js"],
    rules: { "no-restricted-syntax": "off" },
  },
]);

여기서 message는 사람보다 에이전트가 훨씬 자주 읽는다.
"쓰지 마라"로 끝내지 말고 대신 쓸 함수 이름까지 적어 두면, 에이전트가 린트 오류를 보고 알아서 고쳐 온다.

CI: 병합 전 마지막 관문

커밋 단계 검사는 git commit --no-verify 한 번이면 건너뛸 수 있다.
그래서 마지막 관문은 CI에 둔다.
CI는 코드를 올릴 때마다 서버에서 검사와 테스트를 자동으로 돌리는 과정이다.

yaml
# .github/workflows/check.yml
name: check
on:
  pull_request:
  push:
    branches: [main]
jobs:
  check:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 22
          cache: npm
      - run: npm ci
      - run: npm run lint
      - run: npm test

워크플로를 만드는 것만으로는 부족하다.
GitHub 보호된 브랜치 설정에서 이 작업을 필수 상태 검사로 지정해야 검사를 통과하지 못한 코드가 병합되지 않는다.
그래야 사람이 올리든 에이전트가 올리든 같은 기준이 적용된다.

GitHub 브랜치 보호 규칙
브랜치 보호 규칙의 필수 상태 검사

검사 자체의 검증

검사를 이만큼 걸어 둬도 끝이 아니다.
검사가 무엇을 보고 있는지, 정말 잡아내기는 하는지를 따로 확인해야 한다.

처음의 광고 사고가 딱 그 경우였다.
광고 검사는 일반 계산기 페이지만 열어 보고 있었고,
pSEO로 만든 정적 페이지 1만여 장은 검사 목록에 없었다.

광고 검사 범위 도식
광고 검사 범위와 사고 지점

그래서 검사할 페이지를 목록에 적어 두는 방식부터 버렸다.
목록에 적어 두면 새로 생긴 페이지는 조용히 빠진다.
지금은 빌드 결과에 있는 페이지를 전부 훑고, 빼야 할 페이지만 이유와 함께 따로 적어 둔다.

검사 대상 관리 방식 비교
검사 대상 관리 방식 비교

마지막으로 옛 코드를 다시 넣고 새 검사를 돌려 봤다.
60번 중 58번 실패, 고친 코드는 60번 모두 통과. 그제야 이 검사를 믿을 수 있었다.

신규 검사 검증 그래프
새 검사 검증 결과

통과만 확인한 검사는 아무것도 안 하는 검사와 구별되지 않는다.
새 검사를 만들었다면 막으려던 버그를 일부러 다시 넣어 보고, 정말 실패하는지부터 확인하자.

차단과 경고의 기준

검사가 늘면 고민이 하나 생긴다. 걸리면 배포를 멈출지, 경고만 띄우고 보낼지다.
전부 차단으로 두면 잘못 잡은 검사 하나에 배포가 멈추고, 전부 경고로 두면 아무도 안 읽는다.

내가 쓰는 기준은 하나다.
검사에 걸린 문제가 그대로 배포됐을 때 무엇이 망가지는지 한 줄로 말할 수 있으면 배포를 막고, 말할 수 없으면 경고만 띄운다.

공유 이미지가 빠진 페이지는 한번 배포되면 그 상태로 공유되고 검색에 잡히니 차단한다.
분석 이벤트 이름이 목록에 없는 정도는 경고만 띄운다.
이미 위반이 많이 쌓여 있는 검사는 지금의 위반 목록을 기준으로 저장해 두고, 새로 생긴 위반만 막는다.

검사 등급 판정 흐름도
차단과 경고 판정 기준

말투처럼 문맥을 읽어야 판단할 수 있는 건 기계로 막지 않고, 사람이나 다른 AI의 검수로 남겨 둔다.


최소한의 안전장치

아직 검사가 하나도 없다면 이 세 가지부터 넣어 보자.

  1. git checkout -- . 같은 되돌리기 어려운 명령과 --no-verify의 에이전트 훅 차단
  2. 린트 금지 규칙 메시지에 대체 함수 이름 명시
  3. 기존 검사에 막으려던 버그를 다시 넣어 실제 실패 여부 검증

마무리

AI에게 코드를 맡긴 뒤로 코드를 쓰는 시간은 거의 사라졌지만, 확인해야 할 양은 오히려 늘었다.
그 확인을 사람이 전부 할 수는 없다.

AI에게 같은 지시를 두 번 하게 됐다면, 그 지시는 이제 문서가 아니라 검사에 적을 차례다.

최신 글 전체 보기 →