외부 API를 연동하거나 HTTPS 요청을 보낼 때 갑자기 나타나는 cURL ERROR 60. "SSL certificate problem: unable to get local issuer certificate" 메시지를 본 개발자라면 얼마나 답답한지 알 것이다. 다만 대부분의 개발자들은 이 에러의 정확한 원인도 모른 채 구글에서 가져온 한두 줄의 임시방편 코드를 그냥 복사해 붙인다.
이번에는 이 에러가 정확히 뭔지, 왜 발생하는지, 그리고 상황에 맞는 올바른 해결책이 뭔지 실무 경험을 바탕으로 완벽하게 정리하겠다.

 

1단계: cURL ERROR 60이 뭐지?

SSL 인증서 검증 과정에서 발생하는 에러다. cURL은 HTTPS 요청을 할 때 서버의 SSL 인증서가 정말 신뢰할 수 있는지 확인하는데, 이 과정에서 CA(Certificate Authority) 인증서를 찾지 못하거나 검증에 실패하면 ERROR 60이 발생한다.

쉽게 말해, cURL이 "이 HTTPS 서버 정말 믿을 만해?"라고 물어봤는데 답변할 수 없는 상황인 것이다. PHP가 설치된 환경에 CA 인증서 리스트가 없거나, 잘못된 경로를 가리키고 있을 때 자주 발생한다.

 

2단계: 왜 발생하나?

주로 세 가지 원인이 있다:

① 로컬 환경의 CA 번들 부재
개발 PC나 특정 서버 환경에서는 SSL 인증서를 검증할 CA 번들 파일이 없을 수 있다.

② php.ini에서 curl.cainfo 미설정
php.ini에서 CA 번들 경로를 제대로 지정하지 않았을 때다.

③ 자체 서명 인증서(Self-signed Certificate)
테스트 환경에서 자체 서명 인증서를 사용하는 경우 발생할 수 있다.

 

3단계: 실전 해결책

 

✗ 잘못된 방법 1: VERIFYPEER 무조건 끄기

인터넷에서 가장 흔히 보이는 방법이 이것이다:

<?php
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, 'https://api.example.com/data');
curl_setopt($ch, CURLOPT_VERIFYPEER, false); // ✗ 매우 위험!
curl_setopt($ch, CURLOPT_VERIFYHOST, false);
$result = curl_exec($ch);
curl_close($ch);
?>

이 방법은 "SSL 검증을 완전히 무시하라"는 뜻이다. 보안 위험이 크다. 중간에서 요청을 가로채거나 위조된 HTTPS 서버에 연결될 수 있다. 절대 프로덕션 환경에서 쓰면 안 된다.

 

✓ 올바른 방법 1: CA 번들 파일 다운로드 및 지정 (권장)

Mozilla에서 관리하는 공식 CA 번들을 다운로드하는 방법이다. 가장 안전하고 정확하다.

Step 1: CA 번들 파일 다운로드

cd /your/project/directory
wget https://curl.se/ca/cacert.pem
# 또는
curl https://curl.se/ca/cacert.pem -o cacert.pem

프로젝트의 안전한 디렉토리(예: /config, /ssl)에 저장한다.

Step 2: cURL 코드에서 경로 지정

<?php
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, 'https://api.example.com/data');
// CA 번들 경로를 명시적으로 지정
curl_setopt($ch, CURLOPT_CAINFO, __DIR__ . '/config/cacert.pem');
curl_setopt($ch, CURLOPT_VERIFYPEER, true);
curl_setopt($ch, CURLOPT_VERIFYHOST, 2);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);

$result = curl_exec($ch);

// 에러 확인
if (curl_errno($ch)) {
    echo 'cURL Error: ' . curl_error($ch);
}
curl_close($ch);
echo $result;
?>

이 방법이 제일 좋다. CA 번들을 명시적으로 지정하므로 어느 환경에서나 동작하고, VERIFYPEER와 VERIFYHOST를 true로 유지해 보안을 지킨다.

 

✓ 올바른 방법 2: php.ini에 curl.cainfo 설정

