린터(Linter)는 비코드 자산의 가치를 측정할 수 없다 — 이를 위한 LLM 재고 조사 도구를 만들었습니다
요약
저장소 내에 방치된 설정 파일, 워크플로우, 문서 등 비코드 자산의 가치를 측정하기 위해 grep과 LLM을 결합한 재고 조사 도구 설계 방식을 소개합니다. 구조적 분석은 코드로, 의미적 가치 판단은 LLM으로 수행하여 효율성을 극대화합니다.
핵심 포인트
- 린터는 파일의 구조적 유효성만 확인하며 실제 가치는 판단하지 못함
- grep을 통한 구조적 열거와 LLM을 통한 의미적 판단의 2단계 접근법 제안
- 비용 절감을 위해 모든 파일이 아닌 의심스러운 후보군에만 LLM 투입
- Claude Code 스킬로 구현하거나 수동으로 실행 가능한 워크플로우 제공
이 글에서 다루는 내용: 저장소에 쌓이는 "버려진 설정(configs), 죽은 워크플로우(workflows), 아무도 읽지 않는 런북(runbooks)"을 두 단계 방식 — 구조는 grep으로, 가치는 LLM으로 — 사용하여 재고를 조사하는 방법입니다. 이를 Claude Code 스킬로 추가하거나, 스킬 없이 동일한 패턴을 수동으로 실행할 수 있습니다.
전제 조건
- 스킬로 사용하는 경우: git, 그리고
~/.claude/skills/(Claude Code 환경)에 대한 쓰기 권한 - 수동으로 실행하는 경우:
grep이 포함된 셸, 그리고 어떤 LLM(판단을 맡길 대상) - 대상은 일반적으로 "코드가 아닌 파일들"입니다 (설정(configs), CI, 문서(docs), 런북(runbooks) 등). 코드 자체 내의 죽은 코드(dead code)를 탐지하는 것은 범위 밖입니다.
AI 에이전트와 함께 개발하면, 비코드 자산들이 쌓입니다
AI 에이전트와 함께 개발하다 보면, 코드가 아닌 파일들이 조용히 쌓이게 됩니다.
PLAN_xxx.md,HANDOFF_xxx.md,PROGRESS_xxx.md— 한 번 작성되고 다시는 읽히지 않는 작업 노트들- 임시로 추가되었다가 삭제되지 않은 린터(Linter) 또는 포매터(formatter) 설정 파일들
- 실질적으로 아무것도 하지 않는 GitHub Actions 워크플로우(workflows) —
on:조건이 충족되지 않아 실행되지 않거나, 실행되더라도if: false에 의해 모든 작업(job)이 건너뛰어지는 경우 - 설명하는 프로세스가 종료된 지 한참 지났음에도 여전히 남아 있는 런북(runbooks)
이것들은 "고장 난" 것이 아닙니다. YAML은 유효하고, Markdown은 형식이 잘 갖춰져 있습니다. 그래서 린터(Linter)는 아무 말도 하지 않습니다. 린터는 오직 "이것이 구조적으로 올바른가"만을 확인합니다 — 이 파일이 "여전히 존재할 가치가 있는가"는 측정할 수 없습니다.
그것이 바로 제가 겪고 있던 문제였습니다. 저는 "더 이상 아무도 참조하지 않는 런북이나, 작동을 멈춘 워크플로우를 탐지하는 스킬"을 찾기 시작했고, 결국 repo-asset-stocktake라는 이름의 스킬을 만들게 되었습니다. 이 글에서는 이 스킬의 핵심 설계(누구나 재사용할 수 있는 부분)와, 실제로 다른 저장소에 적용하여 "버려진 문서들의 섬"을 발견한 이야기를 다룹니다.
핵심: grep을 통한 구조 파악, LLM을 통한 가치 판단
개발을 시작하기 전 결정적인 깨달음은 이것이었습니다: 내가 확인하고자 하는 속성들은 두 가지 종류로 나뉜다.
| 속성 | 예시 | 결정 요인 |
|---|---|---|
| 구조적 (Structural) (바이트의 형태에 의해 결정됨) | YAML이 유효한가? / 링크 대상이 살아있는가? | 코드 (Code) (grep / 파싱). 100% 정확하며 즉각적임 |
| 의미적 (Semantic) (의도에 대한 이해가 필요함) | 이 설정(config)이 여전히 사용되는가? / 이 런북(runbook)이 여전히 실제로 존재하는 프로세스를 설명하고 있는가? | LLM (판단) |
아무도 더 이상 실행하지 않는 .textlintrc는 코드와 충돌하지 않습니다 (구조적으로는 건강합니다). 하지만 그 가치는 제로입니다. grep은 그 간극을 메울 수 없습니다.
동시에, 처음부터 모든 파일에 LLM을 투입하는 것은 돈 낭비입니다. 그래서 저는 이를 나누었습니다: 열거(enumeration)는 코드, 판단(judgment)은 LLM입니다.
모든 비코드 자산(non-code assets)을 열거
│
▼
...
- tier-1 (코드): grep을 사용하여 "도달 가능성(reachability)"(아래 설명)을 측정하고, 의심스러운 후보군으로 범위를 좁힙니다.
- tier-2 (LLM): 좁혀진 후보군에 대해서만 "이것이 여전히 의미가 있는가"를 판단하여,
Keep(유지) /Update(업데이트) /Retire(폐기) /Merge(병합)라는 판결을 내립니다. 수치 점수는 사용하지 않습니다.
"도달 가능성(reachability)"의 의미
도달 가능성이란 "이 자산을 소비하는 무언가가 여전히 살아있는가"를 의미합니다. 모든 비코드 자산은 무언가가 그것을 소비하고 있기 때문에 살아있는 것입니다.
| 소비자 클래스 | 예시 자산 | 도달 가능성 확인 (grep이 수행하는 작업) |
|---|---|---|
| 도구 호출 (Tool invocation) | .textlintrc와 같은 도구 설정 | package.json 스크립트, pre-commit, Makefile, CI에서 도구 이름을 검색. 호출 지점이 0개라면 후보군이 됨 |
| ... |
예를 들어, "도구 호출" 도달 가능성은 다음과 같이 측정할 수 있습니다:
# .textlintrc가 여전히 "호출"되는지(단순히 설치된 것이 아니라) 확인
grep -rn "textlint" .github/workflows/ .pre-commit-config.yaml Makefile 2>/dev/null
# package.json의 경우, scripts 내부만 확인 (devDependencies에 이름이 있는 것은 "호출"이 아님)
...
이 표와 명령어들은 제가 이 글을 통해 여러분이 가장 얻어갔으면 하는 핵심 내용입니다. 만약 소비자 클래스(consumer class)별로 "소비 연결(자산을 호출하는 모든 연결 방식)이 여전히 온전한가"를 기계적으로 측정한다면, LLM에 무언가를 넘기기 전에 후보군을 대폭 좁힐 수 있습니다.
내가 직접 만든 이유 (기성 제품을 찾아본 후의 결론)
"분명히 이미 이 용도로 쓰이는 도구가 있을 것이다"라고 두 번이나 확인한 결과
처음에는 "기성 제품(off-the-shelf)이 반드시 존재할 것"이라고 가정하고 찾아보았습니다. 그 결론은 계층(layer)에 따라 나뉩니다.
- 구조적 검사(structural checks)의 경우, 기성 옵션이 풍부합니다. MegaLinter와 super-linter는 yamllint, actionlint, markdownlint 등을 묶어서 제공합니다. YAML 유효성 검사에는 yamllint가 있고, 깨진 링크(dead links) 검사에는 markdown-link-check나 Lychee와 같은 전용 도구가 있습니다 (둘 다 MegaLinter에서 호출 가능합니다). 주의할 점은 markdownlint 자체는 깨진 링크를 검사하지 않으므로, 별도의 도구가 필요하다는 것입니다. 이런 것들을 직접 만들 이유는 전혀 없습니다.
- 의미론적 "가치(semantic value)" 검토의 경우, 기성 옵션이 부족합니다. 가장 유사한 것은 상용 서비스인 Dosu이지만, 공개된 정보에 따르면 문서와 코드 사이의 드리프트 탐지(drift detection)는 여러 기능 중 하나일 뿐입니다 (Dosu 자체는 이슈 분류(issue triage) 및 에이전트용 지식 베이스를 더 폭넓게 제안합니다). 또한 어떤 경우에도 이것은 "이 설정이나 워크플로우가 여전히 가치를 지니고 있는가"를 판단하는 메커니즘은 아닙니다.
그리고 그 메커니즘 자체 — "LLM이 자산의 가치를 검토하고 유지(Keep)/폐기(Retire)를 출력한다" — 는 이미 저의 Claude Code 환경 내에서 네 번 구현된 적이 있습니다 (각각 설정(configs), 규칙(rules), 스킬(skills), 프로젝트 문서(project docs)를 대상으로 하는 재고 조사 스킬; 스킬 대상 버전의 설계 과정은 별도의 기사에 작성해 두었습니다). 부족했던 것은 메커니즘이 아니라, 바로 대상(target)이었습니다. "프로젝트 저장소의 비코드 자산(non-code assets)"을 겨냥한 대상은 유일하게 존재하지 않았습니다. 그래서 처음부터 새로 발명하는 대신, 기존 재고 조사 스킬의 골격을 복사하고 대상만 교체했습니다.
구조적 검사는 기성 린터(Linter)에게 맡기고, 부족한 부분인 얇은 "가치 판단(value judgment)" 계층만을 구축하라 — 이것이 결론이었습니다.
설치 방법: Claude Code 스킬로서 설치하기
이 스킬은 독립된 저장소(repository)로 공개되어 있습니다. ~/.claude/skills/ 디렉토리에 넣으면 /repo-asset-stocktake 명령어로 호출할 수 있습니다.
git clone https://github.com/shimo4228/repo-asset-stocktake \\
~/.claude/skills/repo-asset-stocktake
실행하려면 감사(audit)하려는 저장소의 경로를 전달하기만 하면 됩니다.
/repo-asset-stocktake # 현재 디렉토리 감사
/repo-asset-stocktake ~/path/to/repo # 다른 저장소 감사
판정 결과는 감사 대상 저장소 내부의 .repo-asset-stocktake.json 파일에 원장(ledger) 형태로 기록되므로, 이후 실행 시 변경된 자산만 다시 평가할 수 있습니다. 형식은 다음과 같습니다:
{
"evaluated_at": "2026-07-05T05:20:00Z",
"assets": [
...
이 원장 파일을 .gitignore에 추가하면 공개 저장소(public repo)라 하더라도 감사 결과가 유출되지 않습니다.
실제로 실행해 보니 "33개의 고립된 파일이 모인 섬"이 나타났습니다
이를 구축한 후, 제가 가진 또 다른 저장소(iOS 앱)를 대상으로 실행해 보았습니다. 이 저장소에는 CI 워크플로우나 도구 설정(tool configs)이 거의 없으며, 대부분 Markdown 문서로 이루어져 있습니다. 저는 "아무것도 찾지 못하겠군"이라고 생각했습니다. 하지만 정반대의 결과가 나타났습니다.
나온 결과물은 개별적인 쓰레기 파일들이 아니라, 하나의 **구조(structure)**였습니다.
CLAUDE.md (프로젝트 진입점)
├──▶ RUNBOOK.md
├──▶ CONTRIB.md
...
5개월 전 2주간의 집중 개발 기간 동안 생성된 PLAN / PROGRESS / HANDOFF / BUG / REVIEW의 스냅샷들이 docs/plans/와 docs/reports/에 쌓여 있었습니다 — 총 31개의 파일입니다. 이 파일들로 가는 유일한 경로는 단 하나의 체인뿐이었습니다: PROJECT_TIMELINE.md → INDEX.md. 하지만 PROJECT_TIMELINE.md 자체는 CLAUDE.md(프로젝트의 진입점)로부터 링크되어 있지 않았습니다. 이 두 개의 진입점 파일을 포함하면, 진입점으로부터 도달할 수 없는 파일이 33개가 됩니다.
계층-1(tier-1)의 grep과 계층-2(tier-2)의 LLM 사이의 분업이 바로 이 지점에서 빛을 발했습니다.
- 계층-1 (grep): 구조적 사실을 나열합니다 — "
PROJECT_TIMELINE.md에 들어오는 인바운드 링크(inbound links)가 0개임", "INDEX.md가 계획 세트(plan set)로 링크됨". 연결 고리가 고립되어 있다는 사실은 grep을 통해 즉각 도출됩니다. - 계층-2 (LLM): 이러한 사실들을 "전체 섬으로 가는 유일한 다리가 끊어졌다 — 하나의 진입점만 다시 연결하면 33개의 파일이 다시 살아난다"라고 해석하고, 삭제 대신 재연결을 선택합니다.
탐지는 구조적(계층-1)이며, 의미 해석은 LLM(계층-2)이 담당합니다. 열거(enumeration)와 판단(judgment)을 분리한 이유가 이 '섬'에서 정확히 의도한 대로 작동했습니다.
해결책은 삭제가 아니라 하나의 진입점을 재연결하는 것이었습니다. CLAUDE.md에 PROJECT_TIMELINE.md로 연결되는 단 한 줄을 추가함으로써 33개 파일 전체의 이력을 한 번에 구출할 수 있었습니다. 이것은 린트(lint)로는 결코 도출할 수 없는 결론이며, 오직 가치 검토(value review)만이 할 수 있는 결론입니다.
계층-2가 "삭제"보다 더 큰 가치를 전달한 순간
이 기술이 수행한 가장 효과적인 역할은 삭제가 아니라, 삭제를 거부(vetoing)하는 것이었습니다.
tier-1은 이름이 dead-code-analysis.md인 파일을 두 개 발견했고, 이를 '중복(duplicate)' 후보로 표시했습니다. 단순한 파일명 기반 중복 제거(naive basename dedup)만으로는 이 중 하나가 삭제되었을 것입니다.
하지만 tier-2가 그 내용물을 비교했을 때, 하나는 Python의 vulture를 사용한 분석이었고 다른 하나는 수동으로 진행된 Swift 분석이었습니다. 즉, 서로 다른 것들입니다. 이들을 병합하면 분석 내용 중 하나를 잃게 됩니다. 따라서 판결은 '병합 취소(cancel the Merge)'였습니다. 의미론적 계층(semantic layer)이 구조적 신호(structural signal)를 검증하고 뒤집는 것입니다. 이것이 바로 이를 두 개의 티어(tier)로 분리한 정확한 이유입니다.
비슷하게, zenn-content 측면에서는 다른 클래스의 자산을 포착했습니다. .gitignore 파일은 archive/ 폴더가 '게시되어서는 안 된다'고 선언했지만, 이 규칙이 추가되기 전에 커밋된 네 개의 파일은 여전히 추적되고 GitHub에서 계속 게시되는 간극이 있었습니다. 이러한 기술은 도구 설정(tool configs), 워크플로우(workflows), 문서(docs), VCS 설정 등 다양한 소비자 클래스의 자산에 걸쳐 적용될 수 있습니다.
이 패턴을 스킬 없이 실행할 수 있습니다
Claude Code 커스텀 스킬이 없더라도 핵심 원리는 재사용 가능합니다. 자체 에이전트(또는 수동으로)를 사용하여 다음 단계를 순서대로 수행하기만 하면 됩니다:
- 열거 (Enumerate, 코드): 모든 비코드 자산을 나열하고
grep을 사용하여 소비자 클래스별 도달 가능성(reachability)을 측정합니다 (위 '도달 가능성이란 무엇인가' 아래의 표와 명령어 참조). - 좁히기 (Narrow): 도달 가능성이 0인 것들(0 호출 사이트 / 모든 참조가 죽은 것 / 0 인바운드 링크), 그리고
가장 큰 함정은 "도달 가능성 제로(zero reachability)"가 곧 "죽은 자산(dead)"을 의미하지는 않는다는 점입니다.
이 기술을 구축할 때, 저는 다른 모델(Codex)을 병렬로 사용하여 리뷰를 실행했습니다. 그리고 제가 직접 수행한 리뷰(Claude)가 놓쳤던 사각지대를 발견했습니다. 바로 소비(consumption)가 한 단계 간접적으로 이루어질 때, grep 검색 결과가 비어 있게 된다는 사실입니다.
lint-staged를 통해 호출되는 prettier는 직접적인 호출 사이트(invocation site)가 없습니다.- 원격
uses: owner/repo@ref액션은 로컬 복사본이 존재하지 않더라도 살아 있습니다. mkdocs.yml의 네비게이션에 나열된 문서는 마크다운(Markdown) 인바운드 링크가 없더라도 도달 가능합니다..claude/skills/*.md는 인바운드 링크가 0개이지만, 이를 소비하는 주체는 인간이 아니라 Claude Code 로더(loader)입니다.
도달 가능성 제로를 "삭제 대상"이 아니라 **"조사 필요"**로 취급하십시오. 소비 주체를 잘못 판단하면 살아 있는 자산을 죽이게 됩니다. 실제로 이 교정이 없었다면, 저는 iOS 리포지토리의 .claude/skills/*.md 파일 9개를 잘못하여 은퇴(Retired)시켰을 것입니다.
요약
- 비코드 자산의 구조(유효성, 깨진 링크)는 기성 린터(linter)에 맡기십시오. 직접 만들지 마세요.
- LLM은 오직 가치(이것이 여전히 존재할 가치가 있는가)를 판단하는 데에만 얇게 계층화하여 사용하십시오. 모든 것에 LLM을 돌리지 말고, 먼저 grep으로 후보군을 좁히십시오.
- 소비 클래스별(도구 호출 / CI 트리거 / 인간의 탐색)로 도달 가능성을 측정하는 것이 그 좁히기(narrowing)를 위한 기계적 축입니다.
- 도달 가능성 제로는 "삭제"가 아니라 "조사 필요"입니다. 간접적인 소비 주체를 놓치면 살아 있는 자산을 죽이게 됩니다.
- 항상 가역적으로 삭제하십시오(
.disabled로 이동 → 하나씩 확인).
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기