많은 개발자들이 OpenAI API만 사용하다가 Claude API의 존재를 간과한다. 하지만 Claude는 긴 문맥 처리, 한국어 이해도, 안정성 측면에서 OpenAI와 다른 강점을 가지고 있다. 문제는 대부분의 한국 개발자들이 Claude API 연동 방법을 정확히 모른 채 영문 문서만 보면서 시간을 낭비한다는 것이다. 이번에는 Anthropic Claude API를 PHP에서 실제로 연동하는 방법을 단계별로 정리하고, 메시지 전송, 스트리밍 처리, 오류 처리까지 완벽하게 설명하겠다.

 

1단계. Claude API 키 발급 및 기본 설정

Claude API를 사용하려면 먼저 Anthropic 공식 웹사이트에서 API 키를 발급받아야 한다. https://console.anthropic.com/에 접속해서 계정을 만들고 'API Keys' 메뉴에서 새 키를 생성한다. 발급된 키는 다시 볼 수 없으니 안전한 곳에 저장해야 한다.

PHP에서 Claude API를 호출하려면 cURL이나 Guzzle 라이브러리가 필요하다. 이 가이드에서는 cURL을 기본으로 설명하겠다. 환경변수나 .env 파일에 API 키를 저장하는 것이 보안 원칙이다.

<?php
// .env 파일 또는 환경변수에서 로드
$apiKey = getenv('ANTHROPIC_API_KEY');
$apiEndpoint = 'https://api.anthropic.com/v1/messages';

// API 키가 없으면 에러
if (empty($apiKey)) {
    die('ANTHROPIC_API_KEY 환경변수가 설정되지 않았습니다.');
}
?>

 

Claude API 버전 확인

Anthropic은 자주 새 모델을 출시한다. 2024년 기준 claude-3-5-sonnet이 가장 성능이 좋으면서도 저렴한 모델이다. 최신 모델 정보는 공식 문서에서 확인해야 하며, 호출할 때마다 모델 이름을 명시해야 한다.

 

2단계. 기본 메시지 API 호출

Claude API의 기본 구조는 간단하다. 요청 본문에 메시지 내용과 모델을 지정하고, Authorization 헤더에 API 키를 포함해서 POST 요청을 보내면 된다.

<?php
$apiKey = getenv('ANTHROPIC_API_KEY');
$apiEndpoint = 'https://api.anthropic.com/v1/messages';

// ✗ 잘못된 코드: API 키를 헤더에 그냥 붙이거나 없이 보냄
$wrongHeaders = [
    'Content-Type: application/json',
    'x-api-key: ' . $apiKey // 이건 deprecated 방식
];

// ✓ 올바른 코드: Anthropic 최신 인증 방식 사용
$headers = [
    'x-api-key: ' . $apiKey,
    'Content-Type: application/json',
    'anthropic-version: 2023-06-01'
];

$requestBody = [
    'model' => 'claude-3-5-sonnet-20241022',
    'max_tokens' => 1024,
    'messages' => [
        [
            'role' => 'user',
            'content' => '안녕하세요. 당신은 누구인가요?'
        ]
    ]
];

$ch = curl_init($apiEndpoint);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($requestBody));
curl_setopt($ch, CURLOPT_TIMEOUT, 30);

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

// ✗ 잘못된 처리: 응답을 그냥 decode하고 에러 확인 안 함
if ($httpCode === 200) {
    $data = json_decode($response, true);
    echo $data['content'][0]['text'];
}

// ✓ 올바른 처리: HTTP 상태코드와 curl 에러 함께 확인
if (!empty($curlError)) {
    die('cURL 에러: ' . $curlError);
}

if ($httpCode !== 200) {
    $errorData = json_decode($response, true);
    die('API 에러 (' . $httpCode . '): ' . ($errorData['error']['message'] ?? '알 수 없는 오류'));
}

$data = json_decode($response, true);
echo 'Claude 응답: ' . $data['content'][0]['text'];
?>

응답 결과는 이렇게 나온다.

Claude 응답: 저는 Anthropic에서 만든 AI 어시스턴트 Claude입니다. 대화, 질문 답변, 분석, 창작 등 다양한 작업을 도와드릴 수 있습니다.

 

3단계. 스트리밍 응답 처리

Claude API는 스트리밍을 지원한다. 긴 응답이 필요할 때 한 번에 모든 데이터를 받지 않고 실시간으로 토큰 단위로 받을 수 있다. 이는 사용자 경험을 크게 향상시킨다.

