GitHub REST API v3는 엔드포인트마다 여러 번 호출해야 저장소 정보, 이슈, 풀 리퀘스트를 모두 가져올 수 있다. 다만 대부분의 개발자들은 GraphQL이 같은 데이터를 단 한 번의 요청으로 가져올 수 있다는 사실을 모른 채, 네트워크 오버헤드를 감수하며 REST API만 써왔다. 이번에는 GitHub GraphQL API가 정확히 뭔지, REST API와 뭐가 다른지, PHP에서 어떻게 실제로 구현하는지 완벽하게 정리해서 소개하겠다.

 

GraphQL vs REST API 핵심 차이
GitHub REST API는 고정된 응답 구조를 강제한다. 예를 들어 저장소 정보를 가져오려면 /repos/{owner}/{repo} 엔드포인트를 호출하고, 이슈 목록은 /repos/{owner}/{repo}/issues, 브랜치는 /repos/{owner}/{repo}/branches처럼 각각 별도 호출이 필요하다. 네트워크 레이턴시가 100ms라면 세 번의 요청은 최소 300ms 이상 걸린다.

GraphQL은 단일 엔드포인트에서 필요한 데이터를 쿼리문으로 명시한다. 같은 데이터를 한 번의 요청으로 가져올 수 있고, 불필요한 필드는 응답에 포함되지 않는다. 대역폭도 절약하고, API 레이트 리밋도 효율적으로 사용할 수 있다.

 

GitHub GraphQL API 인증 토큰 발급
GraphQL을 쓰려면 먼저 Personal Access Token(PAT)을 발급받아야 한다. GitHub 계정의 Settings > Developer settings > Personal access tokens > Tokens (classic)으로 이동해서 "Generate new token (classic)"을 클릭한다. repo, read:user 스코프를 체크하고 토큰을 생성하면 된다. 이 토큰은 환경 변수로 보관하거나 PHP 코드의 설정 파일에 저장해야 한다.

.env 파일에 저장하는 방식이 가장 안전하다.
GITHUB_TOKEN=ghp_xxxxxxxxxxxxxxxxxxxxxxxxxxxx

 

GraphQL 쿼리 기초 작성법
GraphQL은 쿼리문이 JSON과 유사한 구조인데, 실제로는 다른 문법이다. GitHub 저장소 정보, 최근 이슈 5개, 브랜치 2개를 한 번에 가져오는 쿼리를 보자.
query {
  repository(owner: "facebook", name: "react") {
    name
    description
    stargazerCount
    forkCount
    issues(first: 5, states: OPEN) {
      edges {
        node {
          title
          state
          createdAt
        }
      }
    }
    refs(first: 2, refPrefix: "refs/heads/") {
      edges {
        node {
          name
        }
      }
    }
  }
}
GraphQL에서 edges와 node는 페이지네이션을 위한 표준 구조다. edges는 연결(connection)의 간선이고, node는 실제 데이터를 담은 노드다. first: 5는 "처음 5개를 가져와"라는 뜻이다.

 

PHP에서 GraphQL 쿼리 실행하기
✓ 올바른 방법: cURL로 GraphQL 엔드포인트에 쿼리문을 POST로 전송한다.
<?php
$token = getenv('GITHUB_TOKEN');
$query = '{
  repository(owner: "facebook", name: "react") {
    name
    description
    stargazerCount
    forkCount
    issues(first: 5, states: OPEN) {
      edges {
        node {
          title
          state
          createdAt
        }
      }
    }
  }
}';

$ch = curl_init();
curl_setopt_array($ch, [
    CURLOPT_URL => 'https://api.github.com/graphql',
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER => [
        'Authorization: Bearer ' . $token,
        'Content-Type: application/json',
        'User-Agent: PHP-Client'
    ],
    CURLOPT_POSTFIELDS => json_encode([
        'query' => $query
    ]),
    CURLOPT_TIMEOUT => 10
]);

$response = curl_exec($ch);
$http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$error = curl_error($ch);
curl_close($ch);

if ($error) {
    echo "cURL Error: $error";
    exit;
}

$data = json_decode($response, true);

if (!empty($data['errors'])) {
    echo "GraphQL Error: " . json_encode($data['errors'], JSON_PRETTY_PRINT);
    exit;
}

