인스타그램에서 비즈니스 데이터를 자동으로 수집하거나 게시물을 프로그래밍 방식으로 관리하려는 개발자들이 늘어나고 있다. 다만 Meta의 Graph API 문서는 복잡하고, 접근 토큰 발급부터 API 호출까지 실제로 구현하다 보면 인증 오류나 권한 문제에 자주 막힌다. 이번에는 Instagram Graph API v18을 PHP에서 제대로 연동하는 방법을 처음부터 끝까지 정리해서 소개하겠다.

 

1단계. Meta 개발자 계정과 앱 등록

Instagram Graph API를 사용하려면 먼저 Meta for Developers 계정이 필요하다. developers.facebook.com에 접속해서 회원가입 후 로그인하자. 그 다음 대시보드 > 앱 > 앱 만들기를 선택해 새 앱을 생성한다. 앱 타입은 '비즈니스'를 고르고, 앱 이름과 이메일을 입력하면 된다.

앱이 생성되면 '제품 추가' 버튼에서 'Instagram Graph API'를 검색해 추가하자. 그러면 Instagram 기본 설정 페이지가 나타나는데, 여기서 Instagram 비즈니스 계정을 연결해야 한다. 개인 인스타그램 계정을 비즈니스 계정으로 전환한 후, 계정을 승인하면 Instagram 사용자 ID가 표시된다. 이 ID는 나중에 API 호출할 때 필요하니 기록해두자.

 

2단계. 장기 접근 토큰 발급받기

Instagram Graph API는 두 가지 토큰을 지원한다. 단기 토큰(약 1시간)과 장기 토큰(약 60일)이다. 실제 서비스에서는 장기 토큰을 발급받아 안전하게 저장해두고 사용해야 한다.

앱 설정 > 기본 설정에서 '앱 ID'와 '앱 시크릿'을 복사한다. 그 다음 앱 역할 > 테스트 사용자에서 테스트 사용자를 생성하거나, 실제 계정을 연결하자. Instagram 기본 설정 페이지로 돌아가서 '토큰 생성' 버튼을 클릭하면 단기 토큰이 발급된다.

장기 토큰으로 변환하려면 다음 URL에 접속한다.

https://graph.instagram.com/access_token?grant_type=fb_exchange_token&client_id={앱ID}&client_secret={앱시크릿}&access_token={단기토큰}

브라우저에서 이 URL을 열면 JSON 응답이 나타난다. access_token 값이 장기 토큰이다. 이를 복사해서 서버의 환경 변수나 데이터베이스에 안전하게 저장하자.

 

3단계. PHP cURL로 Instagram 기본 정보 조회하기

이제 PHP에서 API를 호출할 준비가 됐다. 가장 기본적인 작업부터 시작하자. 인스타그램 비즈니스 계정의 기본 정보를 조회하는 코드다.

<?php
// ✗ 잘못된 방식: 토큰을 하드코딩하고 요청 방식이 부정확함
$token = 'IGABCDEFGHIJKLMNOPQRSTUVWXYZabc';
$ig_user_id = '12345678';
$url = "https://graph.instagram.com/{$ig_user_id}?fields=id,username,name&access_token={$token}";

$response = file_get_contents($url); // 네트워크 에러 처리 없음
$data = json_decode($response, true);

echo $data['username'];
?>

위 코드는 토큰을 노출시키고 에러 처리가 없어서 위험하다. 올바른 방식은 이렇다.

<?php
// ✓ 올바른 방식: 환경 변수에서 토큰을 읽고 cURL로 안전하게 요청
$token = getenv('INSTAGRAM_ACCESS_TOKEN'); // .env 파일 또는 환경 변수에서 읽기
$ig_user_id = getenv('INSTAGRAM_USER_ID');

if (!$token || !$ig_user_id) {
    die('Instagram 토큰 또는 사용자 ID가 설정되지 않았습니다.');
}

$url = "https://graph.instagram.com/{$ig_user_id}?fields=id,username,name,biography,profile_picture_url";

