본문으로 건너뛰기

© 2026 Molayo

Dev.to헤드라인2026. 06. 15. 07:46

또 다른 에이전트가 필요하지 않습니다. 필요한 것은 린터입니다.

요약

AI가 생성한 '바이브 코드(vibe code)'의 불안정성을 해결하기 위해 새로운 AI 에이전트 대신 전통적인 린터(Linter) 활용을 제안합니다. 린터는 결정론적이고 비용 효율적이며, 코드의 구조적 문제를 실행 없이 정확히 찾아낼 수 있는 강력한 도구입니다.

핵심 포인트

  • AI 생성 코드(바이브 코드)는 겉보기에 완벽하지만 내부 로직의 예측 불가능성이 높음
  • 린터는 코드를 실행하지 않고 구문 트리를 분석하여 구조적 오류를 탐지함
  • 린터는 결정론적이며 API 비용이나 토큰 소모가 없는 경제적인 도구임
  • 복잡한 AI 에이전트 도입보다 검증된 정적 분석 도구가 코드 품질 유지에 효과적임

지난 글에서 저는 제품 관리자들(product managers)과 그들이 '바이브 코드(vibe code)'로 제 삶을 지옥으로 만들었다는 점에 대해 많이 불평했습니다. PS: 만약 이 글을 읽고 계신 매니저님께 사과드립니다. 하지만 이건 사실입니다.

지금 저는 단지 불평하기 위해 여기에 온 것이 아닙니다. 레거시 코드(legacy code)나 바이브 코드를 다루는 방법 같은 많은 학습 기회도 있었습니다. 왜냐하면 결국 둘 다 같습니다. 아무도 어떻게 작동하는지 모르지만, 왠지 모르게 계속 작동하고 있습니다. 이것들을 건드리는 것은 폭탄을 해체하는 것과 같아서, 당신의 변경 사항이 핵심 로직(core logic)에 어떤 식으로 연쇄적으로 영향을 미쳐 고장 날지 절대 알 수 없습니다.

좋은 소식은 바이브 코드가 레거시 코드보다 훨씬 단순하다는 것입니다. AI는 그 모든 영광 속에서 완벽해 보이는 코드를 작성하려고 합니다. 적절한 함수 이름, 주석 등입니다. 500줄에 걸쳐 하나의 함수가 실행되고, 의미를 알 수 없는 스파게티 같은 이름과 오래된 주석이 난무하는 레거시 코드와는 다릅니다.

그리고 그 점이 제가 실제로 처리할 수 있는 무언가를 만들어줍니다. 아직 완벽하고 단계별 매뉴얼(playbook)은 없습니다. 하지만 조각들은 가지고 있습니다.

첫 번째입니다. 가장 저렴하고 가장 오래된 것입니다. 산업계가 이미 수십 년 전에 해결했고, 'AI가 하루 만에 내 앱을 만들었다'는 사람들 전체가 어떻게 존재한다는 것을 잊어버린 것 말입니다.

린터(A linter).

네, 제가 말씀드린 것이 맞습니다. 린터요. ESLint.

이 산업계에 몸담았던 대부분의 사람들은 이미 알고 있습니다. 이것은 상자 안에 있는 가장 지루하고 신뢰할 수 있는 도구입니다.

하지만 모든 문제에 대한 답이 '또 다른 AI를 추가하라'인 시대에, 이 지루한 도구가 여전히 승리하는 이유를 소리 내어 말할 가치가 있습니다.

린터가 실제로 무엇인지

만약 당신이 바이브 코딩으로 이 세계에 들어왔거나, 웹 개발 자체에 익숙하지 않아 'lint'라는 단어를 들어본 적이 없다면, 솔직한 버전을 알려드리겠습니다.

린터는 레포지토리(repo)에 추가하는 일련의 규칙입니다. 실행하지 않고 코드를 읽어 그 규칙들에 따라 검사하고, 깨졌거나, 허술하거나, 프로덕션 환경에서 당신을 공격할 모든 것을 표시해 줍니다.

사람들이 오해하는 세부 사항: 이것은 나쁜 단어를 찾는 grep이 아닙니다.

진정한 린터 (Linter)는 코드를 구문 트리 (Syntax Tree)로 파싱하며, 무엇이 임포트(Import)되었는지, 무엇이 호출되었는지, 무엇이 도달 가능한지, 타입 (Type)이 어디로 흐르는지 등 구조에 대해 실제로 추론합니다.

