카카오 로그인을 구현한 후 사용자 정보를 서버에서 직접 조회해야 할 상황을 겪은 적 있을까. 대부분의 개발자들은 클라이언트에서 받은 토큰으로 카카오 API를 호출하거나, 공식 문서의 예제를 그냥 복사해서 쓰다가 토큰 검증 실패나 필드 누락 같은 오류에 부딪힌다. 이번에는 카카오 Developers REST API에서 사용자 정보를 안전하게 조회하는 방법을 정리해서 소개하겠다. 토큰 관리 방식부터 API 요청 형식, 응답 파싱과 오류 처리까지 실무에서 즉시 적용할 수 있는 완전한 가이드다.

 

카카오 API 토큰과 사용자 정보 조회의 기초

카카오 API를 통해 사용자 정보를 가져오려면 먼저 토큰의 종류와 역할을 이해해야 한다. 카카오에서는 두 가지 주요 토큰을 다룬다. Access Token은 사용자의 인증을 증명하는 일시적 토큰으로, 주로 1시간의 유효기간을 가진다. Refresh Token은 Access Token이 만료되었을 때 새로운 토큰을 발급받기 위한 토큰으로, 더 긴 유효기간을 가진다.

사용자 정보 조회는 카카오의 /v2/user/me 엔드포인트를 통해 이뤄진다. 이 엔드포인트는 유효한 Access Token을 Authorization 헤더에 담아 GET 요청을 보내면, 사용자의 ID, 이메일, 프로필 사진 등 다양한 정보를 JSON 형태로 반환한다. 중요한 점은 토큰이 유효해야 한다는 것과, 사용자가 해당 정보의 공개를 동의했어야 한다는 점이다.

 

카카오 Developers 앱 설정 및 토큰 확보

API를 호출하기 전에 카카오 Developers 콘솔에서 기본 설정을 완료해야 한다. Kakao Developers에 접속해서 로그인한 후 '내 애플리케이션'에서 새 앱을 생성하거나 기존 앱을 선택한다. 앱 설정 페이지에서 REST API 키를 복사한다. 이 키는 클라이언트에서 카카오 로그인 버튼을 초기화할 때 사용된다.

실제 사용자 정보 조회는 클라이언트의 카카오 로그인을 통해 받은 Access Token을 서버로 전달받아 진행한다. 클라이언트 측에서 JavaScript SDK를 통해 로그인하면, 카카오 서버로부터 발급받은 Access Token이 생성된다. 이 토큰을 서버에 전송하거나 세션에 저장하여 나중에 사용자 정보를 조회할 때 활용한다.

 

PHP에서 cURL로 카카오 사용자 정보 조회하기

이제 PHP에서 실제로 카카오 API를 호출해서 사용자 정보를 가져오는 코드를 작성해보자.

 

기본 API 호출 구조

✗ 잘못된 코드: 토큰 형식을 잘못 지정하거나 헤더를 빠뜨린 경우

<?php
$accessToken = $_POST['access_token'];

$curl = curl_init();
curl_setopt_array($curl, [
    CURLOPT_URL => 'https://kapi.kakao.com/v2/user/me',
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER => [
        'Authorization: ' . $accessToken  // 토큰 형식 틀림
    ],
]);

$response = curl_exec($curl);
curl_close($curl);
echo $response;
?>

이 코드의 문제점은 Authorization 헤더 형식이 올바르지 않다는 것이다. 카카오 API는 'Bearer ' 접두어를 포함해야 하는 경우도 있고, 'Bearer' 없이 순수 토큰만 필요한 경우도 있다. /v2/user/me 엔드포인트는 실제로 'Bearer' 접두어가 필요 없고, 'Authorization: <토큰>' 형식이 맞지만, 헤더 작성 방식이 부정확하거나 토큰 자체가 유효하지 않으면 401 Unauthorized 오류가 발생한다.

✓ 올바른 코드: 정확한 헤더 형식과 에러 처리 포함

<?php
$accessToken = $_POST['access_token'] ?? '';

if (empty($accessToken)) {
    http_response_code(400);
    echo json_encode(['error' => '토큰이 없습니다.']);
    exit;
}

