ai-deslop 입문: AI가 작성한 TypeScript의 '어설픔'을 Oxlint로 기계적으로 걸러내기 [검증 완료]
요약
AI가 생성한 TypeScript 코드의 잠재적 오류를 Oxlint의 JS 플러그인 'ai-deslop'으로 검증하는 방법을 안내합니다. 이 도구는 타입은 통과하지만, 근거가 부족하거나 안전하지 않은 코딩 패턴(예: 이중 캐스팅, unknown 사용)을 기계적으로 감지하여 코드 품질을 높이는 데 도움을 줍니다.
핵심 포인트
- ai-deslop은 Oxlint의 JS 플러그인으로, AI 작성 코드의 취약점을 검출합니다.
- 이 도구는 타입 근거가 부족한(low-evidence) 코딩 패턴을 감지하는 것이 핵심입니다.
- 주요 규칙으로는 이중 캐스팅 금지(`no-chained-type-assertions`) 등이 있습니다.
- 로컬 환경에서 작동하며, LLM API 키나 클라우드 계정이 필요 없습니다.
개요 (Overview)
AI에게 TypeScript를 작성하게 하면, 타입은 통과하지만 리뷰에서 지적받는 코드가 나옵니다. 예를 들면 다음과 같은 경우입니다.
input as unknown as User와 같은 이중 캐스팅unknown나object만 받는 함수filter().map()의 연쇄 호출...(cond ? { x } : {})와 같은 조건부 스프레드- 모크가 호출되었는지 여부만 확인하는 테스트
ai-deslop은 이러한 '근거가 부족한(low-evidence)' 작성 방식을 감지하는 Oxlint의 JS 플러그인입니다 (MIT). 2026년 10월 14일에 Show HN에 막 게시되어 일본어 설명은 아직 보이지 않습니다.
이 글에서는 다음 순서로 실제로 직접 만져보면서 확인한 결과를 정리합니다.
- AI가 작성할 법한 코드를 의도적으로 준비하여, 규칙이 무엇을, 왜 지적하는지 살펴봅니다. - 지적받은 대로 수정하여 에러를 0개로 만듭니다.
- 기존 리포지토리에 단계적으로 적용할 설정을 만듭니다.
README에 적혀있지 않은 어려움도 검증 중에 몇 가지 발견했습니다 (후반부에 소개합니다).
전제 환경 (Prerequisites)
검증 환경은 다음과 같습니다 (2026-10-14 시점).
| 툴 | 버전 |
|---|---|
| Node.js | 22.23.3 (22.18 이상 필요. 이유는 후술) |
| ... | |
| LLM의 API 키나 클라우드 계정은 필요 없습니다. 모두 로컬에서 작동합니다. |
단계별 코드 가이드 (Step-by-Step Code Guide)
1. 설치
mkdir deslop-demo && cd deslop-demo
npm init -y
npm pkg set type=module
...
포인트는 두 가지가 있습니다.
oxlint-tsgolint을 빠뜨리면,Failed to find tsgolint executable.라고 표시되며 종료 코드 1로 멈춥니다. ai-deslop의 권장 설정은 타입 정보를 사용하는 규칙(typeAware: true)을 활성화하므로 이 패키지가 필요합니다. - `
import { describe, it, expect, vi } from "vitest";
vi.mock("../src/jobs.js");
import { sendMail, onSignup } from "../src/jobs.js";
...
4. 실행하기
npx oxlint src test
결과는 16가지 규칙에서 25개의 에러였고, 종료 코드는 1이었습니다. 출력의 일부를 게재합니다.
x deslop(no-chained-type-assertions): This assertion chain discards type evidence.
Keep the original precise type, or parse untrusted input at its boundary before narrowing it.
,-[src/user.ts:6:12]
...
감지된 규칙과 각각의 의도는 다음과 같습니다.
| 규칙 | 감지 위치 | 의도 |
|---|---|---|
no-unknown-parameters / no-object-parameters | input: unknown , meta: object<br>타입 경계를 모호하게 만들지 않음. 파싱은 I/O 경계에서 한 번만 수행할 것 | |
no-runtime-typeof | typeof input === "object""<br>임시방편의 타입 좁히기가 아니라, 경계에서 파싱할 것 | |
no-chained-type-assertions | as unknown as<br>타입 근거를 버리는 이중 캐스팅을 금지함 | |
require-safety-comment-for-type-assertion | as 전반<br>캐스팅하려면 // SAFETY: 주석으로 성립 가정을 작성할 것 | |
no-conditional-empty-object-spread | ...(c ? {x} : {})<br>속성 생략을 빈 객체로 숨기지 않음 | |
no-array-filter-map | .filter().map()<br>배열을 두 번 순회하지 말 것 | |
no-reduce-accumulator-copy | Object.assign({}, acc, …)<br>매번 복사하면 계산량이 O(n²)가 됨 | |
no-reflect-get | Reflect.get<br>타입이 지정된 속성 접근자를 사용할 것 | |
no-shape-in-symbol-names | UserShape<br>이름에는 구조(shape)가 아닌, 도메인상의 역할을 부여할 것 | |
no-pass-through-type-alias | type Account = UserShape<br>단순한 별명을 늘리지 말 것 | |
typescript/no-floating-promises | sendMail(email);<br>await를 잊은 Promise를 감지함 | |
typescript/no-unnecessary-condition | n !== null (n: number)<br>항상 같은 결과를 내는 조건을 감지함 | |
no-module-mocking | vi.mock(...)<br>모듈의 모킹 대신 의존성 주입을 사용할 것 | |
no-call-only-assertions | toHaveBeenCalled()만 있는 테스트<br>“호출되었는지”만 확인하는 테스트를 금지함 | |
no-poor-substring-tests | 상수에 대한 toContain<br>의미 없는 부분 일치 테스트를 금지함 |
실제로 시도해 보니, 규칙이 의외로 놓쳐주는 경우도 있었습니다.
type UserId = string
은 감지되지 않습니다. (no-pass-through-type-alias)는 “다른 이름 붙인 타입에 대한 별명”만을 대상으로 하기 때문입니다.
toHaveBeenCalledWith("[email protected]")
같은 테스트는 허용됩니다. 인자까지 확인했다면, 실제 데이터를 검증하고 있다고 판단되기 때문입니다.
이처럼
받는 것은 I/O 경계의 파싱 함수로만 하고, 그 이후는 도메인 타입으로 처리하도록 하세요.
export type Plan = "free" | "pro";
export type User = { id: string; name: string; plan?: Plan };
export type UserPayload = { id: string; plan?: Plan };
...
$ npx oxlint fixed
Found 0 warnings and 0 errors.
Finished in 304ms on 1 file with 135 rules using 4 threads.
또한, // SAFETY:
줄을 지우면 require-safety-comment-for-type-assertion 오류가 1건 발생합니다. 캐스팅 자체는 금지하지 않고, 전제를 말로 쓰게 하는 규칙입니다.
6. 스크립트로 예상대로 작동하는지 확인하기
-f json
으로 출력하여 기대했던 규칙이 모두 감지되었는지 확인할 수 있는 스크립트입니다. 규칙의 버전을 올렸을 때의 회귀 테스트에도 사용 가능합니다.
import { execFileSync } from "node:child_process";
function lint(...paths) {
try {
...
PASS src/user.ts: no-shape-in-symbol-names
...
PASS test/user.test.ts: no-poor-substring-tests
...
3개 디렉토리, 4개 파일의 lint는 실제 시간으로 약 0.7초 만에 끝났습니다 (타입 정보를 사용하는 규칙 포함).
7. 기존 리포지토리에 단계적으로 적용하기
기존 코드에 갑자기 모든 규칙을 error로 설정하면, CI가 빨갛게 변합니다. 그래서 우선 deslop의 규칙을 warn으로 설정하고, 건수 상한선으로 '늘어나지 않음'만을 보장하는 설정을 만들었습니다.
import { defineConfig } from "oxlint";
import { configs as deslopConfigs } from "ai-deslop";
const asWarn = Object.fromEntries(
...
| 실행 내용 | 결과 | 종료 코드 |
|---|---|---|
--max-warnings=30 (경고 23건) | 상한선 이내이므로 통과 | 0 |
--max-warnings=5 (경고 23건) | Exceeded maximum number of warnings. | 1 |
| await를 잊은 Promise 포함 | 오류가 1건 발생 | 1 |
상한선을 현재 건수에 맞춰두고, 고칠 때마다 낮춰나가는 운영이 가능합니다.
GitHub Actions에서는 -f github을 붙이면 PR의 차분 해당 라인에 주석이 달립니다.
- uses: actions/setup-node@v4
with:
node-version: 22
...
::error file=src/jobs.ts,line=7,endLine=7,col=3,endColumn=18,title=typescript(no-floating-promises)::src/jobs.ts:7:3: Promises must be awaited, add void operator to ignore.
( -f github의 출력 형식은 로컬에서 확인했습니다. Actions 상에서의 실행은 본문에서 시도하지 않았습니다.)
어려움 (Gotchas / Pitfalls)
1. oxlint-tsgolint을 넣지 않으면 아무것도 검사되지 않음
추천 설정은 타입 정보를 사용하는 규칙을 활성화하기 때문에, oxlint-tsgolint이 없으면 전체가 Failed to find tsgolint executable.으로 멈춥니다. README의 설치 절차에는 이 패키지가 적혀있지 않습니다.
2. AI 에이전트 안에서 실행하면 출력 형식이 멋대로 바뀜
검증 중에 가장 놀라웠던 점이 이것입니다. 환경 변수 CLAUDECODE
• AI_AGENT
• CODEX_SANDBOX
중 어느 것이 설정되어 있으면, oxlint는 **코드 발췌가 없는 1줄 형식(agent 형식)**으로 자동 전환됩니다.
src/jobs.ts:7:3: error typescript(no-floating-promises): Promises must be awaited, add void operator to ignore. help: ...
Claude Code나 Codex에 수정하게 할 때는 토큰 절약이 됩니다. 다만, 같은 커맨드인데 로컬과 에이전트에서 출력이 다르기 때문에, 로그를 분석하는 쪽에서는 혼란스럽습니다. CI나 스크립트에서는 -f json
이나 -f github
을 명시해야 합니다 (CI=true만으로는 전환되지 않았습니다).
3. TS 설정 파일은 Node 22.18 이상에서 사용
Node 20.20.2로 실행했을 때 다음 에러가 발생했습니다.
Error: Unknown file extension ".ts" for .../oxlint.config.ts
TypeScript config files require Node.js ^20.19.0 || >=22.18.0.
Detected Node.js v20.20.2.
메시지에는 '^20.19.0이면 대응'이라고 쓰여 있지만, 20.20.2에서 실패합니다. Node 22.18 이상을 사용하거나, 설정을 .oxlintrc.json에 작성하는 것이 확실합니다.
4. 규칙은 '의견이 강한(opinionated)'
no-shape-in-symbol-names는 UserShape 같은 이름을 일률적으로 에러 처리하고, no-module-mocking은 vi.mock을 전면적으로 금지합니다. 팀 방침과 맞지 않는 규칙은 rules에서 `
AI 자동 생성 콘텐츠
본 콘텐츠는 Zenn AI의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기