그렇기 때문에 no-unused-vars와 같은 규칙은 "이것을 임포트했지만 사용하지 않았다"와 "임포트하지 않고 이것을 사용했다"를 구분할 수 있습니다.

또한 eslint-plugin-unicorn이 단 한 줄의 코드도 실행하지 않고도 array.indexOf(x) !== -1array.includes(x) (unicorn/prefer-includes)로 조용히 재작성하거나, 당신을 곤란하게 만들 new Array(...) 호출을 찾아낼 수 있는 이유이기도 합니다.

세 가지 속성이 중요합니다:

  • 코드를 실행하지 않습니다. 부작용 (Side effects)도 없고, 불안정한 네트워크도 없으며, "내 컴퓨터에서는 되는데" 같은 문제도 없습니다.
  • 결정론적 (Deterministic)입니다. 동일한 코드가 입력되면 매번 항상 동일한 결과가 나옵니다. 온도 (Temperature)도 없고, 느낌 (Vibes)도 없습니다.
  • 비용이 들지 않습니다. 토큰 (Tokens)도 필요 없고, API 비용도 들지 않습니다. 노트북이나 CI 서버에서 몇 초 만에 실행됩니다.

2026년에는, 마지막 속성이 논쟁의 핵심입니다.

이것이 예전보다 더 중요한 이유

우리는 코드를 작성하기 위한 AI, 코드를 리뷰하기 위한 AI, 코드를 검증하기 위한 AI를 가지고 있습니다. 에이전트 (Agent) 위에 에이전트가 쌓이고, 각각의 에이전트들은 무료 도구가 이미 더 잘 해내고 있는 일을 하기 위해 조용히 토큰을 소비하고 있습니다.

솔직히 말해서, 그 에이전트들 대부분은 "에이전트"라는 단어가 지금의 의미를 갖기 훨씬 전부터 이미 해결되었던 문제들을 다시 풀고 있습니다.

프로덕션 환경에 디버그 로그를 남겨두었는지 (no-console), 사용하지 않는 모듈을 임포트했는지 (unused-imports/no-unused-imports), 아무도 호출하지 않는 함수를 작성했는지 (knip), 프로미스 (Promise)에 await를 붙이는 것을 잊었는지 (@typescript-eslint/no-floating-promises), 혹은 두 파일이 서로를 순환 참조하도록 임포트했는지 (import/no-cycle)를 알아내기 위해 언어 모델 (Language Model)이 필요하지는 않습니다.

린터는 AI가 깨어나기도 전에 이 모든 것을 즉각적이고, 결정론적이며, 무료로 잡아냅니다.

그러니 파이프라인에 또 다른 에이전트를 붙이기 전에, 지루한 질문을 던져보세요:

이미 검증된 무료 도구가 이 일을 하고 있는가?

대부분은 그렇습니다.

이미 존재하는 것을 사용하세요.

린터는 품질 게이트 (Quality Gate) 입니다

이것이 바로 린터 (Linter)가 중요한 진짜 이유입니다. 당신이 대기업에 다니든, 이제 막 시작하는 프리랜서이든 상관없습니다.

린터는 품질 게이트 (Quality Gate)입니다. 다음과 같이 선언하는 경계선과 같습니다:

이 코드는 기준을 통과하지 못하면 들어올 수 없다.

제가 일하는 곳에서는 이를 풀 리퀘스트 (Pull-request, PR) 흐름에 직접 연결했습니다. PR이 머지 (Merge)되기 전에 반드시 통과해야 하는 체크 항목으로 설정한 것입니다.

단순한 제안이 아닙니다. "나중에 정리하자"는 식도 아닙니다. 머지 버튼에 걸린 강력한 게이트입니다.

그 결과는 직접 경험해 보기 전까지는 지어낸 이야기처럼 들릴 정도의 수치로 나타났습니다. 프로덕션 이슈 (Production issues)와 잘못된 머지 (Bad merges)가 약 80% 감소했습니다.

마법인가요? 아닙니다.

우리에게 발생했던 대부분의 인시던트 (Incidents)은 늘 똑같고 지루한 몇 가지 유형이었습니다. 에러를 삼켜버린 처리되지 않은 프로미스 (Unhandled promise), 남겨진 방치된 디버그 로그 (Debug log), 프로덕션 환경에서만 발생하는 순환 참조 임포트 (Circular import) 같은 것들이었죠. 게이트는 문 앞에서 이 모든 것들을 잡아냈습니다. 우리는 그저 망가진 코드가 통과하도록 내버려 두지 않았을 뿐입니다.

그것이 비결의 전부입니다.

