팀 프로젝트 기여도를 대화로 풀어 주는 서비스를 만든 이유
WhoIsLoafing이 어떤 문제에서 출발했고, 레포 링크 하나가 분석 결과가 되기까지 어떤 단계를 거치는지 전체 구조를 정리했어요.
팀 프로젝트가 끝나면 꼭 나오는 질문이 있어요. "그래서 누가 뭘 했지?" 답은 이미 GitHub에 다 남아 있는데, 막상 확인하려면 커밋 수백 개를 하나씩 열어 봐야 해요. 저희 숨은빚청년들 팀이 WhoIsLoafing을 만든 이유예요.
이 글에서는 서비스가 무엇을 하는지와, 레포 링크 하나가 분석 결과가 되기까지의 과정을 처음부터 끝까지 훑어볼게요. 뒤에 이어지는 글들은 각 단계에서 부딪힌 문제를 하나씩 자세히 다뤄요.
무엇을 하는 서비스인가요
공개 GitHub 레포 링크를 넣으면 아래 내용을 채팅 형식으로 풀어서 보여줘요.
- 어떤 서비스의 어떤 레포인지 한 문장 정의와 기술 스택
- 참여자별 커밋 수, 추가하고 삭제한 라인 수, 기여도
- 참여자마다 맡은 기능 목록과 코드 스타일
- 활동 시간대, 진행 흐름, 커밋 메시지 규칙, PR 현황 같은 추가 질문의 답
로그인은 없어요. 공개 레포만 분석하고, 비공개 레포는 서버 토큰으로 볼 수 있더라도 결과를 내보내지 않아요.
숫자만 보여주면 안 되는 이유
GitHub의 Insights 탭에도 기여자별 커밋 수와 라인 수가 나와요. 그런데 그 숫자만으로는 알 수 있는 것이 별로 없어요.
- 패키지 잠금 파일 하나가 수천 줄을 차지해서 라인 수가 부풀어요.
- 같은 사람이 이메일 두 개로 커밋하면 두 사람으로 나뉘어요.
- 커밋 30개가 무엇을 만든 것인지는 숫자가 말해 주지 않아요.
그래서 목표를 숫자를 보여주는 것이 아니라 숫자가 뜻하는 바를 설명하는 것으로 잡았어요. 숫자는 직접 계산하고, 그 숫자와 커밋 내용을 AI에 넘겨서 "누가 무엇을 맡았는지"를 사람이 읽는 문장으로 받아요.
전체 흐름
레포 링크가 들어오면 서버는 아래 순서로 움직여요.
- 레포 확인: 링크가 GitHub 레포 주소인지 판별하고, 레포가 있는지와 공개 레포인지 확인해요.
- 레포 소개: README, 폴더 구조, 사용 언어, 의존성 파일을 읽어서 한 문장 정의와 기술 스택을 만들어요.
- 메뉴: 무엇부터 볼지 물어봐요. 기여도 분석을 바로 시작할 수도 있고, 궁금한 질문만 골라서 볼 수도 있어요.
- 수치 수집: GitHub 통계 API로 사람별 커밋 수와 라인 수를 가져와요. 잠금 파일을 빼고 계산하기를 고르면 커밋을 하나씩 읽어요.
- 사람 합치기: 여러 이메일로 커밋한 사람을 GitHub 계정 기준으로 합치고, 병합 커밋과 봇은 빼요.
- 순위: 커밋 수 기준과 라인 수 기준 기여도를 각각 계산해요.
- 기능과 코드 스타일: 참여자마다 대표 커밋을 골라 diff를 AI에 보내고, 맡은 기능과 코드 스타일을 목록으로 받아요.
- 전체 비교: 두 가지 기준의 기여도를 막대 그래프로 나란히 보여줘요.
각 단계의 결과는 준비되는 대로 한 말풍선씩 브라우저로 흘려보내요. 분석이 다 끝날 때까지 빈 화면을 보여주지 않으려는 선택이에요.
기술 스택
| 영역 | 선택 |
|---|---|
| 프론트엔드 | React, TypeScript, Vite, Tailwind CSS |
| 서버 | Express와 Cloudflare Workers에서 같은 로직을 실행 |
| 저장소 | Supabase(Postgres) |
| AI | Claude API, 구조화된 출력으로 JSON을 받고 zod로 검증 |
| 외부 API | GitHub REST API |
GitHub와 Claude 호출은 모두 서버에서 해요. 키와 토큰이 브라우저로 나가지 않게 하려는 것이에요.
기여도를 하나의 숫자로 만들지 않았어요
가장 오래 고민한 부분이에요. "기여도 37%"처럼 숫자 하나로 보여주면 보기에는 깔끔하지만, 그 숫자를 어떻게 만들든 누군가에게는 불공평해요.
- 커밋 수로 계산하면 작게 자주 올리는 사람이 유리해요.
- 라인 수로 계산하면 화면 코드를 많이 쓰는 사람이 유리해요.
그래서 커밋 수 기준과 라인 수 기준을 따로 계산해서 나란히 보여주기로 했어요. 소개 순서만 두 값의 평균으로 정해요.
const percent = (part: number, total: number) =>
total > 0 ? Math.round((part / total) * 1000) / 10 : 0
people
.map((p) => ({
...p,
commitShare: percent(p.commits, totalCommits),
lineShare: percent(p.additions + p.deletions, totalLines),
}))
.sort((a, b) => b.commitShare + b.lineShare - (a.commitShare + a.lineShare))
두 숫자가 크게 어긋나는 사람이 있으면 그 자체가 읽을거리가 돼요. 한 번에 크게 올리는 사람인지, 자잘한 수정을 주로 맡은 사람인지가 드러나기 때문이에요.
사람을 평가하는 도구가 되지 않게
서비스 이름이 "누가 놀고 있나요"라서 오해받기 쉬운데, 누군가를 지목하려고 만든 도구는 아니에요. 그래서 몇 가지 원칙을 정했어요.
- 기여도 숫자는 항상 맡은 기능 목록과 함께 보여줘요.
- AI에게 보내는 지시문에 "사람을 깎아내리거나 평가하지 않고 사실을 담백하게 전해요"라고 적어 뒀어요.
- 자료에서 확인되는 내용만 말하게 해요. 당사자가 직접 읽는 글이라 틀린 내용은 바로 드러나기 때문이에요.
기획, 디자인, 회의처럼 커밋에 남지 않는 기여는 이 서비스가 볼 수 없어요. 이 한계를 숨기지 않는 것도 원칙 중 하나예요.
다음 글에서
여기까지가 전체 그림이에요. 이어지는 글에서는 각 단계에서 만난 구체적인 문제를 다뤄요. GitHub 통계 API가 결과 대신 "아직 계산 중"이라고 답할 때 어떻게 했는지부터 시작할게요.