$ch = curl_init();
curl_setopt_array($ch, [
    CURLOPT_URL => $url,
    CURLOPT_HTTPHEADER => [
        'Authorization: Bearer ' . $token,
        'Content-Type: application/json'
    ],
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_TIMEOUT => 10,
    CURLOPT_SSL_VERIFYPEER => true
]);

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

if ($curl_error) {
    die('cURL 오류: ' . $curl_error);
}

if ($http_code !== 200) {
    $error_data = json_decode($response, true);
    die('API 오류 (' . $http_code . '): ' . $error_data['error']['message']);
}

$data = json_decode($response, true);
echo '계정명: ' . htmlspecialchars($data['username']) . '<br />';
echo '자기소개: ' . htmlspecialchars($data['biography']) . '<br />';
echo '프로필 사진: <img src="' . htmlspecialchars($data['profile_picture_url']) . '" />';
?>

핵심 포인트는 세 가지다. 첫째, 토큰을 항상 환경 변수에서 읽기. 둘째, Authorization 헤더에 토큰을 넣어서 URL에 노출되지 않게 하기. 셋째, HTTP 상태 코드와 에러 메시지를 항상 확인하기.

 

4단계. 게시물 목록 조회하기

이제 게시물을 가져오는 방법을 알아보자. Instagram 비즈니스 계정은 본인이 작성한 게시물(미디어)을 조회할 수 있다.

<?php
function getInstagramMedia($ig_user_id, $token) {
    $url = "https://graph.instagram.com/{$ig_user_id}/media?fields=id,caption,media_type,media_url,timestamp,like_count,comments_count";
    
    $ch = curl_init();
    curl_setopt_array($ch, [
        CURLOPT_URL => $url,
        CURLOPT_HTTPHEADER => [
            'Authorization: Bearer ' . $token,
            'Content-Type: application/json'
        ],
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_TIMEOUT => 10,
        CURLOPT_SSL_VERIFYPEER => true
    ]);
    
    $response = curl_exec($ch);
    $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
    $curl_error = curl_error($ch);
    curl_close($ch);
    
    if ($curl_error) {
        throw new Exception('cURL 오류: ' . $curl_error);
    }
    
    if ($http_code !== 200) {
        $error_data = json_decode($response, true);
        throw new Exception('API 오류: ' . $error_data['error']['message']);
    }
    
    $data = json_decode($response, true);
    return $data['data'] ?? [];
}

$token = getenv('INSTAGRAM_ACCESS_TOKEN');
$ig_user_id = getenv('INSTAGRAM_USER_ID');

try {
    $posts = getInstagramMedia($ig_user_id, $token);
    
    foreach ($posts as $post) {
        echo '<div>';
        echo '<p><strong>' . htmlspecialchars($post['caption'] ?? '(캡션 없음)') . '</strong></p>';
        
        if ($post['media_type'] === 'IMAGE') {
            echo '<img src="' . htmlspecialchars($post['media_url']) . '" style="max-width: 300px;" />';
        } elseif ($post['media_type'] === 'VIDEO') {
            echo '<video controls style="max-width: 300px;"><source src="' . htmlspecialchars($post['media_url']) . '" /></video>';
        } elseif ($post['media_type'] === 'CAROUSEL_ALBUM') {
            echo '<p>(여러 미디어 포함)</p>';
        }
        
        echo '<p>좋아요: ' . $post['like_count'] . ' | 댓글: ' . $post['comments_count'] . '</p>';
        echo '<p><small>' . htmlspecialchars($post['timestamp']) . '</small></p>';
        echo '</div><hr />';
    }
} catch (Exception $e) {
    echo '오류: ' . htmlspecialchars($e->getMessage());
}
?>

주목할 점은 fields 파라미터다. API는 기본적으로 id 필드만 반환하므로, 필요한 필드를 명시적으로 지정해야 한다. 위 코드는 캡션, 미디어 타입, URL, 타임스탐프, 좋아요와 댓글 수를 조회한다. 또한 media_type에 따라 IMAGE, VIDEO, CAROUSEL_ALBUM을 구분해서 표시했다.

 