게이트가 누군가를 더 똑똑하게 만들지는 않습니다. 그저 바닥(최소 기준)을 높여줄 뿐입니다.

대기업에 계신 분들을 위한 팁을 드리자면, ESLint 체크를 도입할 때 개발자들의 반발을 많이 듣게 될 것입니다. 특히 스타트업이나 중소기업에서는 더욱 그렇습니다. 그러니 싸울 준비를 하시고, 점진적으로 도입하세요. 처음에는 경고 (Warning) 게이트로 시작하고, 그다음 에러 (Error) 게이트로 전환하십시오.

"세미콜론 누락" 수준에 머물지 않습니다

사람들이 처음 ESLint를 추가하고 수많은 에러를 마주할 때 빠지는 흔한 오해가 있습니다. "이건 별로야, 짜증 나는 형제처럼 잔소리만 해대잖아." 이는 매우 과소평가된 생각입니다.

린터를 잔소리꾼이 아닌 실제 게이트로 취급하기 시작하면, 당신의 특정한 실수들을 린터에게 가르칠 수 있습니다. 그리고 린터는 그것을 절대 잊지 않습니다.

AI의 도움을 받는 코드베이스 (Codebase)에서 발생하는 모든 고통스러운 버그는 하나의 패턴 (Pattern)이기도 합니다. 그리고 패턴은 바로 정적 분석 (Static analysis)이 가장 잘하는 분야입니다.

놀랍게도 이 중 상당 부분은 설정 파일 하나로 해결할 수 있습니다. 현대적인 ESLint 플랫 설정 (Flat config)과 몇 가지 커뮤니티 플러그인만 있으면 대부분을 커버할 수 있습니다:

// eslint.config.js — 세미콜론 누락을 잔소리하는 도구가 아닌, 시작을 위한 "품질 게이트"
import unusedImports from "eslint-plugin-unused-imports";
import importPlugin from "eslint-plugin-import";
...

그것이 바로 바닥입니다.

게다가, 무언가 우리를 괴롭힐 때마다 우리는 그것이 다시는 우리를 괴롭히지 못하도록 규칙을 추가했습니다. 그중 세 가지는 혹독한 과정을 거쳐 자리를 잡았습니다.

  • 순환 의존성 (Circular dependencies)maxDepth: 10 설정이 포함된 import/no-cycle (기본값인 1은 직접적인 A↔B 순환만 감지합니다). 전체 그래프 뷰를 위해 dependency-cruiser를 병행 사용합니다. AI 리팩토링 (AI refactors)은 빌드가 프로덕션 환경에서만 깨질 때까지 두 모듈이 서로를 임포트하게 만드는 것을 매우 좋아합니다. 이는 제가 본 AI 유발 버그 중 가장 흔한 유형이며, 별도의 포스팅을 작성할 가치가 있을 정도입니다.
  • 누락된 await (Dropped awaits)@typescript-eslint/no-floating-promisesno-misused-promises. 파일이나 네트워크 호출에서 await가 누락되면 에러를 조용히 삼켜버리며, 사용자가 문제를 겪을 때서야 이를 알게 됩니다. 타입 인지 린팅 (type-aware linting, parserOptions.projectService)이 필요하며, 이는 기꺼이 지불할 만한 설정 비용입니다.
  • 무료 승리 패키지 (The free-wins pack)eslint-plugin-unicornflat/recommended: 단 하나의 임포트로 약 100개의 규칙을 가져옵니다. 성능 향상 (prefer-includes, prefer-set-has), 실제 숨겨진 버그 포착 (no-array-push-push, no-thenable), 그리고 몇몇 주관적인 스타일이 혼합되어 있습니다. 잦은 변경을 유발하는 규칙(unicorn/filename-case, unicorn/prevent-abbreviations, unicorn/no-null)은 끄고 나머지는 유지하세요. 발견된 실수 대비 설정 효율이 가장 높은 조합입니다.