$curl = curl_init();
curl_setopt_array($curl, [
    CURLOPT_URL => 'https://kapi.kakao.com/v2/user/me',
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_CONNECTTIMEOUT => 5,
    CURLOPT_TIMEOUT => 10,
    CURLOPT_HTTPHEADER => [
        'Authorization: ' . $accessToken,
        'Content-Type: application/x-www-form-urlencoded;charset=utf-8'
    ],
]);

$response = curl_exec($curl);
$httpCode = curl_getinfo($curl, CURLINFO_HTTP_CODE);
$curlError = curl_error($curl);
curl_close($curl);

if ($curlError) {
    http_response_code(500);
    echo json_encode(['error' => 'API 요청 실패: ' . $curlError]);
    exit;
}

if ($httpCode !== 200) {
    http_response_code($httpCode);
    echo json_encode(['error' => 'API 응답 오류', 'code' => $httpCode, 'response' => $response]);
    exit;
}

$userData = json_decode($response, true);
if (json_last_error() !== JSON_ERROR_NONE) {
    http_response_code(500);
    echo json_encode(['error' => 'JSON 파싱 실패: ' . json_last_error_msg()]);
    exit;
}

echo json_encode($userData);
?>

이 버전은 여러 가지 안전 장치를 추가했다. 먼저 토큰 존재 여부를 확인하고, cURL 타임아웃을 설정해서 무한 대기를 방지한다. API 응답의 HTTP 상태 코드를 확인하고, cURL 실행 중 오류가 발생했을 경우 이를 캐치한다. 마지막으로 JSON 파싱 오류도 처리해서 클라이언트에 명확한 에러 메시지를 전달한다.

 

카카오 응답 데이터 파싱과 필드 추출

카카오 API가 반환하는 JSON 응답에는 다양한 필드가 포함되어 있다. 가장 중요한 필드들을 정리하면 다음과 같다.

필드명 설명 타입 비고
id 사용자 고유 ID Long 카카오 내부 사용자 식별자, 항상 제공됨
kakao_account 사용자 계정 정보 객체 Object 이메일, 프로필, 전화번호 등 포함
kakao_account.email 이메일 주소 String 사용자가 동의했을 때만 제공
kakao_account.profile 프로필 정보 객체 Object nickname, profile_image_url, thumbnail_image_url 포함
kakao_account.age_range 나이대 String 예: "20~29"
connected_at 연결 시간 DateTime ISO 8601 형식

카카오 API의 응답 예시는 다음과 같다.

{
  "id": 1234567890,
  "connected_at": "2024-01-15T10:30:00Z",
  "kakao_account": {
    "profile_needs_agreement": false,
    "profile": {
      "nickname": "홍길동",
      "profile_image_url": "https://k.kakaocdn.net/...",
      "thumbnail_image_url": "https://k.kakaocdn.net/..."
    },
    "email_needs_agreement": false,
    "is_email_valid": true,
    "is_email_verified": true,
    "email": "hong@example.com"
  }
}

실제 데이터를 추출할 때는 중첩된 객체 구조를 고려해서 안전하게 접근해야 한다.

✗ 잘못된 방식: 중첩된 필드를 직접 접근하다가 오류 발생

<?php
$userData = json_decode($response, true);
$email = $userData['kakao_account']['email']; // 필드가 없으면 Warning 발생
$nickname = $userData['kakao_account']['profile']['nickname'];
echo $email . ' - ' . $nickname;
?>

사용자가 이메일 공개에 동의하지 않거나, 프로필 정보를 비공개로 설정했다면 이 필드들이 없을 수 있다. 이 경우 'Undefined array key' 경고가 발생한다.

✓ 올바른 방식: null 병합 연산자와 isset()으로 안전하게 접근

<?php
$userData = json_decode($response, true);

// 방법 1: null 병합 연산자 사용
$kakaoId = $userData['id'] ?? null;
$email = $userData['kakao_account']['email'] ?? '미제공';
$nickname = $userData['kakao_account']['profile']['nickname'] ?? '사용자';

// 방법 2: isset() 체크 후 접근
$profileImage = '';
if (isset($userData['kakao_account']['profile']['profile_image_url'])) {
    $profileImage = $userData['kakao_account']['profile']['profile_image_url'];
}