$repo = $data['data']['repository'];
echo "Repository: " . $repo['name'] . "\n";
echo "Stars: " . $repo['stargazerCount'] . "\n";
echo "Open Issues:\n";
foreach ($repo['issues']['edges'] as $edge) {
    echo "  - " . $edge['node']['title'] . " (" . $edge['node']['state'] . ")\n";
}
?>
✗ 잘못된 방법: REST API 엔드포인트로 쿼리를 전송하려고 시도한다.
// 이렇게 하면 안 된다
curl_setopt($ch, CURLOPT_URL, 'https://api.github.com/repos/facebook/react');
// GraphQL 엔드포인트는 반드시 https://api.github.com/graphql이어야 함
?>

 

변수를 사용한 동적 쿼리
쿼리에 값을 직접 입력하면 매번 쿼리문 전체를 다시 작성해야 한다. GraphQL 변수 기능을 쓰면 쿼리 템플릿은 고정하고 값만 바꿀 수 있다.
<?php
$token = getenv('GITHUB_TOKEN');

// 쿼리는 변수 선언으로 파라미터화
$query = 'query GetRepository($owner: String!, $name: String!, $issueCount: Int!) {
  repository(owner: $owner, name: $name) {
    name
    description
    stargazerCount
    issues(first: $issueCount, states: OPEN) {
      totalCount
      edges {
        node {
          title
          state
        }
      }
    }
  }
}';

// 변수값을 별도로 전달
$variables = [
    'owner' => 'laravel',
    'name' => 'framework',
    'issueCount' => 10
];

$ch = curl_init();
curl_setopt_array($ch, [
    CURLOPT_URL => 'https://api.github.com/graphql',
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER => [
        'Authorization: Bearer ' . $token,
        'Content-Type: application/json',
        'User-Agent: PHP-Client'
    ],
    CURLOPT_POSTFIELDS => json_encode([
        'query' => $query,
        'variables' => $variables
    ]),
    CURLOPT_TIMEOUT => 10
]);

$response = curl_exec($ch);
curl_close($ch);

$data = json_decode($response, true);
$repo = $data['data']['repository'];
echo "Repository: " . $repo['name'] . "\n";
echo "Total Issues: " . $repo['issues']['totalCount'] . "\n";
echo "Fetched: " . count($repo['issues']['edges']) . " issues\n";
?>

 

페이지네이션 구현하기
GraphQL 연결은 `after` 커서로 다음 페이지를 가져온다. 100개 이상의 이슈를 반복으로 수집하려면 `hasNextPage`와 `endCursor`를 체크해야 한다.
<?php
$token = getenv('GITHUB_TOKEN');
$allIssues = [];
$afterCursor = null;
$hasNextPage = true;

while ($hasNextPage) {
    $cursorArg = $afterCursor ? ", after: \"$afterCursor\"" : '';
    
    $query = "query {\n      repository(owner: \"torvalds\", name: \"linux\") {\n        issues(first: 50$cursorArg, states: OPEN) {\n          pageInfo {\n            hasNextPage\n            endCursor\n          }\n          edges {\n            node {\n              title\n              createdAt\n            }\n          }\n        }\n      }\n    }";
    
    $ch = curl_init();
    curl_setopt_array($ch, [
        CURLOPT_URL => 'https://api.github.com/graphql',
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_HTTPHEADER => [
            'Authorization: Bearer ' . $token,
            'Content-Type: application/json'
        ],
        CURLOPT_POSTFIELDS => json_encode(['query' => $query]),
        CURLOPT_TIMEOUT => 10
    ]);
    
    $response = curl_exec($ch);
    curl_close($ch);
    
    $data = json_decode($response, true);
    
    if (!empty($data['errors'])) {
        echo "GraphQL Error: " . $data['errors'][0]['message'] . "\n";
        break;
    }
    
    $issues = $data['data']['repository']['issues'];
    $pageInfo = $issues['pageInfo'];
    
    foreach ($issues['edges'] as $edge) {
        $allIssues[] = $edge['node'];
    }
    
    $hasNextPage = $pageInfo['hasNextPage'];
    $afterCursor = $pageInfo['endCursor'];
    
    echo "Fetched " . count($issues['edges']) . " issues (Total: " . count($allIssues) . ")\n";
    sleep(1); // API 레이트 리밋 피하기
}

