PHP에서 다른 서버의 페이지를 호출하거나 외부 API에 값을 던지고 응답을 받아야 할 때 가장 먼저 꺼내는 게 cURL입니다. 이름은 Client URL의 줄임말이고, 원래는 URL 문법으로 파일을 주고받는 커맨드라인 도구입니다. PHP에는 이 라이브러리를 감싼 curl_* 함수들이 들어 있어서, 원하는 주소로 값을 실어 보내고 그 결과를 문자열로 받아 오는 일을 코드로 처리할 수 있습니다.

file_get_contents() 로도 간단한 GET은 되지만 POST, 쿠키, 헤더 조작, HTTPS 인증서 처리처럼 조금만 복잡해지면 cURL 쪽이 훨씬 다루기 편합니다. 특히 allow_url_fopen 이 꺼져 있는 호스팅에서는 원격 URL을 열 방법이 사실상 cURL뿐입니다. 사용하려면 서버에 cURL 확장이 켜져 있어야 하는데, 리눅스는 curl.so, 윈도우는 php_curl.dll 확장 모듈이 필요합니다.

광고

cURL로 처리할 수 있는 대표적인 작업은 HTTPS 인증서 처리, HTTP POST·PUT, FTP 업로드, HTML 폼 전송, 쿠키, 인증 등입니다.

함수는 이 일곱 개면 충분하다

함수 하는 일
curl_init 세션을 만들고 핸들을 돌려줌
curl_setopt 옵션 지정
curl_exec 실제 요청 실행
curl_errno 마지막 에러 번호
curl_error 마지막 에러 메시지
curl_getinfo 전송 결과 정보(HTTP 코드 등)
curl_close 세션 종료

흐름은 늘 같습니다. curl_init 으로 핸들을 받고, curl_setopt 로 주소·방식·옵션을 채운 다음, curl_exec 로 실행하고, 결과를 확인한 뒤 닫습니다. 아래 예제들도 전부 이 틀 안에서 옵션만 바뀝니다.

예제 모음

1. POST로 데이터 보내기(가장 단순한 형태)

<?
$post_data = array(
"name" => "홍길동",
"birthday" => "1980-08-20"
);
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, http://www.example.com);
curl_setopt($ch, CURLOPT_POST, 1);
curl_setopt($ch, CURLOPT_POSTFIELDS, $post_data);
curl_exec($ch);
?>

주의: CURLOPT_URL 값에 따옴표가 빠져 있어서 이대로는 문법 오류가 납니다. "http://www.example.com" 처럼 문자열로 감싸야 합니다. 또 CURLOPT_POSTFIELDS 에 배열을 넘기면 multipart/form-data 로 전송되고, "a=1&b=2" 같은 문자열을 넘기면 일반 폼 방식(x-www-form-urlencoded)으로 나갑니다. 받는 쪽이 어떤 형식을 기대하는지에 따라 골라 쓰면 됩니다.

2. POST 요청을 함수로 묶기(쿠키·리퍼러 포함)

원문 코드에는 \"$url\" 처럼 따옴표 앞에 역슬래시가 붙어 있었는데, 게시 과정에서 생긴 이스케이프 흔적으로 보여 역슬래시만 지웠습니다. 나머지는 그대로입니다.

<?
function fetch_page($url,$param,$cookies,$referer_url){
if(strlen(trim($referer_url)) == 0) $referer_url= $url;
$curlsession = curl_init ();
curl_setopt ($curlsession, CURLOPT_URL, "$url");
curl_setopt ($curlsession, CURLOPT_POST, 1);
curl_setopt ($curlsession, CURLOPT_POSTFIELDS, "$param");
curl_setopt ($curlsession, CURLOPT_POSTFIELDSIZE, 0);
curl_setopt ($curlsession, CURLOPT_TIMEOUT, 60);
if($cookies && $cookies!=""){
curl_setopt ($curlsession, CURLOPT_COOKIE, "$cookies");
}
curl_setopt ($curlsession, CURLOPT_HEADER, 1); //헤더값을 가져오기위해 사용합니다. 쿠키를 가져오려고요.
curl_setopt ($curlsession, CURLOPT_USERAGENT, "Mozilla/4.0 (compatible; MSIE 5.01; Windows NT 5.0)");
curl_setopt ($curlsession, CURLOPT_REFERER, "$referer_url");
ob_start();
$res = curl_exec ($curlsession);
$buffer = ob_get_contents();
ob_end_clean();
if (!$buffer) {
$returnVal = "Curl Fetch Error : ".curl_error($curlsession);
}else{
$returnVal = $buffer;
}
curl_close($curlsession);
return $returnVal;
}
?>

