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