GitHub에서 저장소 정보를 조회하거나 이슈를 자동으로 생성해야 하는 상황을 경험해봤을까. 단순히 웹사이트에서 수동으로 클릭하는 것보다 API를 통해 자동화하면 훨씬 효율적이다. 다만 대부분의 개발자들은 GitHub API 인증 방식이 여러 개인 것을 모르고, 레이트 제한에 걸려서 당황하거나, 응답 형식을 제대로 파싱하지 못해 헤맨다. 이번 글에서는 GitHub REST API v3의 인증 방식, 주요 엔드포인트, PHP에서의 실제 구현까지 완벽하게 정리해서 소개하겠다.

 

1단계. GitHub API 인증 방식 이해하기

GitHub API를 사용하려면 먼저 인증이 필요하다. 크게 세 가지 방식이 있다. Personal Access Token(PAT) 방식이 가장 간단하고, OAuth2는 사용자 로그인이 필요한 웹앱에서 쓰고, GitHub App은 복잡하지만 가장 유연하다. 개인 프로젝트나 자동화 스크립트에서는 PAT를 쓰는 것이 일반적이다.

PAT는 GitHub 계정의 Settings > Developer settings > Personal access tokens > Tokens (classic)에서 생성할 수 있다. 권한 범위를 선택하는데, repo(저장소 접근)와 read:user(사용자 정보 조회) 정도면 충분하다. 생성된 토큰은 한 번만 표시되므로 안전한 곳에 저장해야 한다.

인증 방식사용 사례레이트 제한복잡도
Personal Access Token자동화 스크립트, 개인 프로젝트시간당 5,000 요청낮음
OAuth2사용자 로그인 필요한 웹앱사용자당 5,000 요청중간
GitHub App조직 내 대규모 통합설치당 15,000 요청높음

 

2단계. GitHub API 핵심 엔드포인트 파악

GitHub API 엔드포인트는 REST 기반이고, 응답은 JSON 형식이다. 자주 쓰는 엔드포인트는 다음과 같다.

사용자 정보는 GET /users/{username}으로 조회한다. 인증된 사용자 자신의 정보는 GET /user로 더 간단하게 가져올 수 있다. 저장소 정보는 GET /repos/{owner}/{repo}, 특정 사용자의 저장소 목록은 GET /users/{username}/repos로 조회한다. 이슈 생성은 POST /repos/{owner}/{repo}/issues, 이슈 목록 조회는 GET /repos/{owner}/{repo}/issues를 쓴다.

 

3단계. PHP에서 GitHub API 연동하기
예제 1. 저장소 정보 조회

✗ 잘못된 코드. 인증 헤더 없이 API를 호출하면 레이트 제한이 매우 빠르게 차오른다.

<?php
$owner = 'torvalds';
$repo = 'linux';
$url = "https://api.github.com/repos/{$owner}/{$repo}";

$response = file_get_contents($url);
$data = json_decode($response, true);

echo "Stars: " . $data['stargazers_count'];
?>

✓ 올바른 코드. Authorization 헤더에 token을 포함해서 인증된 요청을 만든다.

<?php
define('GITHUB_TOKEN', 'ghp_your_token_here');
define('GITHUB_API_BASE', 'https://api.github.com');

function getGitHubRepo($owner, $repo) {
    $url = GITHUB_API_BASE . "/repos/{$owner}/{$repo}";
    
    $ch = curl_init();
    curl_setopt($ch, CURLOPT_URL, $url);
    curl_setopt($ch, CURLOPT_HTTPHEADER, [
        'Authorization: token ' . GITHUB_TOKEN,
        'Accept: application/vnd.github.v3+json',
        'User-Agent: MyApp'
    ]);
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    
    $response = curl_exec($ch);
    $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
    curl_close($ch);
    
    if ($http_code !== 200) {
        throw new Exception("GitHub API Error: HTTP {$http_code}");
    }
    
    return json_decode($response, true);
}

$repo = getGitHubRepo('torvalds', 'linux');
echo "Repository: " . $repo['full_name'] . "\n";
echo "Stars: " . $repo['stargazers_count'] . "\n";
echo "Forks: " . $repo['forks_count'] . "\n";
?>

결과는 다음처럼 출력된다.