나머지 규칙들은 조용하지만, 각각 한 번씩은 무언가를 잡아냈습니다. 훑어보세요:

  • Dead code (데드 코드)eslint-plugin-unused-imports는 모델이 시도했다가 포기한 접근 방식을 위해 추가한 임포트(import)들을 휩쓸어 정리합니다 (--fix로 자동 수정됨).
  • No evalno-eval, no-implied-eval, no-new-func, no-script-url. 기본적으로 꺼져 있지만, 정당한 사용 사례는 전무하며 생성된 코드가 이를 몰래 끼워 넣을 방법은 무한합니다.
  • XSS sinks (XSS 싱크)vue/no-v-html / react/no-danger, 그리고 생(raw) innerHTML을 위한 eslint-plugin-no-unsanitized. 모든 HTML 인젝션은 린트(lint) 단계에서 강제되는 새니타이저(sanitizer)를 통과해야 합니다.
  • Hardcoded strings (하드코딩된 문자열)eslint-plugin-i18next를 사용하여, 언어를 추가하는 날에 마크업에 아무것도 박혀 있지 않도록 합니다.
  • Test hygiene (테스트 위생)no-focused-tests를 **에러 (error)**로 설정하여, 길을 잃은 it.only가 CI 스위트(suite)를 조용히 축소시키지 못하게 합니다.
  • Leaked secrets (유출된 비밀 정보)eslint-plugin-no-secrets는 커밋된 토큰처럼 보이는 높은 엔트로피(high-entropy) 리터럴을 플래그(flag)합니다.
  • Design drift (디자인 드리프트, 커스텀) — 이름이 지정된 토큰(named token)이 있어야 할 자리에 생(raw) hex/size/shadow가 나타나면 빌드를 실패하게 만드는 약 40줄짜리 AST 스크립트입니다.
  • Layer boundaries (계층 경계) — 한 계층이 다른 계층을 임포트하는 것을 방지하는 eslint-plugin-boundaries, 그리고 위험한 뮤테이션(mutation)을 단일 함수를 통해서만 수행하도록 강제하는 커스텀 규칙입니다.

이 각각은 우리가 단 한 번만 배우면 되는 교훈들입니다.

그 이후로, 린터(linter)가 우리를 대신해 이를 기억합니다. 영원히, 무료로, 모든 커밋마다, 인간이든 AI든 모든 기여자에게 적용됩니다.

검문소가 짜증 나지 않게 유지하는 비결

대부분의 사람들은 한 가지 이유로 린팅(linting)을 포기합니다. 첫날부터 모든 규칙을 error로 켜두었다가, 수만 개의 경고(warning)에 빠져 분노하며 그만두는 것입니다.

그렇게 하지 마세요.

새로운 규칙은 경고(warn) 우선으로 실행하세요. 차단하지 않고 보고만 하게 두세요. 여러분의 일정에 맞춰 그 개수를 0으로 만드세요. 그다음에 해당 규칙을 error로 격상시켜 다시는 퇴보하지 않도록 하세요.

중요한 관점의 전환은 이것입니다: 그 경고들은 오늘 당장 린터에게 갚아야 할 할 일 목록(to-do list)이 아닙니다. 그것은 지뢰가 어디에 매설되어 있는지 보여주는 지도입니다. 여러분은 그 개수를 동결하고, 아무도 새로운 지뢰를 심지 못하게 막으며, 여러분의 속도에 맞춰 현장을 정리해 나가는 것입니다.

오늘 당장 고칠 수 없는 소수의 레거시 위반 사항(legacy offenders)들에 대해서는 명시적인 허용 목록(allowlist)을 유지하세요. 그리고 그 허용 목록에 항목을 추가하는 행위 자체를 금지된 일로 취급해야 합니다.

이 목록은 오직 줄어들기만 해야 합니다.

이것이 바로 허위 경보(crying wolf)를 울리지 않으면서도 게이트(gate)를 엄격하게 유지하는 방법이며, 상속된 코드(inherited code)가 당신의 얼굴 앞에서 폭발하지 않도록 길들이는 방법입니다.

주의 사항 (Gotchas)

  • 린터(linter)는 정적인 것만 볼 수 있습니다. 동적 임포트(Dynamic imports), 문자열로 생성된 코드, 그리고 런타임(runtime)에 결정되는 값들은 린터에게 보이지 않습니다. 사각지대를 파악하세요. '린트 통과(green lint)'가 곧 '증명된 정확함(provably correct)'을 의미하지는 않습니다.
  • --fix는 자유로운 사고를 하지 않습니다. 자동 수정(Auto-fix)은 사용하지 않는 임포트(unused imports)나 포맷팅(formatting)에는 훌륭합니다. 하지만 플래그(flag)가 지정된 항목이 실제 실수인지 판단하는 데는 적합하지 않습니다. 반드시 차이점(diff)을 읽으세요.
  • 절대적인 개수가 아니라 추세(trend)에 게이트를 설정하세요. 레포지토리에 경고(warning)가 '하나라도' 있다는 이유로 CI를 실패시키는 것은 분위기로 코딩하는(vibe-coded) 코드베이스에서는 유지 불가능합니다. 개수가 기준선(baseline)을 넘어 증가할 때 실패 처리하세요. 그것이 실행 가능한(actionable) 조치입니다. "완벽해져라"는 실행 불가능합니다.
  • 모든 곳에서 억제(suppress)하는 규칙은 삭제된 규칙과 다름없습니다. 만약 코드의 매 줄마다 비활성화(disable) 주석이 달려 있다면, 당신은 게이트를 통과한 것이 아니라 게이트를 제거한 것입니다. 코드를 수정하거나, 아니면 정직하게 규칙을 폐기하세요.