php.ini에 한 번 설정하면 모든 cURL 요청이 그 경로를 기본값으로 사용한다.

; php.ini
curl.cainfo = "/path/to/cacert.pem"
; 또는 (Windows)
curl.cainfo = "C:\path\to\cacert.pem"

설정 후 PHP를 재시작해야 반영된다. 이 방법도 좋지만, 호스팅 환경에서는 php.ini 수정이 제한될 수 있다.

 

✓ 올바른 방법 3: 래퍼 함수로 재사용 가능하게

여러 곳에서 cURL을 쓴다면 래퍼 함수를 만드는 게 좋다:

<?php
function safe_curl_request($url, $method = 'GET', $data = null) {
    $ch = curl_init();
    
    // 기본 옵션
    curl_setopt($ch, CURLOPT_URL, $url);
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    curl_setopt($ch, CURLOPT_TIMEOUT, 10);
    
    // SSL 검증 (중요!)
    curl_setopt($ch, CURLOPT_CAINFO, __DIR__ . '/config/cacert.pem');
    curl_setopt($ch, CURLOPT_VERIFYPEER, true);
    curl_setopt($ch, CURLOPT_VERIFYHOST, 2);
    
    // HTTP 메서드 설정
    if ($method === 'POST') {
        curl_setopt($ch, CURLOPT_POST, 1);
        if ($data) {
            curl_setopt($ch, CURLOPT_POSTFIELDS, is_array($data) ? http_build_query($data) : $data);
        }
    } else if ($method === 'PUT' || $method === 'DELETE') {
        curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $method);
        if ($data) {
            curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data));
        }
    }
    
    $response = curl_exec($ch);
    $error = curl_error($ch);
    curl_close($ch);
    
    if ($error) {
        throw new Exception('cURL Error: ' . $error);
    }
    
    return $response;
}

// 사용 예
try {
    $result = safe_curl_request('https://api.example.com/users', 'GET');
    echo $result;
} catch (Exception $e) {
    echo $e->getMessage();
}
?>

이렇게 하면 매번 cURL 옵션을 다시 쓸 필요가 없고, SSL 검증도 자동으로 적용된다.

 

4단계: 주의사항

CA 번들 파일을 정기적으로 업데이트하자.
인증서 발급 기관의 변동으로 CA 번들이 주기적으로 업데이트된다. 너무 오래된 파일을 쓰면 새 인증서가 검증되지 않을 수 있다. 최소 6개월에 한 번은 확인하자.

개발/테스트 환경과 프로덕션을 구분하자.
로컬 개발 환경에서만 임시로 VERIFYPEER를 끄는 것은 괜찮지만, 프로덕션에는 절대 올리면 안 된다. 환경 변수로 분리하자:

<?php
if (getenv('APP_ENV') === 'production') {
    curl_setopt($ch, CURLOPT_VERIFYPEER, true);
    curl_setopt($ch, CURLOPT_CAINFO, '/path/to/cacert.pem');
} else {
    curl_setopt($ch, CURLOPT_VERIFYPEER, false); // 로컬 개발만
}
?>

cURL 버전 확인하기.
매우 오래된 cURL 버전에서는 일부 옵션이 작동하지 않을 수 있다. curl_version()으로 확인하자:

<?php
echo 'cURL version: ' . curl_version()['version'];
?>

 

5단계: 최종 정리

cURL ERROR 60은 겉으로는 복잡해 보이지만, 결국 SSL 인증서를 검증할 CA 번들이 없거나 잘못 지정된 것이다. 인터넷에서 흔히 보이는 VERIFYPEER 무조건 끄기는 보안상 위험하고, 올바른 해결책은 Mozilla CA 번들을 다운로드해서 cURL 코드에서 명시적으로 경로를 지정하는 것이다. 한 번 이 방식으로 래퍼 함수를 만들어두면 프로젝트의 모든 cURL 요청에 안전성을 보장할 수 있다. 특히 결제 API나 인증 서버처럼 보안이 중요한 외부 API 연동할 때는 절대 검증을 무시하면 안 된다는 점을 기억하자.