Repository: torvalds/linux
Stars: 176500
Forks: 27300
예제 2. 이슈 목록 조회 (페이지네이션)

✗ 잘못된 코드. 한 번에 모든 이슈를 가져오려고 하면 API 응답이 끝나지 않거나 타임아웃될 수 있다.

<?php
$url = "https://api.github.com/repos/torvalds/linux/issues?state=open";
$response = file_get_contents($url);
$issues = json_decode($response, true);

foreach ($issues as $issue) {
    echo $issue['number'] . ": " . $issue['title'] . "\n";
}
?>

✓ 올바른 코드. per_page와 page 매개변수로 페이지네이션을 구현한다.

<?php
function getGitHubIssues($owner, $repo, $page = 1, $per_page = 30) {
    $url = GITHUB_API_BASE . "/repos/{$owner}/{$repo}/issues";
    $url .= "?state=open&per_page={$per_page}&page={$page}";
    
    $ch = curl_init();
    curl_setopt($ch, CURLOPT_URL, $url);
    curl_setopt($ch, CURLOPT_HTTPHEADER, [
        'Authorization: token ' . GITHUB_TOKEN,
        'Accept: application/vnd.github.v3+json',
        'User-Agent: MyApp'
    ]);
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    
    $response = curl_exec($ch);
    $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
    curl_close($ch);
    
    if ($http_code !== 200) {
        throw new Exception("GitHub API Error: HTTP {$http_code}");
    }
    
    return json_decode($response, true);
}

// 첫 번째 페이지 조회
$issues = getGitHubIssues('torvalds', 'linux', 1, 10);

foreach ($issues as $issue) {
    echo "#" . $issue['number'] . ": " . $issue['title'] . "\n";
}
?>

결과 샘플.

#1: Fix NULL pointer in fs/dcache.c
#2: Memory leak in mm/swap.c
#3: Incorrect error handling in drivers/gpu/drm
...
예제 3. 새로운 이슈 생성

✗ 잘못된 코드. POST 요청 시 Content-Type 헤더를 빠뜨리거나 데이터를 제대로 직렬화하지 않으면 실패한다.

<?php
$issue_data = [
    'title' => 'Bug: Memory leak in swap subsystem',
    'body' => 'Description of the issue'
];

$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, 'https://api.github.com/repos/torvalds/linux/issues');
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, $issue_data); // 배열 그대로 전송 (잘못됨)
curl_exec($ch);
?>

✓ 올바른 코드. JSON으로 인코딩하고 Content-Type을 명시한다.

<?php
function createGitHubIssue($owner, $repo, $title, $body, $labels = []) {
    $url = GITHUB_API_BASE . "/repos/{$owner}/{$repo}/issues";
    
    $issue_data = [
        'title' => $title,
        'body' => $body
    ];
    
    if (!empty($labels)) {
        $issue_data['labels'] = $labels;
    }
    
    $payload = json_encode($issue_data);
    
    $ch = curl_init();
    curl_setopt($ch, CURLOPT_URL, $url);
    curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'POST');
    curl_setopt($ch, CURLOPT_HTTPHEADER, [
        'Authorization: token ' . GITHUB_TOKEN,
        'Accept: application/vnd.github.v3+json',
        'Content-Type: application/json',
        'User-Agent: MyApp'
    ]);
    curl_setopt($ch, CURLOPT_POSTFIELDS, $payload);
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    
    $response = curl_exec($ch);
    $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
    curl_close($ch);
    
    if ($http_code !== 201) {
        throw new Exception("Issue creation failed: HTTP {$http_code}, Response: {$response}");
    }
    
    return json_decode($response, true);
}

$new_issue = createGitHubIssue(
    'yourname',
    'yourrepo',
    'Bug: Connection timeout in API client',
    'When making requests over 30 seconds, connection times out.\nExpected: Request should complete successfully',
    ['bug', 'urgent']
);

echo "Issue created: #" . $new_issue['number'] . "\n";
echo "URL: " . $new_issue['html_url'] . "\n";
?>

결과.

Issue created: #42
URL: https://github.com/yourname/yourrepo/issues/42

 

4단계. 주의사항과 흔한 실수