가벼운 일침

벌써 예상되는 아이러니가 하나 있습니다. 당신은 이미 해결된 문제에 AI를 덧붙이는 것이 왜 낭비인지에 대한 이 글 전체를 읽었음에도 불구하고, 바로 다음 행동으로 채팅창을 열어 _"내 레포에 ESLint와 모든 플러그인을 추가해줘"_라고 타이핑할 것입니다. 혹은 더 최악으로, _"사용하지 않는 임포트와 순환 의존성(circular deps)을 찾아내는 코드 리뷰 에이전트를 만들어줘"_라고 할 수도 있겠죠.

멈추세요.

둘 다 잘못된 선택입니다. 첫 번째 방식은 당신을 영원히 파헤쳐야 할 1,000개 이상의 에러라는 토끼굴(rabbit hole)로 빠뜨릴 것입니다. 기본 권장 사항부터 시작하여, 천천히 나머지 단계로 나아가세요.

코드 리뷰 에이전트(code-review agent)에 대해서라면 — 못 들은 걸로 하겠습니다. 우리는 이 글 전체를 통해 왜 여기서 린터(linter)가 AI 리뷰어보다 나은지에 대해 이야기했습니다. 그런데 왜 아직도 그것을 찾으려 하시나요? 보세요, 만약 당신이 토큰(tokens)을 그토록 아까워한다면 — 만약 수십 년 된 무료 도구가 이미 _결정론적(deterministically)_으로 해결하고 있는 문제에 토큰을 쓰고 싶어 몸이 근질거린다면 — 그냥 저에게 보내주세요. 제가 더 유용하게 쓰겠습니다.

AI는 모호한 영역(fuzzy stuff) — 아이디어를 현실로 바꾸는 것, 무(無)에서 유(有)를 창조할 때의 도파민 분출 같은 것 — 에 진정으로 뛰어납니다. AI가 그 일을 하게 두세요. 코드 품질에 대한 무거운 판단은 린터에게 맡기십시오.

그리고 반대편에 대해서도 솔직해지겠습니다. 린터는 당신의 아키텍처(architecture)를 고쳐주지 못하며, 잘못된 비즈니스 규칙(business rule)을 잡아내지 못하고, 해당 기능이 나쁜 아이디어였다고 말해주지도 않습니다. 그 부분은 여전히 당신의 몫입니다. 하지만 린터가 _해줄 수 있는 것_은 지루하고, 기계적이며, 끝이 없는 종류의 실수들을 당신의 업무에서 덜어주는 것입니다. 그래야 당신의 뇌가 실제로 판단이 필요한 부분에 집중할 수 있습니다.

핵심 요약 (Takeaway)

린터는 팀에서 가장 저렴한 리뷰어입니다. 무료이며, 결정론적이고, 지치지 않으며, 유행(hype)에 전혀 휘둘리지 않습니다.

린터를 머지(merge) 시의 엄격한 게이트(gate)로 설정하면, 모든 팀원의 최소 기준(floor)을 한 번에 높일 수 있습니다.

그다음, 당신이 반복하는 실수들 — 토큰 드리프트(token drift), 하드코딩된 문자열(hardcoded strings), 순환 의존성(circular deps), 안전하지 않은 HTML(unsafe HTML), 누락된 await(dropped awaits) — 에 대해 린터를 학습시키세요. 버그 클래스당 하나의 커스텀 규칙(custom rule)을 만들고, 처음에는 경고(warn)로 시작하여 점진적으로 에러(error)로 격상시키십시오.

린터를 먼저 도입하세요.

그러고 나서, 만약 여전히 에이전트가 필요하다면, 적어도 그 에이전트는 이미 기준을 통과한 코드를 리뷰하게 될 것입니다.

당신이 고용할 수 있는 가장 저렴한 리뷰어는 이미 당신의 저장소(repo)에 계속 자리 잡고 있었습니다.

AI 자동 생성 콘텐츠

본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.

원문 바로가기
0

댓글

0