GitHub에 저장된 저장소 정보를 프로그래매틱하게 가져오거나, 이슈를 자동으로 생성하고, 커밋 히스토리를 분석해본 경험이 있을까? 다만 대부분의 개발자들은 GitHub API의 인증 방식이 복잡하거나, API 요청할 때 rate limiting에 걸려본 경험이 있을 것이다. 이번에는 GitHub API v3를 PHP에서 제대로 연동하는 방법을 정확히 무엇인지, 왜 필요한지, 어떻게 구현하는지 완벽하게 정리해서 소개하겠다.

 

1단계: GitHub API 기초 이해하기
GitHub API는 GitHub의 모든 주요 기능에 접근할 수 있는 REST 기반 인터페이스다. 저장소 정보 조회, 이슈 생성/수정, Pull Request 관리, 커밋 정보 조회 등 거의 모든 작업을 자동화할 수 있다.

GitHub API를 사용하려면 먼저 Personal Access Token(PAT)을 발급받아야 한다. 이것은 API 인증을 위한 암호화된 토큰으로, 보안상 사용자의 비밀번호 대신 사용된다. GitHub API는 기본 인증(Basic Auth)도 지원하지만, 보안상 Personal Access Token 사용을 권장한다.

 

GitHub Personal Access Token 발급 받기

GitHub 설정 페이지에서 Developer settings → Personal access tokens → Tokens (classic) 메뉴로 이동한 후 "Generate new token (classic)"을 클릭한다. 다음 권한들을 선택한다:
- repo: 저장소 접근
- read:user: 사용자 정보 읽기
- gist: Gist 접근
- notifications: 알림 접근
발급된 토큰은 한 번만 표시되므로 반드시 안전한 곳에 저장해야 한다.

 

2단계: PHP에서 GitHub API 호출하기
GitHub API는 HTTP 기반이므로 PHP의 curl 함수나 stream context를 통해 요청할 수 있다. 실무에서는 cURL을 많이 사용한다.

 

API 호출을 위한 기본 함수 작성

✗ 잘못된 코드 - 토큰 없이 API 호출하거나 헤더를 잘못 설정한 경우:

<?php
$url = "https://api.github.com/user/repos";
$ch = curl_init($url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_TIMEOUT, 10);
$response = curl_exec($ch);
$data = json_decode($response, true);
?>

위 코드는 인증이 없어서 API 응답이 제한되거나 "Requires authentication" 에러가 발생한다. 또한 User-Agent 헤더가 없으면 GitHub API에서 요청을 거부할 수 있다.

✓ 올바른 코드 - Personal Access Token으로 인증하고 필수 헤더 설정:

<?php
class GitHubAPI {
    private $token;
    private $baseUrl = "https://api.github.com";
    private $userAgent = "PHP-GitHub-Client";

    public function __construct($token) {
        $this->token = $token;
    }

    public function makeRequest($endpoint, $method = "GET", $data = null) {
        $url = $this->baseUrl . $endpoint;
        
        $ch = curl_init($url);
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
        curl_setopt($ch, CURLOPT_TIMEOUT, 10);
        curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $method);
        
        $headers = [
            "Authorization: token " . $this->token,
            "Accept: application/vnd.github.v3+json",
            "User-Agent: " . $this->userAgent,
            "Content-Type: application/json"
        ];
        curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
        
        if ($data !== null && in_array($method, ["POST", "PATCH", "PUT"])) {
            curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data));
        }
        
        $response = curl_exec($ch);
        $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
        curl_close($ch);
        
        if ($httpCode >= 400) {
            throw new Exception("GitHub API Error (" . $httpCode . "): " . $response);
        }
        
        return json_decode($response, true);
    }
}
?>

결과: Personal Access Token으로 인증되어 사용자의 모든 저장소 정보를 성공적으로 조회할 수 있다.

 

3단계: 실전 예제 - 주요 기능 구현하기

 

예제 1: 사용자 저장소 목록 조회
<?php
$github = new GitHubAPI("ghp_YOUR_PERSONAL_ACCESS_TOKEN");

try {
    // 인증된 사용자의 모든 저장소 조회
    $repos = $github->makeRequest("/user/repos?per_page=30&page=1");
    
    foreach ($repos as $repo) {
        echo "저장소명: " . $repo["name"] . "n";
        echo "설명: " . $repo["description"] . "n";
        echo "별: " . $repo["stargazers_count"] . "n";
        echo "포크: " . $repo["forks_count"] . "n";
        echo "---n";
    }
} catch (Exception $e) {
    echo "에러: " . $e->getMessage();
}
?>

 

예제 2: 특정 저장소의 이슈 생성하기
<?php
$github = new GitHubAPI("ghp_YOUR_PERSONAL_ACCESS_TOKEN");

try {
    $issueData = [
        "title" => "자동으로 생성된 버그 리포트",
        "body" => "이것은 PHP 스크립트에서 자동으로 생성된 이슈입니다.nn버그 설명:n- 문제 1n- 문제 2",
        "labels" => ["bug", "automated"],
        "assignee" => "your-username"
    ];
    
    $result = $github->makeRequest(
        "/repos/your-username/your-repo/issues",
        "POST",
        $issueData
    );
    
    echo "이슈 생성 완료: " . $result["html_url"] . "n";
} catch (Exception $e) {
    echo "에러: " . $e->getMessage();
}
?>

 