✗ 토큰을 코드에 하드코딩하기. 위 예제처럼 GITHUB_TOKEN 상수를 정의하는 것도 좋지만, 정말로는 환경변수에서 읽어야 한다. 특히 GitHub에 코드를 푸시할 때는 절대 토큰이 포함되지 않게 주의해야 한다. 실수로 노출되면 즉시 GitHub에서 재생성하고 기존 토큰을 폐기해야 한다.

✓ 환경변수 또는 설정 파일 사용하기.

<?php
$token = getenv('GITHUB_TOKEN');
if (!$token) {
    throw new Exception('GITHUB_TOKEN environment variable not set');
}
?>

✗ 레이트 제한 무시하기. GitHub API는 인증된 요청에 대해 시간당 5,000개까지만 허용한다. 반복 작업에서 이를 무시하면 갑자기 요청이 막힐 수 있다.

✓ 응답 헤더에서 X-RateLimit-* 확인하기.

<?php
function checkRateLimit($ch) {
    $info = curl_getinfo($ch);
    $headers = get_headers($info['url'], 1);
    
    echo "Requests Remaining: " . ($headers['X-RateLimit-Remaining'] ?? 'N/A') . "\n";
    echo "Rate Limit Reset: " . ($headers['X-RateLimit-Reset'] ?? 'N/A') . "\n";
}
?>

✗ 에러 응답을 구분하지 않기. GitHub API는 404(리소스 없음), 422(검증 실패), 403(권한 없음) 등 다양한 상태 코드를 반환한다.

✓ HTTP 상태 코드와 응답 본문을 함께 확인하기.

<?php
if ($http_code === 404) {
    $error = json_decode($response, true);
    echo "Not Found: " . $error['message'] . "\n";
} elseif ($http_code === 422) {
    $error = json_decode($response, true);
    echo "Validation Error: " . $error['message'] . "\n";
    foreach ($error['errors'] as $err) {
        echo "  - " . $err['field'] . ": " . $err['code'] . "\n";
    }
}
?>

 

5단계. 실전 활용 예제

자신의 저장소에 있는 모든 public 이슈를 조회하고, 특정 키워드가 있는 이슈만 필터링하는 코드를 작성해보자.

<?php
define('GITHUB_TOKEN', getenv('GITHUB_TOKEN'));
define('GITHUB_API_BASE', 'https://api.github.com');

function getAllGitHubIssues($owner, $repo, $keyword = null) {
    $all_issues = [];
    $page = 1;
    
    while (true) {
        $url = GITHUB_API_BASE . "/repos/{$owner}/{$repo}/issues?state=all&per_page=100&page={$page}";
        
        $ch = curl_init();
        curl_setopt($ch, CURLOPT_URL, $url);
        curl_setopt($ch, CURLOPT_HTTPHEADER, [
            'Authorization: token ' . GITHUB_TOKEN,
            'Accept: application/vnd.github.v3+json',
            'User-Agent: MyApp'
        ]);
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
        
        $response = curl_exec($ch);
        $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
        curl_close($ch);
        
        if ($http_code !== 200) {
            break;
        }
        
        $issues = json_decode($response, true);
        if (empty($issues)) {
            break;
        }
        
        foreach ($issues as $issue) {
            if ($keyword === null || stripos($issue['title'], $keyword) !== false) {
                $all_issues[] = $issue;
            }
        }
        
        $page++;
    }
    
    return $all_issues;
}

$issues = getAllGitHubIssues('yourname', 'yourrepo', 'bug');
echo "Found " . count($issues) . " issues with 'bug' keyword\n";
foreach ($issues as $issue) {
    echo "#" . $issue['number'] . ": " . $issue['title'] . " (" . $issue['state'] . ")\n";
}
?>

 

마무리

GitHub API는 저장소 관리, 이슈 자동화, CI/CD 파이프라인 통합 등 다양한 목적으로 쓸 수 있다. 이번 글에서 다룬 인증 방식, 주요 엔드포인트, PHP cURL 구현 방식을 이해하면 대부분의 GitHub 자동화 작업을 수행할 수 있다. 특히 토큰 관리와 레이트 제한 확인이 중요하다는 점을 잊지 말자. 이 글의 예제 코드를 참고해 자신의 프로젝트에 GitHub API를 통합하면, 수동 작업을 크게 줄이고 개발 생산성을 높일 수 있을 것이다.