5단계. 페이지네이션 처리하기

게시물이 많으면 API는 페이지네이션으로 데이터를 나눠서 반환한다. 응답에 paging 객체가 포함되는데, after 커서를 이용해 다음 페이지를 가져올 수 있다.

<?php
function getAllInstagramMedia($ig_user_id, $token, $limit = 25) {
    $all_posts = [];
    $after_cursor = null;
    $request_count = 0;
    
    do {
        $url = "https://graph.instagram.com/{$ig_user_id}/media?fields=id,caption,media_type,media_url,timestamp,like_count,comments_count&limit={$limit}";
        
        if ($after_cursor) {
            $url .= '&after=' . urlencode($after_cursor);
        }
        
        $ch = curl_init();
        curl_setopt_array($ch, [
            CURLOPT_URL => $url,
            CURLOPT_HTTPHEADER => [
                'Authorization: Bearer ' . $token,
                'Content-Type: application/json'
            ],
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_TIMEOUT => 10,
            CURLOPT_SSL_VERIFYPEER => true
        ]);
        
        $response = curl_exec($ch);
        $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
        curl_close($ch);
        
        if ($http_code !== 200) {
            throw new Exception('페이지네이션 오류: HTTP ' . $http_code);
        }
        
        $data = json_decode($response, true);
        $all_posts = array_merge($all_posts, $data['data'] ?? []);
        
        // 다음 페이지 커서 확인
        $after_cursor = $data['paging']['cursors']['after'] ?? null;
        $request_count++;
        
        // API 레이트 제한 방지 (선택사항)
        sleep(1);
        
    } while ($after_cursor && $request_count < 100); // 무한 루프 방지
    
    return $all_posts;
}

$token = getenv('INSTAGRAM_ACCESS_TOKEN');
$ig_user_id = getenv('INSTAGRAM_USER_ID');

try {
    $all_posts = getAllInstagramMedia($ig_user_id, $token, 25);
    echo '총 ' . count($all_posts) . '개 게시물 조회됨';
} catch (Exception $e) {
    echo '오류: ' . htmlspecialchars($e->getMessage());
}
?>

페이지네이션할 때는 무한 루프를 방지하기 위해 요청 횟수 제한을 두자. 또한 API 레이트 제한을 피하기 위해 요청 사이에 짧은 지연을 넣는 것이 좋다.

 

주의사항과 흔한 실수

✗ 토큰을 PHP 코드에 직접 삽입: API 키가 노출되면 누구나 계정을 악용할 수 있다. 환경 변수나 보안 볼트에 저장하자.

✗ Authorization 헤더 형식 틀림: 'Authorization: Bearer 토큰' 형식이 정확해야 한다. 'access_token=토큰' URL 파라미터는 권장되지 않는다.

✗ 필드 이름 오타: fields 파라미터에서 정확한 필드명을 사용해야 한다. 오타가 있으면 null이 반환되거나 에러가 난다.

✗ 권한 부족으로 인한 403 에러: API 스코프 설정이 부족할 수 있다. 앱 설정 > 권한에서 instagram_business_basic, instagram_business_content_publish 등 필요한 권한이 추가됐는지 확인하자.

✓ 충분한 에러 처리와 로깅: HTTP 상태 코드와 API 에러 메시지를 항상 확인하고, 서버 로그에 기록하자.

✓ 토큰 만료 전에 갱신: 장기 토큰도 60일 후 만료되므로, 토큰 갱신 로직을 미리 구현하거나 만료 알림을 설정하자.

 

마무리

Instagram Graph API는 강력한 도구지만 인증과 권한이 복잡해서 처음엔 막혀 보인다. 하지만 토큰 발급 절차를 정확히 이해하고 cURL 요청을 제대로 설정하면, PHP에서도 문제없이 인스타그램 데이터를 자동으로 수집하고 활용할 수 있다. 이 글의 코드를 참고해 단계별로 구현해보면, 비즈니스 인스타그램 계정 관리 자동화나 소셜 미디어 대시보드 같은 실무 프로젝트를 만들 수 있을 것이다.