// 방법 3: 전용 함수로 안전한 추출
function getKakaoUserInfo($userData) {
    return [
        'kakao_id' => $userData['id'] ?? null,
        'email' => $userData['kakao_account']['email'] ?? null,
        'nickname' => $userData['kakao_account']['profile']['nickname'] ?? null,
        'profile_image' => $userData['kakao_account']['profile']['profile_image_url'] ?? null,
        'thumbnail_image' => $userData['kakao_account']['profile']['thumbnail_image_url'] ?? null,
        'age_range' => $userData['kakao_account']['age_range'] ?? null,
        'connected_at' => $userData['connected_at'] ?? null,
    ];
}

$userInfo = getKakaoUserInfo($userData);
echo json_encode($userInfo);
?>

 

토큰 유효성 검증 및 갱신

Access Token은 시간이 지나면 만료된다. 사용자 정보 조회 시 토큰이 만료되었다면 401 Unauthorized 오류가 발생한다. 이를 처리하는 방법은 두 가지다. 첫 번째는 Refresh Token을 사용해서 새로운 Access Token을 발급받는 것이고, 두 번째는 클라이언트에 재로그인을 요청하는 것이다.

✗ 잘못된 방식: 토큰 유효성을 확인하지 않고 계속 사용

<?php
// $_SESSION에 토큰이 저장되어 있다고 가정
$accessToken = $_SESSION['kakao_access_token'];

$curl = curl_init();
curl_setopt_array($curl, [
    CURLOPT_URL => 'https://kapi.kakao.com/v2/user/me',
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER => ['Authorization: ' . $accessToken],
]);

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

// 토큰이 만료되었을 수도 있지만 체크 안 함
echo json_decode($response, true)['kakao_account']['email'];
?>

이 코드는 토큰이 만료되었을 때 적절한 처리 없이 오류를 그대로 노출한다.

✓ 올바른 방식: 토큰 갱신 로직 포함

<?php
function getKakaoUserInfo($accessToken, $refreshToken = null) {
    $curl = curl_init();
    curl_setopt_array($curl, [
        CURLOPT_URL => 'https://kapi.kakao.com/v2/user/me',
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_TIMEOUT => 10,
        CURLOPT_HTTPHEADER => ['Authorization: ' . $accessToken],
    ]);

    $response = curl_exec($curl);
    $httpCode = curl_getinfo($curl, CURLINFO_HTTP_CODE);
    curl_close($curl);

    // 토큰 만료 감지
    if ($httpCode === 401) {
        if ($refreshToken) {
            // Refresh Token으로 새 Access Token 발급
            $newAccessToken = refreshKakaoToken($refreshToken);
            if ($newAccessToken) {
                // 새 토큰으로 재시도
                return getKakaoUserInfo($newAccessToken, $refreshToken);
            }
        }
        return ['error' => '토큰이 만료되었습니다. 다시 로그인해주세요.'];
    }

    if ($httpCode !== 200) {
        return ['error' => 'API 오류: ' . $httpCode];
    }

    return json_decode($response, true);
}

function refreshKakaoToken($refreshToken) {
    $curl = curl_init();
    curl_setopt_array($curl, [
        CURLOPT_URL => 'https://kauth.kakao.com/oauth/token',
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_POST => true,
        CURLOPT_POSTFIELDS => http_build_query([
            'grant_type' => 'refresh_token',
            'client_id' => 'YOUR_REST_API_KEY',
            'refresh_token' => $refreshToken,
        ]),
        CURLOPT_HTTPHEADER => [
            'Content-Type: application/x-www-form-urlencoded;charset=utf-8'
        ],
    ]);

    $response = curl_exec($curl);
    $httpCode = curl_getinfo($curl, CURLINFO_HTTP_CODE);
    curl_close($curl);

    if ($httpCode !== 200) {
        return null;
    }

    $data = json_decode($response, true);
    return $data['access_token'] ?? null;
}

// 사용 예
$accessToken = $_SESSION['kakao_access_token'];
$refreshToken = $_SESSION['kakao_refresh_token'] ?? null;

$userData = getKakaoUserInfo($accessToken, $refreshToken);
echo json_encode($userData);
?>

YOUR_REST_API_KEY 자리에 실제 앱의 REST API 키를 넣어야 한다. 토큰 갱신에 성공하면 새로운 Access Token과 Refresh Token이 반환되므로, 세션에 저장해서 다음번 요청에 사용하도록 업데이트한다.

 

흔한 실수와 해결 방법

 

토큰 형식 오류 (401 Unauthorized)