예제 3: 저장소 커밋 히스토리 조회
<?php
$github = new GitHubAPI("ghp_YOUR_PERSONAL_ACCESS_TOKEN");

try {
    // 최근 30개 커밋 조회
    $commits = $github->makeRequest(
        "/repos/your-username/your-repo/commits?per_page=30"
    );
    
    foreach ($commits as $commit) {
        echo "커밋: " . substr($commit["sha"], 0, 7) . "n";
        echo "저자: " . $commit["commit"]["author"]["name"] . "n";
        echo "메시지: " . $commit["commit"]["message"] . "n";
        echo "날짜: " . $commit["commit"]["author"]["date"] . "n";
        echo "---n";
    }
} catch (Exception $e) {
    echo "에러: " . $e->getMessage();
}
?>

 

4단계: 주의사항과 흔한 실수
문제 원인 해결방법
"Bad credentials" 에러 토큰이 잘못됨 또는 만료됨 토큰을 새로 발급받고 정확하게 입력하기
Rate Limit 초과 (403 에러) 인증되지 않은 요청은 시간당 60개, 인증된 요청은 3600개로 제한됨 요청 간격을 조정하거나 GraphQL API 사용 검토
"Not Found" (404 에러) 잘못된 저장소명 또는 엔드포인트 GitHub API 문서에서 정확한 엔드포인트 확인
timeout 에러 네트워크 지연 또는 너무 큰 데이터 요청 timeout 값을 늘리거나 페이지네이션 사용

 

✗ Rate Limiting 무시하고 무분별한 요청하기
<?php
// 잘못된 방식 - rate limit 체크 없음
for ($page = 1; $page <= 100; $page++) {
    $repos = $github->makeRequest("/user/repos?page=" . $page);
    // 처리...
}
?>

이렇게 하면 rate limit에 빠르게 도달하고 API 호출이 차단된다.

✓ Rate Limiting 정보를 확인하고 대기시간 설정하기

<?php
class GitHubAPI {
    // 기존 코드...
    
    public function makeRequest($endpoint, $method = "GET", $data = null) {
        $url = $this->baseUrl . $endpoint;
        
        $ch = curl_init($url);
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
        curl_setopt($ch, CURLOPT_TIMEOUT, 10);
        curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $method);
        curl_setopt($ch, CURLOPT_HEADER, true); // 헤더도 가져오기
        
        $headers = [
            "Authorization: token " . $this->token,
            "Accept: application/vnd.github.v3+json",
            "User-Agent: " . $this->userAgent
        ];
        curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
        
        if ($data !== null) {
            curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data));
        }
        
        $response = curl_exec($ch);
        $headerSize = curl_getinfo($ch, CURLINFO_HEADER_SIZE);
        $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
        curl_close($ch);
        
        $responseHeader = substr($response, 0, $headerSize);
        $responseBody = substr($response, $headerSize);
        
        // Rate limit 정보 확인
        if (preg_match('/X-RateLimit-Remaining: (d+)/', $responseHeader, $matches)) {
            $remaining = $matches[1];
            if ($remaining < 10) {
                // Rate limit이 거의 다 찼으면 대기
                if (preg_match('/X-RateLimit-Reset: (d+)/', $responseHeader, $resetMatches)) {
                    $resetTime = $resetMatches[1];
                    $sleepTime = max(0, $resetTime - time() + 1);
                    if ($sleepTime > 0) {
                        echo "Rate limit 대기 중... " . $sleepTime . "초n";
                        sleep($sleepTime);
                    }
                }
            }
        }
        
        if ($httpCode >= 400) {
            throw new Exception("GitHub API Error (" . $httpCode . "): " . $responseBody);
        }
        
        return json_decode($responseBody, true);
    }
}
?>

 

✗ 민감한 정보(토큰)를 소스 코드에 하드코딩하기
<?php
$github = new GitHubAPI("ghp_abc123xyz456..."); // 위험!
?>

이렇게 하면 GitHub에 코드가 공개될 때 토큰도 함께 노출되어 보안 사고가 발생한다.

✓ 환경변수나 .env 파일에서 토큰 로드하기

<?php
// .env 파일 (서버에만 저장, 버전 관리에서 제외)
GITHUB_TOKEN=ghp_abc123xyz456...

// config.php
$dotenv = parse_ini_file('.env');
$github = new GitHubAPI($dotenv['GITHUB_TOKEN']);
?>

.env 파일을 .gitignore에 추가하고 서버에만 배포한다.

 

마무리: GitHub API 연동의 실무 활용

GitHub API는 단순히 저장소 정보를 읽는 것 뿐만 아니라, 자동화된 배포 파이프라인, 이슈 관리 시스템, CI/CD 통합 등 매우 다양한 곳에서 쓰인다. Personal Access Token의 안전한 관리와 Rate Limiting 체크가 모여서 안정적인 API 연동을 만든다는 점을 잊지 말자. 이 글의 클래스 구조와 에러 처리 방식을 참고해 자신의 프로젝트에 맞게 확장하면, GitHub를 프로그래매틱하게 제어할 수 있을 것이다.