CURLOPT_RETURNTRANSFER 를 쓰지 않고 출력 버퍼(ob_start)로 결과를 가로채는 방식입니다. 동작은 하지만 요즘은 CURLOPT_RETURNTRANSFER 를 켜서 curl_exec 의 반환값으로 받는 게 더 깔끔합니다. CURLOPT_HEADER 를 1로 둔 건 응답 헤더의 Set-Cookie 를 같이 받아 다음 요청에 쿠키를 넘기려는 의도입니다. 로그인이 필요한 페이지를 연달아 호출할 때 이런 구조를 씁니다.

3. 파일 전송

<?
$post_data['data[0]'] = "@image/img_01.jpg";
$post_data['data[0]'] = "@image/img_02.jpg";
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, http://www.example.com/upload.php);
curl_setopt($ch, CURLOPT_POST, 1);
curl_setopt($ch, CURLOPT_POSTFIELDS, $post_data);
$postResult = curl_exec($ch);
?>

이 예제는 지금 그대로 쓰기 어렵습니다. 두 줄 모두 data[0] 키에 넣고 있어서 두 번째 값이 첫 번째를 덮어쓰고(아마 data[1] 의도), URL 따옴표도 빠져 있습니다. 더 큰 문제는 파일 경로 앞에 @ 를 붙여 업로드하는 방식이 PHP 5.5부터 권장되지 않다가 PHP 7에서는 막혔다는 점입니다. 요즘 버전에서는 CURLFile 객체를 써야 합니다. 의도를 살려 고치면 대략 이런 모양이 됩니다.

<?php
$post_data['data[0]'] = new CURLFile('image/img_01.jpg');
$post_data['data[1]'] = new CURLFile('image/img_02.jpg');
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, "http://www.example.com/upload.php");
curl_setopt($ch, CURLOPT_POST, 1);
curl_setopt($ch, CURLOPT_POSTFIELDS, $post_data);
$postResult = curl_exec($ch);
?>

4. HTTPS 접속

<?
$ch = curl_init();
curl_setopt ($ch, CURLOPT_URL,"https://www.test.com"); //접속할 URL 주소
curl_setopt ($ch, CURLOPT_SSL_VERIFYPEER, FALSE); // 인증서 검증. true 로 두면 접속이 안 되는 경우가 많음
// 기본값이 true 라서 이 부분을 조심 (https 접속 시 필요)
curl_setopt ($ch, CURLOPT_SSLVERSION,3); // SSL 버전 (https 접속 시 필요)
curl_setopt ($ch, CURLOPT_HEADER, 0); // 헤더 출력 여부
curl_setopt ($ch, CURLOPT_POST, 1); // POST/GET 여부
curl_setopt ($ch, CURLOPT_POSTFIELDS, "var1=str1&var2=str2"); // POST 값을 GET 방식처럼 적는다
curl_setopt ($ch, CURLOPT_TIMEOUT, 30); // 타임아웃(초)
curl_setopt ($ch, CURLOPT_RETURNTRANSFER, 1); // 결과를 반환값으로 받을지
$result = curl_exec ($ch);
curl_close ($ch);
echo $result;
?>

HTTPS가 안 붙을 때 가장 흔히 쓰는 처방이 CURLOPT_SSL_VERIFYPEER 를 FALSE 로 끄는 건데, 이건 상대 서버 인증서를 확인하지 않겠다는 뜻이라 중간자 공격에 그대로 노출됩니다. 내부망 테스트 정도에서만 쓰고, 운영 환경에서는 CURLOPT_CAINFO 로 CA 번들 파일을 지정해 검증을 켜 두는 쪽이 맞습니다. CURLOPT_SSLVERSION 을 3(SSLv3)으로 고정하는 것도 지금은 오히려 접속 실패 원인이 됩니다. 대부분의 서버가 SSLv3를 막아 두었기 때문에, 특별한 이유가 없으면 이 줄은 빼고 cURL이 알아서 협상하게 두는 게 낫습니다.

5. Gmail 피드 읽기(기본 인증)

