라인 수에서 사람이 쓰지 않은 줄을 빼는 방법
패키지 하나를 설치했을 뿐인데 수천 줄을 쓴 사람이 되기도 해요. 잠금 파일, 빌드 결과물, 자동 생성 파일을 가려내는 규칙을 정리했어요.
기여도를 라인 수로 계산하면 이상한 결과가 자주 나와요. 프로젝트 초기에 세팅만 한 사람이 라인 수 기준으로 80%를 차지하는 식이에요. 열어 보면 이유는 뻔해요. package-lock.json 한 파일이 만 줄이 넘어요.
사람이 쓴 줄과 도구가 만든 줄을 구분하지 않으면 라인 수는 아무 의미가 없어요. WhoIsLoafing에서 이 둘을 어떻게 가려내는지 정리했어요.
빼려면 파일 단위로 알아야 해요
GitHub의 기여자 통계 API는 사람별 추가 라인과 삭제 라인의 합계만 줘요. 어떤 파일에서 나온 줄인지는 알 수 없어요. 그래서 특정 파일을 빼려면 커밋을 하나씩 받아야 해요.
GET /repos/{owner}/{repo}/commits/{sha}
이 응답에는 커밋이 건드린 파일마다 파일 이름, 추가 라인, 삭제 라인이 들어 있어요. 파일 이름을 보고 뺄지 정한 다음, 남은 파일의 줄 수만 더해요.
for (const file of detail.files ?? []) {
if (options.exclude && isExcludedFile(file.filename)) continue
person.additions += file.additions
person.deletions += file.deletions
}
대가는 요청 수예요. 통계 API는 요청 한 번이면 되지만, 이 방법은 커밋 수만큼 요청이 나가요. 그래서 최근 300개 커밋까지만 읽고, 동시에 6개씩만 요청해요. 한꺼번에 300개를 던지면 GitHub의 남용 방지 제한에 걸릴 수 있기 때문이에요.
네 가지를 빼요
1. 잠금 파일
패키지 관리자가 설치한 버전을 기록해 두는 파일이에요. 사람이 직접 고치지 않고, 패키지 하나만 추가해도 수백 줄씩 바뀌어요.
const LOCK_FILES = new Set([
'package-lock.json', 'yarn.lock', 'pnpm-lock.yaml', 'bun.lock',
'cargo.lock', 'gemfile.lock', 'poetry.lock', 'uv.lock',
'composer.lock', 'go.sum', 'podfile.lock', 'pubspec.lock',
])
언어마다 이름이 달라서 목록으로 관리해요. JavaScript뿐 아니라 Rust, Ruby, Python, PHP, Go, Swift, Dart까지 넣어 뒀어요. 파일 이름은 소문자로 바꿔서 비교해요. Cargo.lock과 Gemfile.lock처럼 대문자로 시작하는 이름이 있기 때문이에요.
2. 빌드 결과물과 외부 코드 폴더
원래는 저장소에 올리지 않는 폴더인데, 실수로 올라가 있는 경우가 생각보다 많아요.
const BUILD_DIRS = new Set([
'dist', 'build', 'out', 'node_modules', 'vendor', '.next', '.nuxt',
'coverage', 'target', '__pycache__', '.gradle', 'pods',
])
경로를 /로 쪼갠 다음, 파일 이름을 뺀 폴더 중에 이 이름이 하나라도 있으면 빼요. 그래서 dist/app.js뿐 아니라 packages/web/dist/app.js처럼 깊은 곳에 있어도 걸러져요.
3. 자동 생성 파일
폴더가 아니라 파일 이름의 끝부분으로 알아보는 것들이에요.
const GENERATED_PATTERN =
/(\.min\.(js|css)|\.map|\.snap|\.pb\.go|_pb2(_grpc)?\.py|\.g\.dart|\.freezed\.dart|\.generated\.[a-z]+|\.designer\.cs)$/
압축된 JavaScript와 CSS, 소스맵, 테스트 스냅샷, 프로토콜 버퍼에서 만들어진 코드, Dart의 코드 생성 결과물 같은 것들이에요.
4. 바이너리 파일
이미지, 글꼴, 압축 파일, 실행 파일은 줄 수라는 개념이 맞지 않아요. GitHub도 이런 파일은 대부분 줄 수를 세지 않지만, 혹시 잡히더라도 섞이지 않게 확장자로 한 번 더 걸러요.
하나의 함수로 판단해요
네 가지 규칙은 함수 하나에 모여 있어요.
export function isExcludedFile(path: string): boolean {
const lower = path.toLowerCase()
const segments = lower.split('/')
const fileName = segments[segments.length - 1]
if (LOCK_FILES.has(fileName)) return true
if (segments.slice(0, -1).some((dir) => BUILD_DIRS.has(dir))) return true
return GENERATED_PATTERN.test(fileName) || BINARY_PATTERN.test(fileName)
}
규칙을 한곳에 모아 두면 새 언어나 도구가 나왔을 때 이 파일만 고치면 돼요.
규칙이 놓치는 것
이름으로만 판단하기 때문에 한계가 분명해요.
build라는 이름의 폴더에 사람이 쓴 빌드 스크립트를 두는 프로젝트가 있어요. 이런 파일은 억울하게 빠져요.- 이름에 표시가 없는 자동 생성 파일은 걸러내지 못해요. 예를 들어 API 명세에서 뽑아낸 클라이언트 코드가 평범한 이름으로 저장되어 있으면 사람이 쓴 것으로 세요.
- 코드 정리 도구를 한 번 돌려서 전체 파일의 형식이 바뀐 커밋도 그대로 세요.
완벽하게 맞추려고 규칙을 계속 늘리면 오히려 예측하기 어려워져요. 그래서 누가 봐도 사람이 쓰지 않은 것만 빼는 선에서 멈췄어요.
사용자에게 고르게 한 이유
이 계산을 기본값으로 하지 않고, 분석을 시작하기 전에 물어봐요.
- 전부 포함하기: 통계 API를 써서 빨라요.
- 빼고 계산하기: 커밋을 하나씩 읽어서 느리지만 정확해요.
속도와 정확도 중 무엇이 중요한지는 상황마다 달라요. 어떤 레포인지 가볍게 훑어보려는 사람에게 30초를 기다리게 할 이유가 없고, 포트폴리오에 넣을 숫자를 뽑으려는 사람에게는 정확한 쪽이 맞아요. 그래서 선택을 사용자에게 넘겼어요.
커밋이 300개를 넘는 레포에서는 최근 300개만 본다는 점도 결과에 같이 알려줘요. 계산 범위를 숨기면 숫자를 잘못 읽게 되기 때문이에요.
정리
- 라인 수는 파일 단위로 봐야 걸러낼 수 있어요.
- 잠금 파일, 빌드 폴더, 자동 생성 파일, 바이너리 파일 네 가지를 빼요.
- 규칙은 한 함수에 모으고, 애매한 것까지 욕심내지 않아요.
- 느린 계산은 필요한 사람만 고르게 해요.