<?php
$apiKey = getenv('ANTHROPIC_API_KEY');
$apiEndpoint = 'https://api.anthropic.com/v1/messages';

$headers = [
    'x-api-key: ' . $apiKey,
    'Content-Type: application/json',
    'anthropic-version: 2023-06-01'
];

$requestBody = [
    'model' => 'claude-3-5-sonnet-20241022',
    'max_tokens' => 1024,
    'stream' => true, // ← 스트리밍 활성화
    'messages' => [
        [
            'role' => 'user',
            'content' => '파이썬으로 간단한 뉴럴네트워크를 만드는 예제를 작성해줘'
        ]
    ]
];

$ch = curl_init($apiEndpoint);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, false); // 스트림이므로 false
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($requestBody));
curl_setopt($ch, CURLOPT_TIMEOUT, 60);

// ✗ 잘못된 코드: 스트림 데이터를 일반 문자열처럼 처리
$response = curl_exec($ch); // 이렇게 하면 스트림이 작동 안 함

// ✓ 올바른 코드: 콜백 함수로 스트림 데이터 처리
curl_setopt($ch, CURLOPT_FILE, $fp); // 파일 핸들로 받기
// 또는
$output = '';
curl_setopt($ch, CURLOPT_WRITEFUNCTION, function($curl, $data) use (&$output) {
    $output .= $data;
    
    // SSE(Server-Sent Events) 형식의 데이터 파싱
    $lines = explode("\n", $output);
    $output = array_pop($lines); // 마지막 불완전한 라인은 남겨두기
    
    foreach ($lines as $line) {
        if (empty($line)) continue;
        
        // 'data: {...}' 형식 파싱
        if (strpos($line, 'data: ') === 0) {
            $jsonStr = substr($line, 6);
            $event = json_decode($jsonStr, true);
            
            // content_block_delta 이벤트가 실제 텍스트 스트림
            if ($event && $event['type'] === 'content_block_delta') {
                if ($event['delta']['type'] === 'text_delta') {
                    echo $event['delta']['text'];
                    flush(); // 브라우저에 즉시 전송
                }
            }
        }
    }
    return strlen($data);
});

curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);

if ($httpCode !== 200) {
    die('API 에러: HTTP ' . $httpCode);
}
?>

위 코드를 실행하면 Claude의 응답이 실시간으로 한 단어씩 화면에 나타난다. 이는 일반 API 호출과는 다르게 사용자에게 응답이 로딩되고 있다는 피드백을 준다.

 

4단계. 다중 턴 대화(Multi-turn Conversation) 구현

Claude와 여러 번 주고받으며 대화하려면 대화 히스토리를 유지해야 한다. 모든 이전 메시지를 messages 배열에 포함시키면 된다.

<?php
function callClaudeAPI($messages, $apiKey) {
    $apiEndpoint = 'https://api.anthropic.com/v1/messages';
    
    $headers = [
        'x-api-key: ' . $apiKey,
        'Content-Type: application/json',
        'anthropic-version: 2023-06-01'
    ];
    
    $requestBody = [
        'model' => 'claude-3-5-sonnet-20241022',
        'max_tokens' => 2048,
        'messages' => $messages // 전체 대화 히스토리
    ];
    
    $ch = curl_init($apiEndpoint);
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
    curl_setopt($ch, CURLOPT_POST, true);
    curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($requestBody));
    curl_setopt($ch, CURLOPT_TIMEOUT, 30);
    
    $response = curl_exec($ch);
    $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
    curl_close($ch);
    
    if ($httpCode !== 200) {
        throw new Exception('Claude API 에러: ' . $httpCode);
    }
    
    $data = json_decode($response, true);
    return $data['content'][0]['text'];
}

// ✗ 잘못된 코드: 매번 새 배열로 시작
$apiKey = getenv('ANTHROPIC_API_KEY');

$conversation = []; // 대화 히스토리

// 첫 번째 메시지
$userMessage = '한국 전통 음식의 특징을 설명해줘';
$conversation[] = ['role' => 'user', 'content' => $userMessage];

try {
    $response = callClaudeAPI($conversation, $apiKey);
    $conversation[] = ['role' => 'assistant', 'content' => $response];
    echo '첫 번째 응답: ' . $response . "\n\n";
} catch (Exception $e) {
    echo '에러: ' . $e->getMessage();
    exit;
}