✗ 토큰에 접두어를 붙인 경우

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

✓ 토큰만 그대로 전달

CURLOPT_HTTPHEADER => ['Authorization: ' . $accessToken]  // 올바름

 

필수 필드 누락 (400 Bad Request)

✗ Content-Type 헤더 누락

$curl = curl_init();
curl_setopt_array($curl, [
    CURLOPT_URL => 'https://kapi.kakao.com/v2/user/me',
    CURLOPT_RETURNTRANSFER => true,
]);
// Content-Type 헤더가 없음

✓ Content-Type 포함

CURLOPT_HTTPHEADER => [
    'Authorization: ' . $accessToken,
    'Content-Type: application/x-www-form-urlencoded;charset=utf-8'
]

 

클라이언트 ID와 토큰 불일치

카카오 앱에서 발급한 REST API 키가 바뀌거나, 여러 앱을 운영하면서 잘못된 앱의 토큰을 사용하는 경우가 있다. 이 경우 API가 정상 응답을 하더라도 토큰이 유효하지 않아 401 오류가 발생한다. 앱 설정에서 REST API 키를 다시 확인하고, 클라이언트에서 발급받은 토큰의 앱이 서버 코드에서 사용하는 앱과 같은지 확인한다.

 

실전 예제: 카카오 로그인 시스템 통합

카카오 로그인 후 사용자 정보를 데이터베이스에 저장하고 세션을 관리하는 실제 시나리오를 다뤄보자.

<?php
session_start();

$accessToken = $_POST['access_token'] ?? null;
$refreshToken = $_POST['refresh_token'] ?? null;

if (!$accessToken) {
    http_response_code(400);
    echo json_encode(['error' => '토큰이 없습니다.']);
    exit;
}

function getKakaoUserInfo($token) {
    $ch = curl_init();
    curl_setopt_array($ch, [
        CURLOPT_URL => 'https://kapi.kakao.com/v2/user/me',
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_TIMEOUT => 10,
        CURLOPT_HTTPHEADER => [
            'Authorization: ' . $token,
            'Content-Type: application/x-www-form-urlencoded;charset=utf-8'
        ],
    ]);
    
    $response = curl_exec($ch);
    $code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
    curl_close($ch);
    
    if ($code !== 200) {
        return null;
    }
    
    return json_decode($response, true);
}

$kakaoUser = getKakaoUserInfo($accessToken);

if (!$kakaoUser) {
    http_response_code(401);
    echo json_encode(['error' => '카카오 정보 조회 실패']);
    exit;
}

// 데이터베이스에 사용자 저장 또는 업데이트 (예시)
$kakaoId = $kakaoUser['id'];
$email = $kakaoUser['kakao_account']['email'] ?? null;
$nickname = $kakaoUser['kakao_account']['profile']['nickname'] ?? '사용자';
$profileImage = $kakaoUser['kakao_account']['profile']['profile_image_url'] ?? null;

// DB 저장 로직 (실제 구현에서는 PDO나 ORM 사용)
// $db->insertOrUpdate('users', ['kakao_id' => $kakaoId, 'email' => $email, ...]);

// 세션에 사용자 정보 저장
$_SESSION['user_id'] = $kakaoId;
$_SESSION['user_email'] = $email;
$_SESSION['user_nickname'] = $nickname;
$_SESSION['user_profile_image'] = $profileImage;
$_SESSION['kakao_access_token'] = $accessToken;
$_SESSION['kakao_refresh_token'] = $refreshToken;

http_response_code(200);
echo json_encode([
    'success' => true,
    'message' => '로그인 성공',
    'user' => [
        'kakao_id' => $kakaoId,
        'email' => $email,
        'nickname' => $nickname,
    ]
]);
?>

 

정리와 다음 단계

카카오 Developers REST API를 PHP에서 연동하는 것은 정확한 헤더 형식, 안전한 데이터 파싱, 토큰 관리가 핵심이다. 토큰이 만료되었을 때 대응 방법을 미리 준비하고, 중첩된 JSON 필드를 안전하게 접근하는 습관이 모여서 안정적인 인증 시스템을 만든다. 이 글의 토큰 갱신 로직과 에러 처리 부분을 참고해 실제 프로젝트에 적용하면, 사용자 정보 조회 오류로 인한 서비스 중단을 크게 줄일 수 있을 것이다.