$src = "https://".$gmailId.":".$gmailPw."@mail.google.com/mail/feed/atom";
$ch = curl_init();
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, TRUE);
curl_setopt($ch, CURLOPT_TIMEOUT, 10);
curl_setopt ($ch, CURLOPT_HEADER, 0);
curl_setopt($ch, CURLOPT_POST,true);
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, 1);
curl_setopt($ch, CURLOPT_URL,$url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, TRUE);
curl_setopt($ch, CURLOPT_USERAGENT, 'My Agent Name');
curl_setopt($ch, CURLOPT_COOKIEJAR, 'cookie.txt');
curl_setopt($ch, CURLOPT_COOKIEFILE, 'cookie.txt');
$res = curl_exec($ch);
curl_close($ch);
/** 결과는 Atom XML 형식. DOM 이나 XML 파싱 함수로 파싱하면 됩니다. **/
echo $res;
?>

URL에 아이디와 비밀번호를 넣어 기본 인증을 하는 방식입니다. 주의할 점이 몇 가지 있습니다. 위에서 만든 주소는 $src 인데 CURLOPT_URL 에는 $url 을 넘기고 있어서 그대로는 엉뚱한 주소로 갑니다. 여는 <?php 태그도 빠져 있고요. 그리고 구글은 이런 비밀번호 기반 접근을 더 이상 허용하지 않기 때문에, 지금은 쿠키 저장(COOKIEJAR/COOKIEFILE)과 기본 인증을 어떻게 조합하는지 보는 참고용으로만 보시면 됩니다.

6. 웹페이지 내용 가져오기

<?php
function get_content($url) {
$agent = 'Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.0)';
$curlsession = curl_init ();
curl_setopt ($curlsession, CURLOPT_URL, $url);
curl_setopt ($curlsession, CURLOPT_HEADER, 0);
curl_setopt ($curlsession, CURLOPT_RETURNTRANSFER, 1);
curl_setopt ($curlsession, CURLOPT_POST, 0);
curl_setopt ($curlsession, CURLOPT_USERAGENT, $agent);
curl_setopt ($curlsession, CURLOPT_REFERER, "");
curl_setopt ($curlsession, CURLOPT_TIMEOUT, 3);
$buffer = curl_exec ($curlsession);
$cinfo = curl_getinfo($curlsession);
curl_close($curlsession);
if ($cinfo['http_code'] != 200)
{
return "";
}
return $buffer;
}
?>

복사해서 가져다 쓰기 제일 좋은 형태가 이겁니다. curl_getinfo() 로 HTTP 상태 코드를 확인해서 200이 아니면 빈 문자열을 돌려주니, 호출하는 쪽에서 빈 값인지만 보면 됩니다. 타임아웃이 3초로 짧게 잡혀 있어서 응답이 느린 사이트라면 값을 늘려야 합니다. 리다이렉트되는 주소라면 301/302가 돌아와 빈 문자열이 되므로 CURLOPT_FOLLOWLOCATION 을 같이 켜 주세요.

함수별로 알아 둘 것

curl_init([string $url]) 은 세션을 만들고 핸들을 돌려줍니다. 실패하면 false 입니다. 인자로 URL을 주면 CURLOPT_URL 을 따로 지정하지 않아도 됩니다. 가장 기본적인 사용 형태는 다음과 같습니다.

<?php
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, "http://www.example.com/");
curl_setopt($ch, CURLOPT_HEADER, 0);
curl_exec($ch);
curl_close($ch);
?>

curl_exec(resource ch) 는 준비해 둔 세션을 실행만 하는 함수라서, 반드시 curl_init 과 옵션 설정이 끝난 뒤에 불러야 합니다. CURLOPT_RETURNTRANSFER 를 켜지 않았다면 응답 본문은 바로 화면에 출력되고 반환값은 성공 여부(true/false)만 남습니다.

curl_errno(resource ch) 는 마지막 에러 번호를 주고, 에러가 없으면 0입니다. curl_error(resource ch) 는 같은 상황의 메시지 문자열을 주고, 에러가 없으면 빈 문자열입니다. 저는 curl_exec 결과가 false 일 때 이 둘을 같이 로그에 남겨 두는 편입니다. 숫자만 있으면 원인을 바로 알기 어렵고, 메시지만 있으면 분기 처리하기가 불편하거든요.

curl_getinfo(resource ch [, int opt]) 는 방금 끝난 전송의 정보를 돌려줍니다. 두 번째 인자 없이 부르면 HTTP 코드, 총 소요 시간, 최종 URL 등이 담긴 배열이 나옵니다.

curl_close(resource ch) 는 세션을 닫고 핸들 자원을 해제합니다. 참고로 PHP 8부터는 curl_init 이 리소스 대신 CurlHandle 객체를 돌려주고, curl_close 를 불러도 실제로는 아무 일도 하지 않습니다. 객체가 더 이상 참조되지 않으면 자동으로 정리되기 때문입니다. PHP 7과 8을 함께 지원해야 하는 코드라면 그냥 남겨 둬도 문제는 없습니다.