// ✓ 올바른 코드: 히스토리를 유지하면서 새 메시지 추가
$followUpMessage = '그 중에서 가장 건강한 음식은?';
$conversation[] = ['role' => 'user', 'content' => $followUpMessage];

$response = callClaudeAPI($conversation, $apiKey);
$conversation[] = ['role' => 'assistant', 'content' => $response];
echo '두 번째 응답: ' . $response . "\n\n";

// 세 번째 턴
$thirdMessage = '만드는 방법을 알려줘';
$conversation[] = ['role' => 'user', 'content' => $thirdMessage];

$response = callClaudeAPI($conversation, $apiKey);
$conversation[] = ['role' => 'assistant', 'content' => $response];
echo '세 번째 응답: ' . $response;
?>

이렇게 하면 Claude가 이전 대화 맥락을 기억하고 일관성 있게 응답한다.

 

5단계. 오류 처리와 재시도 로직

실제 운영 환경에서는 네트워크 오류, API 레이트 리미트, 타임아웃 등이 발생할 수 있다. 이를 대비한 견고한 에러 핸들링이 필수다.

<?php
class ClaudeAPIClient {
    private $apiKey;
    private $maxRetries = 3;
    private $retryDelay = 1000; // ms
    
    public function __construct($apiKey) {
        $this->apiKey = $apiKey;
    }
    
    // ✗ 잘못된 코드: 재시도 없이 한 번만 요청
    public function callWrong($messages) {
        $response = $this->makeRequest($messages);
        return json_decode($response, true);
    }
    
    // ✓ 올바른 코드: 지수 백오프로 재시도
    public function call($messages) {
        $attempt = 0;
        
        while ($attempt < $this->maxRetries) {
            try {
                return $this->makeRequest($messages);
            } catch (Exception $e) {
                $attempt++;
                
                // 429(Rate Limit) 또는 503(Service Unavailable)이면 재시도
                if ($e->getCode() === 429 || $e->getCode() === 503) {
                    if ($attempt < $this->maxRetries) {
                        $delay = $this->retryDelay * pow(2, $attempt - 1); // 지수 백오프
                        usleep($delay * 1000);
                        continue;
                    }
                }
                
                // 다른 에러는 즉시 throw
                throw $e;
            }
        }
        
        throw new Exception('최대 재시도 횟수 초과');
    }
    
    private function makeRequest($messages) {
        $apiEndpoint = 'https://api.anthropic.com/v1/messages';
        
        $headers = [
            'x-api-key: ' . $this->apiKey,
            'Content-Type: application/json',
            'anthropic-version: 2023-06-01'
        ];
        
        $requestBody = [
            'model' => 'claude-3-5-sonnet-20241022',
            'max_tokens' => 1024,
            'messages' => $messages
        ];
        
        $ch = curl_init($apiEndpoint);
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
        curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
        curl_setopt($ch, CURLOPT_POST, true);
        curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($requestBody));
        curl_setopt($ch, CURLOPT_TIMEOUT, 30);
        curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 10);
        
        $response = curl_exec($ch);
        $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
        $curlError = curl_error($ch);
        curl_close($ch);
        
        // cURL 에러
        if (!empty($curlError)) {
            throw new Exception('네트워크 에러: ' . $curlError, 0);
        }
        
        // HTTP 에러
        if ($httpCode !== 200) {
            $errorData = json_decode($response, true);
            $errorMessage = $errorData['error']['message'] ?? '알 수 없는 에러';
            throw new Exception('Claude API 에러: ' . $errorMessage, $httpCode);
        }
        
        return json_decode($response, true)['content'][0]['text'];
    }
}

// 사용 예시
$apiKey = getenv('ANTHROPIC_API_KEY');
$client = new ClaudeAPIClient($apiKey);

try {
    $response = $client->call([
        ['role' => 'user', 'content' => '시간이 얼마나 걸릴 것 같은가?']
    ]);
    echo '응답: ' . $response;
} catch (Exception $e) {
    echo '최종 실패: ' . $e->getMessage();
}
?>

 