echo "\nTotal issues collected: " . count($allIssues) . "\n";
echo "First issue: " . $allIssues[0]['title'] . "\n";
?>

 

주의사항과 흔한 실수

✗ 잘못된 것 1: Authorization 헤더 포맷 오류

REST API는 `Authorization: token {토큰}`이지만, GraphQL은 `Authorization: Bearer {토큰}` 형식이어야 한다.

// 잘못됨
CURLOPT_HTTPHEADER => ['Authorization: token ' . $token]

// 올바름
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $token]
?>

✗ 잘못된 것 2: 쿼리 문자열 이스케이프 누락

PHP에서 GraphQL 쿼리를 변수에 담을 때 작은따옴표('')로 감싸면 `$variable` 치환이 안 되고, 큰따옴표("")로 감싸면 따옴표를 이스케이프해야 한다. JSON 직렬화 시 자동으로 처리되므로 원문 그대로 보내는 게 낫다.

// 복잡하고 오류가 많음
$query = "query { repository(owner: \"owner\", name: \"name\") { ... } }";

// 간단함 - JSON 인코딩이 자동으로 처리
$payload = json_encode(['query' => $query]);
?>

✓ 올바른 것: 에러 응답 구분하기

GraphQL은 HTTP 200 상태 코드로 응답하더라도 쿼리에 문법 오류가 있으면 `errors` 필드에 에러가 담긴다. HTTP 상태 코드만 체크하면 실제 오류를 놓친다.

$data = json_decode($response, true);

// HTTP 상태 코드 체크는 네트워크 오류 판단용
if ($http_code !== 200) {
    echo "HTTP Error: $http_code\n";
}

// GraphQL 논리 오류 체크는 필수
if (!empty($data['errors'])) {
    foreach ($data['errors'] as $error) {
        echo "GraphQL Error: " . $error['message'] . "\n";
    }
}

// 데이터가 있어야만 진행
if (empty($data['data'])) {
    echo "No data returned\n";
    exit;
}
?>

 

REST API 3번 호출 vs GraphQL 1번 호출 성능 비교
방식 요청 수 응답 시간 대역폭 레이트 리밋
REST API (저장소+이슈+브랜치) 3회 300~500ms 높음 3 포인트
GraphQL (단일 쿼리) 1회 100~150ms 낮음 1 포인트

 

레이트 리밋 확인하기
GitHub API는 시간당 5000번 호출을 허용하는데, GraphQL 쿼리의 복잡도에 따라 1회 호출이 여러 포인트를 소비할 수 있다. 응답 헤더의 `x-ratelimit-` 정보로 남은 할당량을 확인할 수 있다.
<?php
$ch = curl_init();
curl_setopt_array($ch, [
    CURLOPT_URL => 'https://api.github.com/graphql',
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HEADER => true, // 응답 헤더 포함
    CURLOPT_HTTPHEADER => [
        'Authorization: Bearer ' . $token,
        'Content-Type: application/json'
    ],
    CURLOPT_POSTFIELDS => json_encode(['query' => $query])
]);

$response = curl_exec($ch);
$info = curl_getinfo($ch);
curl_close($ch);

// 응답 헤더에서 레이트 리밋 정보 추출
list($headers, $body) = explode("\r\n\r\n", $response, 2);

echo "Rate Limit Remaining: " . $info['x_ratelimit_remaining'] . "\n";
echo "Rate Limit Reset: " . date('Y-m-d H:i:s', $info['x_ratelimit_reset']) . "\n";
?>

 

마무리
GitHub GraphQL API는 REST API의 번거로운 다중 호출과 오버페칭을 한 번에 해결한다. 단 한 번의 요청으로 필요한 정보만 가져올 수 있고, 네트워크 지연도 줄일 수 있으며, API 레이트 리밋도 효율적으로 사용할 수 있다. 이 글의 쿼리 작성법과 변수 처리 방식을 참고해 자신의 프로젝트에 적용하면, GitHub API와의 상호작용을 대폭 간결하게 만들 수 있을 것이다.