자주 쓰는 curl_setopt 옵션

옵션 설명
CURLOPT_URL 요청할 주소. curl_init() 에서 넘겼다면 생략 가능
CURLOPT_POST true(1)면 일반 폼 방식 POST, 0이면 GET
CURLOPT_POSTFIELDS POST로 보낼 데이터. 문자열(a=1&b=2) 또는 배열
CURLOPT_RETURNTRANSFER true면 응답을 화면에 바로 찍지 않고 curl_exec() 반환값으로 돌려줌
CURLOPT_HEADER true면 응답 헤더를 결과에 포함
CURLOPT_NOBODY true면 본문을 빼고 받음(헤더만 확인할 때)
CURLOPT_FOLLOWLOCATION true면 Location: 헤더를 따라 리다이렉트. 횟수 제한은 CURLOPT_MAXREDIRS
CURLOPT_TIMEOUT cURL 함수 실행에 허용할 최대 시간(초)
CURLOPT_SSL_VERIFYPEER false면 상대 인증서 검증 안 함. 기본값 true. 다른 인증서로 검증하려면 CURLOPT_CAINFO/CURLOPT_CAPATH
CURLOPT_SSL_VERIFYHOST 인증서의 호스트명 검증. 기본값 2
CURLOPT_SSLVERSION 사용할 SSL 버전. 보통은 PHP가 알아서 정함
CURLOPT_PORT 기본 포트 대신 접속할 포트 번호
CURLOPT_REFERER 요청의 Referer: 헤더 값
CURLOPT_USERAGENT 요청의 User-Agent: 헤더 값

옵션 몇 개는 원문에 사용 예시가 있었는데 그대로 옮기면 이렇습니다. 마지막 줄의 $cu 는 다른 예제의 $ch 와 같은 cURL 핸들입니다.

curl_setopt($ch, CURLOPT_HEADER, false);
curl_setopt($ch, CURLOPT_POST,1);
curl_setopt($ch, CURLOPT_RETURNTRANSFER,1);
curl_setopt ($ch, CURLOPT_SSL_VERIFYPEER, FALSE);
curl_setopt ($ch, CURLOPT_SSLVERSION,3);
curl_setopt($ch, CURLOPT_TIMEOUT,100);
curl_setopt($ch, CURLOPT_URL, 'http://www.exsample.com');
curl_setopt($ch, CURLOPT_USERAGENT,"Mozilla/4.0 (compatible; MSIE 5.01; Windows NT 5.0)");
curl_setopt($cu, CURLOPT_POSTFIELDS,$vars); // 보낼 데이터는 GET 쿼리 문자열 형식
// ex) $vars = "arg=$arg1&arg2=$arg2&arg3=$arg3";

CURLOPT_FOLLOWLOCATION 예시는 원문에서 curl_setopt($ch, CURLOPT_FOLLOWLOCATION, tru 로 잘려 있었습니다. 완성하면 아래와 같습니다.

curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);

curl_errno 에러 코드 표

curl_errno() 가 돌려주는 번호는 libcurl 에러 코드와 같습니다. 제가 자주 만난 건 6(호스트를 못 찾음), 7(접속 실패), 28(타임아웃), 35(SSL 핸드셰이크 실패), 51·60(인증서 문제) 정도입니다.