주의사항과 흔한 실수
실수 해결책
✗ stream: true로 설정했는데 일반 응답만 받음 ✓ CURLOPT_RETURNTRANSFER를 false로 설정하고 WRITEFUNCTION으로 데이터 수신
✗ API 키를 Authorization 헤더에 넣음 ✓ x-api-key 헤더에만 API 키를 넣음. Anthropic은 Basic Auth를 사용하지 않음
✗ 대화 중 이전 메시지를 messages 배열에서 제거 ✓ 모든 턴의 메시지를 유지. 문맥 손실로 응답 품질 저하
✗ max_tokens를 매우 크게 설정(예: 128000) ✓ 필요한 만큼만 설정. 큰 값은 비용 증가와 응답 시간 지연
✗ 스트리밍에서 flush() 호출 안 함 ✓ 실시간 출력을 위해 flush()를 호출해 버퍼를 즉시 전송

 

실전 예제: 간단한 챗봇 구현

지금까지 배운 내용을 모두 활용해 간단한 웹 기반 챗봇을 만들어보자.

<?php
session_start();

class SimpleChatBot {
    private $apiKey;
    private $sessionKey = 'claude_conversation';
    
    public function __construct($apiKey) {
        $this->apiKey = $apiKey;
        // 새 세션이면 대화 히스토리 초기화
        if (!isset($_SESSION[$this->sessionKey])) {
            $_SESSION[$this->sessionKey] = [];
        }
    }
    
    public function addUserMessage($text) {
        $_SESSION[$this->sessionKey][] = [
            'role' => 'user',
            'content' => $text
        ];
    }
    
    public function getResponse() {
        if (empty($_SESSION[$this->sessionKey])) {
            return '먼저 메시지를 입력해주세요.';
        }
        
        $headers = [
            'x-api-key: ' . $this->apiKey,
            'Content-Type: application/json',
            'anthropic-version: 2023-06-01'
        ];
        
        $requestBody = [
            'model' => 'claude-3-5-sonnet-20241022',
            'max_tokens' => 1024,
            'system' => '당신은 친절한 한국어 챗봇입니다. 사용자의 질문에 명확하고 도움이 되는 답변을 해주세요.',
            'messages' => $_SESSION[$this->sessionKey]
        ];
        
        $ch = curl_init('https://api.anthropic.com/v1/messages');
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
        curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
        curl_setopt($ch, CURLOPT_POST, true);
        curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($requestBody));
        curl_setopt($ch, CURLOPT_TIMEOUT, 30);
        
        $response = curl_exec($ch);
        $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
        curl_close($ch);
        
        if ($httpCode !== 200) {
            $errorData = json_decode($response, true);
            return '오류 발생: ' . ($errorData['error']['message'] ?? 'Unknown error');
        }
        
        $data = json_decode($response, true);
        $assistantMessage = $data['content'][0]['text'];
        
        // 히스토리에 추가
        $_SESSION[$this->sessionKey][] = [
            'role' => 'assistant',
            'content' => $assistantMessage
        ];
        
        return $assistantMessage;
    }
    
    public function clearHistory() {
        $_SESSION[$this->sessionKey] = [];
    }
}

// 요청 처리
header('Content-Type: application/json');

if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    $input = json_decode(file_get_contents('php://input'), true);
    $action = $input['action'] ?? '';
    $apiKey = getenv('ANTHROPIC_API_KEY');
    
    $chatbot = new SimpleChatBot($apiKey);
    
    if ($action === 'send') {
        $userMessage = $input['message'] ?? '';
        if (empty($userMessage)) {
            echo json_encode(['error' => '메시지를 입력해주세요']);
            exit;
        }
        
        $chatbot->addUserMessage($userMessage);
        $response = $chatbot->getResponse();
        echo json_encode(['response' => $response]);
    } elseif ($action === 'clear') {
        $chatbot->clearHistory();
        echo json_encode(['status' => 'cleared']);
    }
}
?>

HTML 프론트엔드는 JavaScript로 위 PHP 엔드포인트를 호출하면 된다. 각 사용자별 대화 히스토리가 세션에 저장되므로 여러 사용자가 동시에 독립적으로 사용할 수 있다.

 

마무리

Claude API는 OpenAI보다 복잡한 문맥 처리와 한국어 이해에 탁월하다. 작은 최적화와 올바른 에러 처리가 모여서 안정적이고 빠른 AI 서비스를 만든다는 점을 잊지 말자. 이 글의 스트리밍 처리와 재시도 로직을 참고해 실제 프로덕션 환경에 적용하면, 사용자에게 신뢰감 있는 챗봇 경험을 제공할 수 있을 것이다. 특히 max_tokens 조정과 system 프롬프트 최적화를 통해 응답 품질을 더욱 높일 수 있으니 꼭 시도해보길 바란다.