본문 바로가기
튜토리얼/웹

[React] GitHub 프로필 검색기 만들기 (GitHub API)

by 루까(Luka) 2026. 3. 1.
반응형

GitHub 프로필 검색기 만들기

왜 만들었나

외부 API를 React로 다루는 가장 직관적인 예제가 GitHub API다. 인증 없이 기본 60회/시간 요청이 가능하고, 응답 스키마가 일관적이다. 프로필 + 레포 두 엔드포인트만으로 의미있는 UI를 만들 수 있다.

구현 목표:

  1. 사용자명 검색 → GitHub REST API 호출
  2. 프로필 카드 (아바타, 이름, 바이오, 위치, 링크)
  3. 통계 바 (레포 수, 팔로워, 팔로잉, Gist)
  4. 인기 레포 그리드 (스타 순 정렬)
  5. 로딩 / 에러 / 빈 상태 처리
  6. Rate Limit 잔여량 표시

구조

09-react-github-profile/
└── index.html

React 18 CDN + Babel Standalone. 빌드 없이 단일 HTML 파일로 완성.


핵심 구현

API 호출 흐름

GitHub API 엔드포인트 두 개를 순서대로 호출한다.

async function handleSearch(username) {
  setLoading(true);
  setError(null);

  try {
    // 1. 유저 정보
    const userRes = await fetch(
      `https://api.github.com/users/${encodeURIComponent(username)}`
    );

    if (userRes.status === 404) throw new Error(`"${username}" 사용자를 찾을 수 없습니다.`);
    if (userRes.status === 403) throw new Error('API 요청 한도에 도달했습니다.');
    if (!userRes.ok) throw new Error('GitHub API 요청 실패: ' + userRes.status);

    const userData = await userRes.json();
    setUser(userData);

    // 2. 레포 목록 (스타 순 정렬, 최대 6개)
    const reposRes = await fetch(
      `https://api.github.com/users/${encodeURIComponent(username)}/repos?sort=stars&per_page=6&type=owner`
    );
    if (reposRes.ok) setRepos(await reposRes.json());

  } catch (err) {
    setError(err.message);
  } finally {
    setLoading(false);
  }
}

유저 API와 레포 API를 분리한 이유: 유저가 없으면 레포 요청 자체가 무의미하다. 에러 핸들링도 단계별로 명확하게 분리된다. encodeURIComponent는 특수문자가 포함된 사용자명을 안전하게 처리한다.

Rate Limit 헤더 읽기

const remaining = userRes.headers.get('X-RateLimit-Remaining');
const limit      = userRes.headers.get('X-RateLimit-Limit');
if (remaining !== null) setRateLimit({ remaining, limit });

GitHub API는 응답 헤더에 X-RateLimit-Remaining을 포함한다. 미인증 요청은 시간당 60회 제한이고, 이를 UI에 표시하면 디버깅에 유용하다.

언어별 컬러 매핑

const LANG_COLORS = {
  JavaScript: '#f1e05a',
  TypeScript: '#3178c6',
  Python:     '#3572A5',
  HTML:       '#e34c26',
  CSS:        '#563d7c',
  Go:         '#00ADD8',
  Rust:       '#dea584',
  // ...
  default:    '#8b949e',
};

function getLangColor(lang) {
  return LANG_COLORS[lang] || LANG_COLORS.default;
}

GitHub 웹사이트와 동일한 언어 컬러를 사용한다. 정의되지 않은 언어는 회색(default)으로 처리한다.

블로그/사이트 URL 정규화

function normalizeUrl(url) {
  if (!url) return null;
  return url.startsWith('http') ? url : 'https://' + url;
}

GitHub 프로필의 blog 필드는 https:// 없이 등록된 경우가 많다. <a href> 속성에 그대로 넣으면 상대 경로로 처리돼서 깨진다. 정규화 함수로 일괄 처리한다.


API 응답 구조

/users/{username}

{
  "login":          "torvalds",
  "avatar_url":     "https://...",
  "name":           "Linus Torvalds",
  "bio":            "Just a random computer programmer",
  "location":       "Portland, OR",
  "company":        "Linux Foundation",
  "blog":           "https://...",
  "twitter_username": null,
  "public_repos":   7,
  "followers":      237000,
  "following":      0,
  "public_gists":   0,
  "html_url":       "https://github.com/torvalds"
}

/users/{username}/repos?sort=stars&per_page=6

[{
  "name":             "linux",
  "description":      "Linux kernel source tree",
  "language":         "C",
  "stargazers_count": 190000,
  "forks_count":      55000,
  "fork":             false,
  "html_url":         "https://github.com/torvalds/linux"
}]

비교 테이블

상태 조건 표시
초기 user = null, error = null 검색 안내 메시지
로딩 loading = true 스피너
에러 error != null 에러 메시지
성공 user != null 프로필 + 레포

API 상태 코드별 처리:

코드 원인 처리
200 정상 프로필 렌더
404 사용자 없음 "찾을 수 없습니다"
403 Rate Limit 초과 "잠시 후 시도" 안내
기타 서버 오류 상태 코드 표시

삽질 기록

encodeURIComponent 빠뜨리기

사용자명에 하이픈이나 점이 포함될 수 있다. URL에 직접 넣으면 대부분 동작하지만, 만약 특수문자가 섞인 입력이 들어오면 API 요청이 망가진다. encodeURIComponent로 감싸는 게 맞다.

유저 API와 레포 API를 병렬로 실행하면?

Promise.all로 동시에 요청하면 더 빠르다. 하지만 유저가 없는 경우(404)에도 레포 요청이 나가 불필요한 API 소비가 생긴다. 순차 처리가 더 합리적이다.

blog 필드 URL 처리

GitHub 프로필의 blog 필드는 프로토콜 없이 example.com만 입력해도 저장된다. <a href="example.com">은 현재 도메인 기준 상대경로로 해석된다. normalizeUrlhttps://를 붙여서 해결했다.

레포 정렬: sort=stars의 함정

sort=stars는 해당 사용자 소유 레포를 스타 순으로 반환한다. Fork된 레포도 포함된다. type=owner를 추가하면 직접 만든 레포만 필터링된다.


마무리

GitHub REST API는 문서가 잘 정리되어 있고, 인증 없이도 기본 기능을 테스트하기에 충분하다. fetch + async/await + 상태 관리 패턴을 CDN React로 간결하게 구현할 수 있다. 다음 단계로는 localStorage 캐싱(동일 사용자 재검색 시 캐시 활용), 검색 히스토리, Infinite Scroll(레포 페이지네이션) 같은 기능을 붙여볼 수 있다.


기술 스택: React 18 CDN · Babel Standalone · GitHub REST API · Vanilla CSS
소스 코드: GitHub
데모: GitHub Pages

반응형