
요즘 제가 AI 바이브 코딩을 꽤 자주 하고 있어요. 예전 같으면 한참 고민하면서 만들 기능도, 이제는 AI랑 대화 몇 번 주고받으면 금방 뼈대가 나오잖아요. 솔직히 이 맛을 한번 보면 다시 예전 속도로 돌아가기가 쉽지 않습니다. 그런데 어느 날, 프로젝트를 열어보는데 뭔가 이상하더라고요. 파일은 많아졌고, 함수도 많아졌고, import 경로는 여기저기 뻗어 있는데 정작 “이거 지금 쓰는 거 맞나?” 싶은 코드가 너무 많은 거예요. 그때부터 좀 찝찝했습니다. 여행 갈 때도 캐리어에 안 입을 옷이 계속 들어 있으면 괜히 무겁잖아요. 코드도 딱 그렇더라고요. 그래서 이번에 Knip을 제대로 써봤고, 생각보다 꽤 도움이 돼서 제 경험을 편하게 풀어보려고 합니다.
AI 바이브 코딩이 빨라질수록 미사용 코드도 같이 빨리 쌓이더라
AI로 코딩하면 확실히 속도는 빨라져요. 이건 부정하기 어렵습니다. 특히 React, TypeScript, Vite 조합으로 화면 만들고 상태 관리 붙이고 API 연결하는 작업은 AI 도움을 받으면 체감상 몇 배는 빨라져요. 문제는 AI가 꽤 친절하다는 겁니다. 너무 친절해요.
예를 들면 제가 “날짜 포맷 유틸 하나 만들어줘”라고 하면, 단순히 formatDate만 만드는 게 아니라 formatDateTime, formatRelativeTime, isToday, isYesterday까지 같이 만들어주는 경우가 많아요. 그 순간에는 “오, 센스 있네?” 싶죠. 그런데 실제 서비스 코드에서는 그중 하나만 쓰고 나머지는 조용히 방치됩니다.
저도 처음에는 별생각 없었어요. 나중에 쓰겠지, 뭐. 개발자들이 자주 하는 그 말 있잖아요. “나중에 필요할 수도 있으니까 일단 놔두자.” 그런데 그 나중은 생각보다 잘 안 오고, 프로젝트는 점점 무거워집니다. IDE에서 자동완성 목록은 지저분해지고, 파일 검색하면 비슷한 이름의 함수가 여러 개 나오고, 새로 기능 붙일 때 “기존에 만들어둔 게 있었나?” 하면서 시간을 까먹게 돼요.
제가 운영하던 사이드 프로젝트는 대략 이런 상태였습니다.
| 항목 | 상태 | 느낌 |
|---|---|---|
| 기술 스택 | React + TypeScript + Vite | 요즘 많이 쓰는 무난한 조합 |
| 파일 수 | 약 200개 이상 | 혼자 관리하기엔 슬슬 부담 |
| AI 활용 기간 | 약 6개월 | 속도는 빨랐지만 코드가 좀 부풀어 있음 |
| 체감 문제 | 빌드 지연, 자동완성 버벅임 | 뭔가 정리가 필요하다는 신호 |
대충 봤을 때는 미사용 파일이 한 10개쯤 있겠지 싶었어요. 그런데 막상 Knip을 돌려보니까 제 예상보다 훨씬 많았습니다. 뭐랄까, 책상 서랍 하나만 정리하려고 열었는데 안쪽에서 오래된 충전기랑 영수증이 계속 나오는 느낌이었어요.
Knip을 써보니 그냥 린터랑은 느낌이 좀 달랐다
Knip은 TypeScript나 JavaScript 프로젝트에서 사용되지 않는 파일, export, 의존성을 찾아주는 도구예요. ESLint도 어느 정도 안 쓰는 변수나 import를 잡아주긴 하죠. 그런데 Knip은 프로젝트 전체를 훑으면서 “이 파일이 실제 진입점에서 이어지는 코드 흐름 안에 들어와 있나?”를 꽤 집요하게 봅니다.
처음엔 저도 반신반의했어요. 개발 도구가 워낙 많잖아요. 설치해보고 두세 번 쓰다가 package.json 한구석에만 남는 도구도 많고요. 그런데 Knip은 첫 실행부터 제법 현실적인 목록을 뽑아줬습니다. 특히 AI가 만들어놓고 아무 데서도 import하지 않은 함수나 컴포넌트를 잘 찾아내더라고요.
설치는 가볍습니다.
npm install -D knip
설치하고 프로젝트 루트에서 바로 실행할 수 있어요.
npx knip
간단한 프로젝트라면 이 정도로도 꽤 볼만한 결과가 나옵니다. 다만 제 프로젝트처럼 라우팅이 복잡하거나 SSR(Server-Side Rendering)이 들어가 있거나, AI가 만든 파일들이 여기저기 흩어져 있는 구조라면 설정 파일을 하나 두는 편이 훨씬 낫습니다.
저는 프로젝트 루트에 knip.json을 만들었어요.
{
"entry": ["src/main.tsx", "src/entry-server.tsx"],
"project": ["src/**/*.{ts,tsx,js,jsx}", "!src/**/*.test.*"],
"ignoreDependencies": ["@tanstack/react-query"],
"ignoreExportsUsedInFile": { "interface": true, "type": true }
}
여기서 제가 제일 신경 쓴 건 entry입니다. Knip은 이 entry를 기준으로 실제로 연결되는 코드를 추적하거든요. 그러니까 앱의 시작점이 제대로 잡히지 않으면, 멀쩡히 쓰고 있는 코드도 미사용이라고 나올 수 있습니다. 저는 클라이언트 진입점인 src/main.tsx와 SSR용 src/entry-server.tsx를 같이 넣었습니다.
그리고 테스트 파일은 일단 제외했어요. 테스트 코드까지 처음부터 같이 보려니까 결과가 너무 시끄럽더라고요. 이런 도구는 처음부터 완벽하게 잡으려 하기보다, 내가 감당할 수 있는 범위부터 줄여가는 게 훨씬 편합니다.
실제로 돌려보니 숫자가 꽤 세게 나왔다
처음 npx knip을 실행했을 때 결과를 보고 잠깐 멈췄습니다. 생각보다 많이 나왔거든요. “아니, 내가 이렇게 대충 살았나?” 싶은 마음도 들고요. 물론 다 지워도 되는 건 아니지만, 목록 자체는 꽤 의미가 있었습니다.
| Knip이 잡아낸 항목 | 개수 | 제가 확인해본 내용 |
|---|---|---|
| 미사용 파일 | 28개 | 대부분 AI가 만들어준 util, helper, 임시 컴포넌트 |
| 미사용 export | 47개 | 함수, type, interface가 섞여 있었음 |
| 미사용 의존성 | 3개 | 설치만 해놓고 실제 import가 없는 패키지 |
재밌는 건 미사용 파일보다 미사용 export가 더 많았다는 점이에요. AI가 코드를 만들 때 재사용성을 고려한답시고 함수를 export로 열어두는 경우가 꽤 많습니다. 그런데 실제 화면이나 서비스 로직에서는 안 쓰는 거죠.
예를 들면 이런 식입니다.
export const formatDate = (date: Date) => {
return date.toISOString().slice(0, 10);
};
export const formatDateTime = (date: Date) => {
return date.toLocaleString();
};
export const formatRelativeTime = (date: Date) => {
return "방금 전";
};
실제로는 formatDate만 쓰고 있었는데, 나머지 두 함수는 아무도 부르지 않고 있었습니다. 코드만 보면 별거 아닌데, 이런 게 프로젝트 전체에 쌓이면 꽤 신경 쓰입니다. 비슷한 함수가 많아지면 나중에 누가 봐도 헷갈려요. 심지어 그 누가가 한 달 뒤의 저일 때가 많고요.
무턱대고 지웠다가 한 번 당했다
여기서 조심해야 할 게 있습니다. Knip이 알려준다고 해서 전부 바로 삭제하면 안 됩니다. 이건 진짜예요. 저도 한번 살짝 당했습니다.
utils/formatDate.ts 파일이 미사용 파일로 표시됐어요. 이름만 봐도 “이건 어디선가 쓰겠지” 싶은 파일이잖아요. 그런데 Knip 결과에는 안 쓰는 파일로 나왔습니다. 처음에는 “아, 옛날 코드였나 보다” 하고 지우려다가 뭔가 찝찝해서 검색을 해봤어요. 알고 보니 dynamic import로 불러오는 구조가 있었습니다.
const formatter = await import("../utils/formatDate");
이런 케이스는 정적 분석 도구가 놓칠 수 있어요. 특히 경로가 변수로 조합되거나, 런타임에 결정되는 구조라면 더 그렇습니다. Knip이 부족하다기보다, 정적 분석이라는 방식 자체의 한계에 가깝습니다.
그래서 저는 그 뒤로 Knip 결과를 볼 때 나름대로 순서를 정했습니다.
- 파일 삭제는 branch를 따고 진행합니다. main에서 바로 지우면 마음이 불편해요.
- 미사용 export부터 정리합니다. 파일 삭제보다 영향 범위가 작아서 시작하기 좋습니다.
- dynamic import, route 기반 로딩, codegen 파일은 한 번 더 봅니다.
- 삭제 후에는 빌드와 테스트를 꼭 실행합니다. 이건 귀찮아도 해야 합니다.
- CSS, GraphQL, 자동 생성 타입은 false positive가 나올 수 있다고 생각하고 접근합니다.
한 번은 styles/theme.css도 미사용으로 보여서 지웠다가 화면 스타일이 살짝 무너진 적이 있어요. 다행히 branch에서 작업 중이라 금방 되돌렸습니다. 그때 다시 느꼈어요. 도구는 좋은 조수지, 최종 결정권자는 개발자여야 합니다.
AI랑 같이 쓰면 Knip이 더 빛난다
제가 Knip을 마음에 들어 한 이유는 단순히 코드 삭제 도구라서가 아니에요. AI 바이브 코딩과 궁합이 꽤 좋습니다. AI가 빠르게 만들어낸 코드의 뒷정리를 Knip이 도와주는 느낌이거든요.
제가 요즘 쓰는 방식은 이렇습니다. 기능을 빠르게 만들 때는 AI 도움을 꽤 적극적으로 받습니다. 화면 구조도 만들고, API 호출 코드도 만들고, 타입도 잡고요. 대신 기능이 어느 정도 동작하면 바로 Knip을 한 번 돌립니다. 그러면 AI가 만들어놓고 실제로 연결하지 않은 코드가 눈에 보입니다.
그리고 결과를 JSON으로 뽑아서 AI에게 다시 물어보기도 합니다.
npx knip --output json > knip-report.json
그다음 AI에게 이런 식으로 요청합니다.
이 Knip 결과를 보고,
삭제해도 비교적 안전해 보이는 항목과
직접 확인이 필요한 항목을 나눠줘.
dynamic import나 codegen 가능성이 있는 파일은 조심스럽게 분류해줘.
이게 은근히 쓸만합니다. 물론 AI 답변을 그대로 믿으면 안 됩니다. 그래도 사람이 긴 목록을 처음부터 다 보는 것보다는 훨씬 덜 피곤해요. 저는 이 과정을 “AI가 어질러놓은 방을 AI와 같이 치우는 느낌”이라고 생각합니다. 좀 웃기지만 실제로 그렇습니다.
제가 써보면서 정착한 Knip 활용 습관
처음에는 Knip을 한 번 실행하고 끝낼 줄 알았어요. 그런데 몇 번 써보니 이건 가끔 대청소하듯 쓰는 것보다, 개발 흐름 안에 살짝 끼워 넣는 게 더 좋더라고요. 집도 1년에 한 번 몰아서 치우면 힘들잖아요. 매일 컵 하나씩만 치워도 훨씬 낫습니다.
Entry files는 조금 귀찮아도 직접 챙긴다
Knip 설정에서 entry는 정말 중요합니다. 특히 React Router나 Next.js처럼 파일 기반 라우팅이 섞인 프로젝트는 진입점을 대충 잡으면 결과가 흔들릴 수 있어요. 저는 화면 단위 파일도 entry에 포함시키는 편입니다.
{
"entry": [
"src/main.tsx",
"src/entry-server.tsx",
"src/pages/**/*.tsx",
"src/routes/**/*.tsx"
]
}
이렇게 잡아두면 AI가 새로 만든 페이지 컴포넌트가 분석 흐름 안에 들어올 확률이 높아집니다. 작은 차이 같지만 결과 품질이 꽤 달라져요.
Monorepo에서는 workspace 모드를 확인한다
회사 프로젝트처럼 패키지가 여러 개로 나뉜 구조라면 Monorepo일 가능성이 높죠. 이럴 때는 패키지 하나만 보고 판단하면 위험합니다. A 패키지에서는 안 쓰는 것처럼 보여도 B 패키지에서 가져다 쓰고 있을 수 있거든요.
npx knip --workspace
이 옵션을 쓰면 workspace 기준으로 의존성을 더 넓게 볼 수 있어서 마음이 조금 편합니다. 물론 이것도 프로젝트 설정에 따라 결과가 달라질 수 있으니, 처음에는 결과를 꼼꼼히 봐야 합니다.
CI에 넣을 때는 너무 빡빡하게 시작하지 않는다
Knip을 GitHub Actions에 넣는 것도 좋습니다. PR이 올라올 때마다 미사용 코드가 생겼는지 확인할 수 있으니까요.
npx knip --ci
다만 기존 프로젝트에 갑자기 넣고 바로 실패 처리하면 팀원들이 싫어할 수 있습니다. 저라도 싫을 것 같아요. 그래서 저는 처음에는 report만 남기고, 어느 정도 정리된 뒤에 CI 실패 조건으로 바꾸는 방식을 좋아합니다. 도구는 팀을 편하게 해야지, 괜히 분위기를 험악하게 만들면 안 되니까요.
| 단계 | 운영 방식 | 제가 느낀 장점 |
|---|---|---|
| 초기 도입 | 로컬에서 수동 실행 | false positive를 파악하기 좋음 |
| 정리 기간 | CI에서 report만 생성 | 팀에 부담이 덜함 |
| 안정화 이후 | npx knip --ci로 실패 처리 |
미사용 코드가 다시 쌓이는 걸 막기 좋음 |
정리하고 나니 실제로 체감이 있었다
Knip으로 한 번 크게 정리하고 나서 수치상으로도 변화가 있었습니다. 아주 드라마틱한 수준은 아니지만, 개발할 때 느껴지는 답답함이 줄었어요.
| 항목 | 정리 전 | 정리 후 | 체감 |
|---|---|---|---|
| 빌드 시간 | 약 12초 | 약 8초 | 기다리는 시간이 확실히 줄었음 |
| 초기 번들 크기 | 기준값 | 약 15% 감소 | 불필요한 코드 제거 효과가 있었음 |
| IDE 반응 | 가끔 버벅임 | 조금 더 가벼움 | 자동완성 목록이 덜 지저분해짐 |
개인적으로 제일 좋았던 건 수치보다 마음의 부담이 줄었다는 점이에요. 코드를 열었을 때 “이거 살아 있는 코드인가?”라는 의심이 줄어듭니다. 이게 은근히 큽니다. 오래 개발하다 보면 코드 자체보다 코드에 대한 불신이 사람을 더 피곤하게 만들거든요.
그리고 AI에게 요청하는 방식도 조금 바뀌었습니다. 예전에는 “이 기능 만들어줘”라고만 했다면, 요즘은 뒤에 조건을 붙입니다.
실제로 사용하는 함수만 만들어줘.
나중에 쓸 것 같은 helper는 만들지 말고,
필요해지면 그때 추가할게.
export도 외부에서 쓰는 것만 열어줘.
이렇게 말하면 AI가 불필요하게 넓은 코드를 덜 만드는 편입니다. 완벽하진 않지만 꽤 차이가 있어요. 사람도 지시가 구체적이면 일을 더 잘하잖아요. AI도 비슷합니다.
Knip이 잘 맞는 사람, 조금 조심해야 할 사람
제가 써본 기준으로 Knip은 이런 분들에게 꽤 잘 맞습니다.
- AI 바이브 코딩으로 빠르게 기능을 만들고 있는 개발자
- React, TypeScript, Vite 기반 프로젝트를 오래 굴리고 있는 사람
- 파일은 많은데 뭐가 실제로 쓰이는지 감이 안 오는 레거시 프로젝트 담당자
- 미사용 dependency를 정리해서 package.json을 가볍게 만들고 싶은 사람
- PR 단계에서 불필요한 코드가 들어오는 걸 줄이고 싶은 팀
반대로 이런 프로젝트라면 조금 천천히 접근하는 게 좋습니다.
- dynamic import를 많이 쓰는 프로젝트
- 런타임에 경로를 조합해서 모듈을 불러오는 구조
- GraphQL codegen, OpenAPI generator 같은 자동 생성 파일이 많은 경우
- CSS module, CSS-in-JS, 전역 스타일 참조가 복잡한 프로젝트
- 테스트 커버리지가 낮아서 삭제 후 검증이 어려운 프로젝트
이런 경우에도 못 쓰는 건 아닙니다. 다만 Knip 결과를 “삭제 명령서”처럼 보면 안 되고, “점검 목록”처럼 보는 게 좋습니다. 저는 이 관점 차이가 꽤 중요하다고 봐요. 도구가 알려주는 건 단서이고, 판단은 사람이 해야 합니다.
제가 느낀 Knip의 진짜 장점
사실 미사용 코드 정리는 예전에도 할 수 있었습니다. 검색하고, import 따라가고, IDE 기능 쓰고, 빌드 돌리고. 문제는 귀찮다는 거죠. 그리고 바쁠 때는 이런 정리 작업이 늘 뒤로 밀립니다.
Knip의 장점은 “정리를 시작하게 만들어준다”는 데 있습니다. 막연히 지저분하다고 느끼던 프로젝트에 숫자와 목록을 보여줘요. 그럼 손을 댈 수 있습니다. 개발자에게는 이게 은근히 중요합니다. 감으로 불편한 상태와 목록으로 보이는 문제는 완전히 다르거든요.
저는 요즘 기능 하나를 크게 만들고 나면 이렇게 마무리합니다.
- 빌드가 정상인지 확인합니다.
- 간단한 화면 동작을 직접 눌러봅니다.
npx knip을 한 번 돌립니다.- 미사용 export를 먼저 정리합니다.
- 파일 삭제는 branch에서 조심스럽게 합니다.
- 정리 결과를 commit으로 따로 남깁니다.
이렇게 해두면 나중에 문제가 생겨도 추적이 편합니다. 기능 추가 commit과 정리 commit을 섞어버리면 rollback할 때 꽤 피곤해지거든요. 20년 가까이 개발하면서 배운 건데, 코드는 잘 짜는 것도 중요하지만 되돌리기 쉽게 남기는 것도 정말 중요합니다.
AI 시대에는 코드를 잘 만드는 것만큼 잘 버리는 것도 중요하다
AI 덕분에 코드를 만드는 비용은 많이 낮아졌습니다. 예전에는 함수 하나 만들 때도 꽤 고민했는데, 이제는 몇 초 만에 만들어지죠. 그래서 오히려 이제는 무엇을 남기고 무엇을 버릴지가 더 중요해진 것 같아요.
저는 Knip을 쓰면서 그 생각을 많이 했습니다. 코드를 많이 만든다고 좋은 프로젝트가 되는 건 아니더라고요. 진짜 좋은 프로젝트는 필요한 코드가 필요한 자리에 있고, 안 쓰는 코드는 미련 없이 빠지는 쪽에 가깝습니다. 여행 짐도 비슷하잖아요. 혹시 몰라서 다 챙기면 몸만 힘듭니다. 막상 가보면 자주 쓰는 건 몇 개 안 돼요.
Knip은 AI 바이브 코딩을 하는 개발자에게 꽤 괜찮은 안전장치입니다. 특히 빠르게 프로토타입을 만들고, 이후에 제품 코드로 다듬어야 하는 상황이라면 한 번쯤 꼭 써볼 만합니다. 오래된 레거시 프로젝트를 맡았는데 어디서부터 손대야 할지 막막한 분에게도 좋고요.
다만 너무 믿고 막 지우지는 마세요. branch 따고, 테스트 돌리고, dynamic import 의심하고, CSS나 codegen 파일은 한 번 더 보고요. 이 정도만 지켜도 꽤 든든합니다.
이 글은 AI로 개발 속도는 빨라졌는데 프로젝트가 점점 어수선해지는 분, TypeScript 프로젝트에서 미사용 코드를 정리하고 싶은 분, 그리고 “내가 만든 코드인지 AI가 만든 코드인지 모르겠지만 아무튼 정리가 필요하다” 싶은 분이 읽으면 딱 좋습니다. 저도 앞으로 새 기능 붙일 때마다 Knip을 한 번씩 돌려볼 생각이에요. 뭐랄까, 개발 끝나고 책상 한번 슥 닦는 기분이라 꽤 개운합니다.
댓글
댓글 쓰기