GitHub 프로필 검색기 만들기
왜 만들었나
외부 API를 React로 다루는 가장 직관적인 예제가 GitHub API다. 인증 없이 기본 60회/시간 요청이 가능하고, 응답 스키마가 일관적이다. 프로필 + 레포 두 엔드포인트만으로 의미있는 UI를 만들 수 있다.
구현 목표:
- 사용자명 검색 → GitHub REST API 호출
- 프로필 카드 (아바타, 이름, 바이오, 위치, 링크)
- 통계 바 (레포 수, 팔로워, 팔로잉, Gist)
- 인기 레포 그리드 (스타 순 정렬)
- 로딩 / 에러 / 빈 상태 처리
- Rate Limit 잔여량 표시
구조
09-react-github-profile/
└── index.htmlReact 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">은 현재 도메인 기준 상대경로로 해석된다. normalizeUrl로 https://를 붙여서 해결했다.
레포 정렬: 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
'튜토리얼 > 웹' 카테고리의 다른 글
| [React] 날씨 앱 만들기 (Open-Meteo API · API 키 불필요) (0) | 2026.03.02 |
|---|---|
| [React] 영화 검색 앱 만들기 (TMDB API) (0) | 2026.03.01 |
| [HTML/CSS/JS] 뽀모도로 타이머 만들기 (0) | 2026.03.01 |
| [HTML/CSS/JS] Canvas API로 그림판 만들기 (0) | 2026.03.01 |
| [HTML/CSS/JS] 로컬스토리지 메모장 앱 만들기 (0) | 2026.02.28 |