Articles

GitHub 통계 API가 200 대신 202를 돌려줄 때

기여자 통계 API는 처음 부르면 결과 대신 "계산 중"이라고 답해요. 기다리는 방법과, 끝내 오지 않을 때를 대비한 방법을 정리했어요.

GitHub에서 사람별 커밋 수와 라인 수를 한 번에 받을 수 있는 API가 있어요.

GET /repos/{owner}/{repo}/stats/contributors

요청 한 번으로 기여자마다 주 단위의 추가 라인, 삭제 라인, 커밋 수가 돌아와요. 커밋을 하나씩 읽지 않아도 되니 가장 빠른 방법이에요. 그런데 이 API에는 처음 써 보면 당황하게 되는 특징이 있어요.

처음 부르면 빈 응답이 와요

한동안 아무도 조회하지 않은 레포에 이 API를 부르면 상태 코드 202와 함께 빈 본문이 돌아와요. 오류가 아니에요. "요청은 받았고, 지금 통계를 계산하고 있으니 잠시 뒤에 다시 불러 주세요"라는 뜻이에요.

GitHub는 이 통계를 미리 만들어 두지 않고, 누군가 요청하면 그때 계산을 시작해서 결과를 캐시해 둬요. 그래서 첫 요청은 거의 항상 202를 받아요.

간격을 두고 다시 물어봐요

해결은 단순해요. 잠깐 기다렸다가 다시 부르는 것이에요. WhoIsLoafing은 3초 간격으로 최대 8번까지 다시 시도해요.

const STATS_RETRY_DELAY_MS = 3000
const STATS_MAX_ATTEMPTS = 8

async function getContributorStats(owner: string, repo: string) {
  for (let attempt = 0; attempt < STATS_MAX_ATTEMPTS; attempt++) {
    const res = await request(`/repos/${owner}/${repo}/stats/contributors`)
    if (res.status === 200) {
      const data: unknown = await res.json()
      return Array.isArray(data) ? data : []
    }
    if (res.status === 204) return []
    if (res.status !== 202) return null
    await sleep(STATS_RETRY_DELAY_MS)
  }
  return null
}

상태 코드마다 뜻이 달라서 따로 처리해요.

상태 코드 뜻 처리
200 통계가 준비됐어요 결과를 써요
202 계산하는 중이에요 3초 뒤에 다시 물어봐요
204 내용이 없어요 빈 목록으로 다뤄요
그 밖 예상하지 못한 응답이에요 포기하고 다른 방법으로 넘어가요

200이 왔더라도 본문이 배열인지 한 번 더 확인해요. 계산이 덜 끝난 상태에서 빈 객체가 오는 경우가 있기 때문이에요.

기다리는 동안 화면이 멈춰 보이면 안 돼요

최대 24초를 기다릴 수 있다는 뜻이라, 그동안 사용자가 고장 났다고 느끼지 않게 해야 해요. 통계를 요청하기 전에 "통계를 가져오는 중이에요"라는 말풍선을 먼저 보내고, 말풍선 사이에는 입력 중 표시와 함께 "커밋 기록 뒤적이는 중" 같은 문구를 번갈아 보여줘요.

서버와 브라우저 사이의 연결이 오래 조용하면 중간의 프록시가 연결을 끊기도 해요. 그래서 15초마다 내용 없는 신호를 한 줄씩 보내서 연결을 살려 둬요.

끝내 오지 않을 때를 대비해요

8번을 물어봐도 통계가 준비되지 않는 레포가 있어요. 커밋이 아주 많은 레포에서 특히 그래요. 또 통계는 왔는데 라인 수가 전부 0으로 비어 있는 경우도 있어요.

이럴 때 "분석할 수 없어요"로 끝내지 않고 커밋 단위 분석으로 넘어가요. 최근 커밋 300개를 하나씩 읽어서 사람별 수치를 직접 계산하는 방법이에요. 요청이 훨씬 많이 나가고 느리지만, 결과를 못 보여주는 것보다는 나아요.

정리하면 수치를 얻는 길이 두 개예요.

  1. 통계 API: 빠르고 요청이 적어요. 대신 준비가 안 됐을 수 있고, 어떤 파일의 줄 수인지는 알 수 없어요.
  2. 커밋 단위 분석: 느리고 요청이 많아요. 대신 항상 되고, 파일마다 줄 수를 알 수 있어서 잠금 파일 같은 것을 뺄 수 있어요.

사용자가 "잠금 파일과 빌드 결과물을 빼고 계산하기"를 고르면 처음부터 두 번째 길로 가요.

호출 제한도 같이 봐야 해요

다시 시도하는 코드를 넣을 때 조심할 점이 있어요. 호출 제한에 걸린 응답을 "잠깐 기다리면 되는 응답"으로 착각하면 안 돼요. GitHub는 제한에 걸리면 403이나 429를 돌려주는데, 403은 권한이 없을 때도 오기 때문에 헤더와 본문을 같이 봐야 구분할 수 있어요.

const rateLimited =
  (res.status === 403 || res.status === 429) &&
  (res.headers.get('x-ratelimit-remaining') === '0' ||
    res.headers.has('retry-after') ||
    /rate limit/i.test(body))

제한에 걸렸다면 x-ratelimit-reset이나 retry-after 헤더에서 언제 풀리는지 읽어서, 사용자에게 "몇 분 뒤에 다시 시도해 주세요"라고 구체적으로 알려줘요. 막연한 오류 문구보다 훨씬 도움이 돼요.

통계 API의 숫자를 그대로 믿으면 안 되는 경우

마지막으로 하나 더 있어요. 통계 API는 가끔 GitHub 계정에 연결되지 않은 커밋을 엉뚱한 계정의 몫으로 돌려줘요. 커밋 목록에는 한 번도 작성자로 나오지 않는 계정이 통계에만 등장하는 식이에요.

커밋이 300개 미만인 레포라면 커밋 목록 전체를 받을 수 있어서, 통계와 대조해 원래 주인에게 돌려줘요. 이 이야기는 다음 글에서 자세히 다뤄요.

정리

  • stats/contributors의 202는 오류가 아니라 "계산 중"이에요.
  • 간격을 두고 다시 물어보되, 횟수에 끝을 정해요.
  • 끝내 오지 않을 때 갈 수 있는 두 번째 길을 마련해 둬요.
  • 기다리는 동안 사용자에게 무슨 일이 일어나고 있는지 알려줘요.
다음 글 이메일이 여러 개인 사람을 한 명으로 합치는 방법