코드 의미
1 지원하지 않는 프로토콜. 이 cURL 빌드에 해당 프로토콜 지원이 없음
2 초기화 실패
3 URL 형식 오류. 주소 문법이 잘못됨
5 프록시 호스트 이름을 풀 수 없음
6 원격 호스트 이름을 풀 수 없음. 도메인이 정확한지, 네임서버가 제대로 등록됐는지 확인
7 호스트 접속 실패
8 FTP 서버 응답을 해석할 수 없음
9 FTP 접근 거부. 로그인 거부 또는 서버에 없는 디렉터리로 이동하려 한 경우가 많음
11 FTP PASS 요청에 대한 응답을 해석할 수 없음
13 FTP PASV 요청에 대한 응답을 해석할 수 없음
14 FTP 227 응답 줄 형식이 이상함
15 FTP 227 응답에서 받은 호스트 IP를 풀 수 없음
17 FTP 바이너리 전송 모드로 전환 실패
18 파일 일부만 전송됨
19 FTP 파일 다운로드/접근 실패(RETR 등 명령 실패)
21 FTP quote 명령이 서버에서 오류 반환
22 HTTP 페이지를 받지 못함. 400 이상 응답(-f/--fail 사용 시에만 발생)
23 로컬 파일 시스템 등에 쓰기 실패
25 FTP 업로드(STOR) 거부
26 읽기 오류
27 메모리 부족
28 지정한 제한 시간 초과
30 FTP PORT 명령 실패. PASV 방식으로 시도해 볼 것
31 FTP REST 명령 실패(이어받기 불가)
33 HTTP Range 요청 실패
34 HTTP POST 요청 생성 중 내부 오류
35 SSL 연결(핸드셰이크) 실패
36 중단된 FTP 다운로드를 이어받지 못함
37 FILE 프로토콜에서 파일을 열 수 없음(권한 확인)
38 LDAP bind 실패
39 LDAP 검색 실패
41 필요한 LDAP 함수를 찾을 수 없음
42 콜백에서 작업을 중단시킴
43 내부 오류. 함수에 잘못된 인자가 전달됨
45 지정한 송신 인터페이스를 사용할 수 없음
47 리다이렉트 최대 횟수 초과
48 알 수 없는 TELNET 옵션
49 잘못된 형식의 TELNET 옵션
51 상대 SSL 인증서 또는 SSH MD5 지문이 올바르지 않음
52 서버가 아무 응답도 보내지 않음
53 SSL 암호화 엔진을 찾을 수 없음
54 SSL 암호화 엔진을 기본값으로 설정할 수 없음
55 네트워크 데이터 전송 실패
56 네트워크 데이터 수신 실패
58 로컬 인증서 문제
59 지정한 SSL 암호(cipher)를 쓸 수 없음
60 알려진 CA 인증서로 상대 인증서를 검증할 수 없음
61 알 수 없는 전송 인코딩
62 잘못된 LDAP URL
63 최대 파일 크기 초과
64 요청한 FTP SSL 수준 설정 실패
65 데이터 재전송을 위한 되감기(rewind) 실패
66 SSL 엔진 초기화 실패
67 사용자 이름·비밀번호 등이 거부되어 로그인 실패
68 TFTP 서버에 파일 없음
69 TFTP 서버 권한 문제
70 TFTP 서버 디스크 공간 부족
71 허용되지 않는 TFTP 동작
72 알 수 없는 TFTP 전송 ID
73 파일이 이미 있음(TFTP)
74 해당 사용자 없음(TFTP)
75 문자 변환 실패
76 문자 변환 함수가 필요함
77 SSL CA 인증서 읽기 문제(경로·접근 권한 확인)
78 URL이 가리키는 리소스가 없음
79 SSH 세션 중 알 수 없는 오류
80 SSL 연결 종료 실패

이후 버전에서 번호가 더 추가될 수 있지만, 이미 정해진 번호의 의미는 바뀌지 않는 것이 원칙입니다.

인증서 에러 51, 60을 만났을 때

51번은 인증서에 적힌 이름과 접속한 호스트 이름이 다를 때 자주 납니다. 예를 들어 인증서는 www.test.co.kr 로 발급됐는데 test.co.kr 로 호출하면 SSL: certificate subject name 'www.test.co.kr' does not match target host name 'test.co.kr' 같은 메시지가 나옵니다. 가장 좋은 해결은 인증서가 발급된 주소 그대로 호출하는 것이고, 불가피하면 호스트명 검증을 끌 수 있습니다. 원문에서는 SOAP 클라이언트 설정으로 이렇게 처리했습니다.

$soapClient->setOpt('curl', CURLOPT_SSL_VERIFYHOST, 0);

60번은 상대 인증서를 신뢰할 수 있는 CA로 검증하지 못했다는 뜻입니다. 중간 인증서(체인)가 서버에 제대로 설치되지 않았거나, 내 서버의 CA 번들이 오래됐을 때 흔히 생깁니다. 메시지는 SSL certificate problem, verify that the CA cert is OK. Details: error:14090086:SSL routines:SSL3_GET_SERVER_CERTIFICATE:certificate verify failed 같은 형태입니다. 서로 믿을 수 있는 내부 서버끼리라면 검증을 끄는 방법도 있습니다.

$soapClient->setOpt('curl', CURLOPT_SSL_VERIFYPEER, 0);

다만 검증을 끄는 건 증상만 없애는 처방입니다. 외부 서비스와 통신하는 코드라면 먼저 CA 번들(cacert.pem)을 최신으로 받아 php.ini 의 curl.cainfo 나 CURLOPT_CAINFO 에 지정해 보고, 그래도 안 될 때만 검증 해제를 고려하는 순서가 안전합니다.

참고: cyworld everysons, 메모하는습관 - xshine.tistory.com