
본 블로그에서 [작업일지]로 올라오는 코드는 한 차례 이상 재편집 되었습니다.
실 작업에 사용되는 코드가 아니며, 로직 등 참고로만 봐주세요.
1. 기획
기획 내용
RSS는 Really Simple Syndication의 약자로 블로그나 뉴스 사이트의 새로운 글을 자동으로 받기 위해서 만들어져 있다. 당장 티스토리도 지원 중이기도 하다. 개인 홈페이지 간에도 RSS 피드를 통해 서로의 글을 받아보거나 할 수는 있을 것이다. 그러나 이 경우 상호의 글을 받기 위해선 상호가 RSS를 만들어야 하며, 보안을 위해서는 상호 API 키를 주고 받는 것이 좋을 텐데, 너무 번거롭다. 안 하기에는 파일을 다운 받는 것만으로 상대의 홈페이지를 건들 수 있다는 것이라 보안적으로 좋지 않다.
들어온 요청은 개인 홈페이지 커뮤니티에서 서로의 새 글에 대한 알림을 받아보고 싶다는 것이었다. 소규모라면 모를까 10명 정도는 된다. 그 사람들 모두에게 서로의 API 키를 주고 받으세요 할 수는 없다. 다행히도 해당 커뮤니티에는 커뮤니티 관리자가 있었다. 관리자 홈페이지에서 해당 커뮤니티가 관리되고는 했으니, 관리자의 홈페이지를 허브HUB 형태로 사용하면 되었다.

사실 허브가 되는 페이지 자체에서 타 페이지에게 보내는 것도 괜찮다. 왜냐하면 보내는 파일의 유출이 없을 테니까. API 판단 없이 허브가 되는 페이지의 주소만 인가하면 receive 하는 쪽에서도 문제가 없다.
필요한 파일
이를 위해서 필요한 파일(페이지)은 다음과 같다.
허브 페이지의 경우
- API 키 발급 및 보관 및 관리
- 각 포스트 알림을 receive
- 포스트 알림의 목록을 보관 및 관리
전송할 페이지의 경우(회원 페이지)
- config(설정) 페이지(반드시 필요하지는 않지만 용이를 위해 분리 추가했다)
- 포스트 알림을 보낼 페이지
홈페이지들이 PHP 기반인 그누보드였으므로, 글 작성 또한 PHP를 기반으로 만들어진다. 따라서 언어는 PHP를 기반으로 하고, 저장 방식 등을 정해야 했다.
2. API 키 발급 및 관리
API 키 저장 방식 정하기
관리자 페이지가 있기 때문에 발급 및 관리는 커뮤니티 관리자가 관리하기 쉽도록 해당 페이지에 만들었다. 가장 중요한 건, 발급 API의 관리도 가능해야 한다. 저장되지 않을 경우 구분 및 인증에 의미가 없어지므로 API 키는 무용지물이 된다. 저장 방법은 다음 두 가지 선택지가 있다. 파일로 저장할지, 아니면 DB로 저장할지였다. 보통의 경우 json 파일 같은 걸로 저장하면 압도적으로 DB 쪽이 더 보안이 좋다. 그러나 아이디나 비밀번호 같은 것은 아니기도 하고, json 수련도 할 겸 이번에는 json으로 결정했다. 그리고 API 키만이 아니라 발급받은 사람의 닉네임으로 한 차례 더 필터하면 접근 보안의 큰 문제는 없을 것이다(아예 API 키가 담긴 파일이 유출된다면 모를까).
API 키의 발급은 openssl_random_pseudo_bytes 메서드를 사용했다. 이 경우 openssl 모듈이 깔려 있어야 하지만 해당 페이지에는 깔려 있었다.
json의 양식은 다음 처럼 정했다.
key(API 키니 당연하다), sitename(커뮤니티 사람들의 분간을 위해서다), createdat(생성일을 정했다.)
참고로 json 파일은 다음처럼 작성한다. 맨 위와 아래의 대괄호에 주의한다.
[
{ "key": "...",
"sitename": "...",
"createdat": "...", ... },
{ "key": "...",
"sitename": "...",
"createdat": "...", ... }
]
이렇게 하면 이제 관리자 페이지에서 해당 json 파일을 생성…하면 좋지만, 요즘은 파일 직접 생성 등이 막혀 있어서, 그냥 빈 json 파일을 넣는 것으로 했다. 이렇게 하면 쓰고 읽는 것은 문제가 없다.
관리자 페이지 만들기
일단 json file을 읽고 쓰는 함수를 만든다. 한 차례만 이루어지기 때문에 굳이 function으로 만들 필요는 없지만, 길기 때문에 function으로 따로 정리했다.
function read_json_file($filepath) {
if (!file_exists($filepath)) {
return [];
}
$content = file_get_contents($filepath);
$data = json_decode($content, true);
return is_array($data) ? $data : [];
}
function write_json_file($filepath, $data) {
$json = json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
return file_put_contents($filepath, $json, LOCK_EX);
}
JSON_PRETTY_PRINT: json_encode 옵션 중 하나다. json 파일을 보기 좋게 정리해준다.
JSON_UNESCAPED_UNICODE: 유니코드로 escape하지 않는 옵션이다. 한국어 json 데이터는 유니코드로 escape 되게 저장되는 것이 기본이기 때문에, 한글 그대로 저장 되도록 하는 옵션 중 하나다.
사실 이것만 하면 끝나는 편이다. 나머진 그냥 HTML 파일을 생성하면 될 뿐 … form을 사용하여 POST 형태로 내용을 보내어서 그대로 json 파일로 하면 된다(write_json_file). 그리고 불러들이는 것도, read_json_file을 사용하여 부른 후 배열로 돌려서 그대로 프린트 하도록 하면 된다.

이것으로 API 키 발급 처리는 종료. 이제 포스트에 대한 것이다.
3. 포스트 알람을 보내기
설정config 만들기
포스트 알림을 보내는 입장에서 API 키를 발급 받았기 때문에, API 키를 입력할 장소가 필요하다. 위에서 말했다 시피, 그냥 API 키와 닉네임을 그냥 post를 보내면서 보내도 되지만, 그 경우 파일의 복잡함이 커진다. 커뮤니티 사람들의 PHP 숙련도는 천차만별이기 때문에, 초보자여도 문제 없도록 파일을 분리하는 것이 좋다.
설정config 파일은 다음처럼 이루어져야 했다.
- recieve 페이지의 주소. 적지 않는다면 통신이 불가능하다.
- API 키
- 닉네임
- 알림 보낼 게시판. 프라이버시한 게시판에 대한 알림이 가면 상호 곤란하기 때문에 필터를 거친다.
- 그 외 옵션적인 부분들(디버깅 등)
이 모든 것이 충족되면 포스트 알림을 보낸다. 그렇지 않다면 작용하지 않도록 처리했다.
알림 보내기
통신 방식- fsockopen? cURL?
다른 사이트에 접근하기 위해서는 보통의 방법으론 안 된다. 요즘은 보안을 위해서 제약이 많아지고 있다. 더군다나 다른 사이트의 요소를 함부로 가져오는 것은, 해당 사이트의 트래픽에 누가 되므로 방식은 매우 제한적이다. 본래 PHP에서는 fopen과 같은 방식으로도 다른 사이트의 컨텐츠를 가져올 수 있었는데, 요즘 대부분의 홈페이지에서는 지원하지 않는다. 크로스 호스팅 등으로 인하여 막혀 있는 것이나 다름 없다.
이에 따라 fsockopen 혹은 cURL이 무난한 선택이다. 본래 사용하던 방식은 cURL인데, 보안도 그렇고 코드도 그렇고 간편하기 때문이다. 하지만 cURL을 사용하면서 '많이 느리다'라는 생각을 한 적도 있다. 그리고 cURL 미지원 홈페이지도 존재한다. 기본적으로 cURL은 라이브러리이기 때문에, fsockopen과는 달리 깔려 있지 않으면 사용하지 않는다. 고민을 하다가 cURL이 지원될 경우 cURL로, 아닐 경우 fsockopen을 사용하기로 했다. 사실 이 때 당시에는 file_get_contents도 충분히 지원된다는 사실을 몰랐기 때문에(예전에 했을 때는 오류가 났었다), 이 둘이 되었다.
| 비교 항목 | fsockopen | cURL |
| 종류 | PHP 함수 | 확장 라이브러리 |
| 오버헤드 | 적음 | 많음 |
| 속도 | 빠름 | 느릴 수 있음 |
| 복잡한 요청 | 느림 | 빠름 |
| HTTPS | stream_context_create 필요 | 자동 지원 |
| 대용량 업로드 | 느림 | 빠름 |
| HTTP GET/POST | 직접 구현 | 자동 지원 |
| SSL 검증 | 선택 가능(빠름) | 기본으로 실행(느림) |
| 파일 업로드 | 복잡 | 간단 |
| 인증 | 직접 구현 | 자동 지원 |
여기서 오버헤드란, 특정 기능을 수행하는데 소비되는 간접적인 시간이나 메모리 등의 자원을 말한다. 보면 알겠지만 fsockopen이 좀 더 빠르다. 대신 확장 라이브러리인 cURL보다 더 번거롭고, 가능한 기능도 적다. fsockopen의 기본적인 사용 방식은 다음과 같다.
fsockopen(
string $hostname,
int $port = -1,
int &$error_code = null,
string &$error_message = null,
?float $timeout = null
): resource|false
여기서 $hostname은 ssl 주소를 넣으면 된다. 그 외에는 보이는 그대로다.
반면 cURL의 사용은 다음과 같다.
기본적인 함수
// 1. curl_init() - cURL 세션 초기화
$ch = curl_init();
// 또는 URL 바로 지정
$ch = curl_init("http://example.com");
// 2. curl_setopt() - cURL 옵션 설정 (가장 많이 사용)
curl_setopt($ch, CURLOPT_URL, "http://example.com");
// 3. curl_exec() - cURL 세션 실행 (요청 전송)
$result = curl_exec($ch);
// 4. curl_close() - cURL 세션 종료 (리소스 해제)
curl_close($ch);
// 5. curl_error() - 마지막 에러 메시지 반환
$error = curl_error($ch);
// 6. curl_errno() - 마지막 에러 코드 반환 (0이면 성공)
$errno = curl_errno($ch);
// 7. curl_getinfo() - cURL 정보 배열 반환
$info = curl_getinfo($ch);
// $info['http_code'] - HTTP 상태 코드 (200, 404 등)
// $info['total_time'] - 총 소요 시간
// $info['content_type'] - Content-Type
// $info['size_download'] - 다운로드 크기
// 8. curl_version() - cURL 버전 정보 반환
$version = curl_version();
// 9. curl_multi_init() - 비동기 cURL 멀티 핸들 초기화
$mh = curl_multi_init();
// 10. curl_multi_add_handle() - 멀티 핸들에 cURL 핸들 추가
curl_multi_add_handle($mh, $ch1);
curl_multi_add_handle($mh, $ch2);
// 11. curl_multi_exec() - 멀티 핸들 실행 (비동기 처리)
do {
curl_multi_exec($mh, $running);
curl_multi_select($mh);
} while ($running > 0);
// 12. curl_multi_getcontent() - 멀티 핸들의 응답 가져오기
$result = curl_multi_getcontent($ch1);
// 13. curl_multi_close() - 멀티 핸들 종료
curl_multi_close($mh);
CURL_SETOPT 옵션
// === 기본 옵션 ===
curl_setopt($ch, CURLOPT_URL, "http://example.com"); // 요청할 URL
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); // 응답을 변수로 반환 (false면 바로 출력)
curl_setopt($ch, CURLOPT_TIMEOUT, 30); // 요청 타임아웃 (초)
curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 10); // 연결 타임아웃 (초)
// === HTTP 메서드 ===
curl_setopt($ch, CURLOPT_POST, true); // POST 요청
curl_setopt($ch, CURLOPT_POSTFIELDS, "key=value"); // POST 데이터 (문자열)
curl_setopt($ch, CURLOPT_POSTFIELDS, ['key' => 'value']); // POST 데이터 (배열)
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, "PUT"); // PUT, DELETE 등 커스텀 메서드
curl_setopt($ch, CURLOPT_HTTPGET, true); // GET 요청 (기본값)
// === 헤더 ===
curl_setopt($ch, CURLOPT_HTTPHEADER, [ // HTTP 헤더 배열
"Content-Type: application/json",
"Authorization: Bearer token",
"Accept: application/json"
]);
curl_setopt($ch, CURLOPT_USERAGENT, "MyApp/1.0"); // User-Agent 설정
curl_setopt($ch, CURLOPT_REFERER, "http://example.com"); // Referer 설정
curl_setopt($ch, CURLOPT_COOKIE, "name=value"); // 쿠키 문자열
curl_setopt($ch, CURLOPT_COOKIEFILE, "/path/to/cookie.txt"); // 쿠키 파일 읽기
curl_setopt($ch, CURLOPT_COOKIEJAR, "/path/to/cookie.txt"); // 쿠키 파일 저장
// === HTTPS/SSL ===
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true); // SSL 인증서 검증 (기본값)
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 2); // 호스트 검증 (2=엄격)
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); // SSL 검증 끄기 (보안 위험)
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 0); // 호스트 검증 끄기 (보안 위험)
curl_setopt($ch, CURLOPT_CAINFO, "/path/to/cacert.pem"); // CA 인증서 파일 경로
// === 리다이렉트 ===
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true); // 리다이렉트 자동 따라가기
curl_setopt($ch, CURLOPT_MAXREDIRS, 5); // 최대 리다이렉트 횟수
// === 파일 처리 ===
curl_setopt($ch, CURLOPT_FILE, $fp); // 응답을 파일 핸들로 출력
curl_setopt($ch, CURLOPT_INFILE, $fp); // 업로드할 파일 핸들
curl_setopt($ch, CURLOPT_INFILESIZE, filesize("file.jpg")); // 업로드 파일 크기
curl_setopt($ch, CURLOPT_UPLOAD, true); // 파일 업로드 모드
// === 인증 ===
curl_setopt($ch, CURLOPT_USERPWD, "username:password"); // HTTP 기본 인증
curl_setopt($ch, CURLOPT_HTTPAUTH, CURLAUTH_BASIC); // HTTP 인증 방식
curl_setopt($ch, CURLOPT_HTTPAUTH, CURLAUTH_DIGEST); // Digest 인증
// === 프록시 ===
curl_setopt($ch, CURLOPT_PROXY, "proxy.example.com:8080"); // 프록시 서버
curl_setopt($ch, CURLOPT_PROXYUSERPWD, "user:pass"); // 프록시 인증
// === 기타 ===
curl_setopt($ch, CURLOPT_HEADER, true); // 응답 헤더 포함
curl_setopt($ch, CURLOPT_NOBODY, true); // HEAD 요청 (바디 제외)
curl_setopt($ch, CURLOPT_VERBOSE, true); // 상세 정보 출력
curl_setopt($ch, CURLOPT_ENCODING, ""); // Accept-Encoding 자동 처리
curl_setopt($ch, CURLOPT_AUTOREFERER, true); // 리다이렉트 시 Referer 자동 설정
json 데이터 구성해서 보내기
대부분의 페이지가 PHP, 그것도 그누보드5로 이루어져 있기 때문에 게시판에서 글을 쓰게 된다. bbs/write_update.php가 그 업데이트 관련인데, 여기에 fsockopen이 들어가 있는, 알림을 보내는 파일을 삽입하면 된다. 일단 보낼 데이터는 다음처럼 구성했다. jsonencode를 해야하니 array 형태로 해야한다. PHP 5.4 미만을 고려하여 array는 구식 문법으로 사용했다.
$post_data = array(
'api_key' => $api_key, //API 키
'sitename' => $site_name, // 검증용 닉네임
'board_id' => $bo_table, // 게시판 아이디, write_update.php에 변수가 지정되어 있다.
'title' => $wr_subject, //게시글 제목, write_update.php에 변수가 지정되어 있다.
'url' => $post_url, //누르면 바로 해당 포스트로 이동 가능하도록 url을 저장했다.
'wr_id' => $wr_id //게시글 아이디(번호)인데 후술한다.
);
이제 이것을 json_encode를 사용해서 보낼 준비를 하면 된다. 물론 이것만 있다고 보낼 수는 없다. 이제 통신을 하는 법을 적어보자.
cURL의 경우
$ch = curl_init($url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, $json_data);
curl_setopt($ch, CURLOPT_HTTPHEADER, array(
'Content-Type: application/json',
'Content-Length: '.strlen($json_data)
));
curl_setopt($ch, CURLOPT_TIMEOUT, $timeout);
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
$response = curl_exec($ch);
$curl_error = curl_error($ch);
$http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
fsockopen의 경우 다음처럼 구성했다.
$fp = @fsockopen($protocol_prefix . $host, $port, $error_number, $error_message, 1);
if ($fp) {
@file_put_contents($log_file, date('Y-m-d H:i:s')." | 연결 성공\n", FILE_APPEND);
// 스트림 타임아웃 설정 (1초)
@stream_set_timeout($fp, 1);
// HTTP POST 요청 헤더 작성
$http_request = "POST $path HTTP/1.1\r\n";
$http_request .= "Host: $host\r\n";
$http_request .= "Content-Type: application/json\r\n";
$http_request .= "Content-Length: " . strlen($post_data) . "\r\n";
$http_request .= "Connection: Close\r\n";
$http_request .= "\r\n";
$http_request .= $post_data;
// 데이터 전송 (응답 대기 안 함)
@fwrite($fp, $http_request);
@fclose($fp);
} else {
}
타임아웃 설정
홈페이지 간 통신에서 상대 홈페이지의 응답이 없다면 바로 종료해야 한다. 그렇지 않으면 하염없이 기다리고 있기 때문이다. 데이트에서 바람 맞는다고 그대로 계속 서 있을 수는 없으니까.
이제 보내는 것은 종료되었다. 허브에서 받게 처리하면 된다.
4. 포스트 알람을 받기
포스트 데이터 저장하기
일단 포스트 일람을 볼 수 있는 곳이 필요하기 때문에, 포스트 데이터를 저장해야할 공간이 필요하다. 이것은 json 파일로 처리하기로 했다. 제목, 닉네임, 작성 시간, url만 받아서 저장하면 된다.
Receive하기
일단은 API 키 검증이 필요하다. 다음 function을 작성했다.
function verify_api_key($api_key) {
$keys = read_json_file($key_file);
foreach ($keys as &$key_data) { // 참조로 원본 배열 수정을 위해 사용
if ($key_data['key'] === $api_key) {
$key_data['last_used'] = time();
write_json_file($key_file, $keys);
$result = $key_data; // 미리 저장
unset($key_data); // 참조 해제
return $result;
}
}
unset($key_data); // foreach 완료 후 참조 해제
return false;
}
그리고 알림을 보내는 쪽에서 보냈던 POST 데이터를 받을 처리를 해야한다.
$input = file_get_contents('php://input');
$post_data = json_decode($input, true);
여기서 형식들을 판단해서 맞지 않으면(API키 검증, json 형식과 그 데이터 검증 등) exit 한다.
받은 후부터는 보내준 json을 받아, json파일을 연 뒤 그것을 저장하면 된다. (write_json_file)
사용할 수 있는 옵션들
아무리 json 파일이어도 포스트 알람이 너무 쌓이면 곤란하다. 따라서 몇 가지 제한을 걸어 파일을 관리해주기로 했다.
개수 제한
if (count($posts) > 1000) { //1000개 이상일 경우 포스트를 삭제한다.
$posts = array_slice($posts, 0, 1000);
}
기간 제한
//30일 이상이라면 삭제한다.
$month_ago = $current_time - (30 * 24 * 60 * 60);
$posts = array_filter($posts, function($post) use ($month_ago) {
return $post['timestamp'] > $month_ago;
});
최신순으로 정렬
usort($posts, function($a, $b) {
return $b['timestamp'] - $a['timestamp'];
});
이렇게 한 뒤 저장해주면 된다.
목록에 대한 구현은 json 데이터를 불러 정리하는 형태이기 때문에 생략한다.
5. 디버깅
새 글이 올라오면 이전 포스트 알림에 덮어써지는 문제
기능이 만들어지고 난 뒤, 하루 정도 지나자 문제를 깨닫는다. 바로 새 글들의 갱신이 좀 이상한 것. 바로 새 글을 작성하면 이전 포스트 알림에 그대로 덮어써지는 문제였다.
대체 왜 이렇게 되었나? 하고 생각하고 있던 찰나에, 이 기능의 본래 포스트 구분법이 url이었다는 걸 떠올렸다.
본래는 수정 처리도 알림으로 가곤 했었다. 그러나, 수정까지 하나하나 새 포스트 알림으로 올라오는 것은 매우 번거로운 일이다. 이에 따라 포스트를 구분하여 수정 처리는 새 알림 처리로 안 하게 처리했는데… 문제는 url 처리를 한 차례 수정했던 것. 바로 게시판 목록으로 가도록 url을 처리하는 바람에, url이 게시판마다 동일하게 처리 되어 이전 포스트가 수정되는 형태가 된 것이다.
…그렇다면 새로운 구분법이 필요했다. 그렇게 결정한 것이 바로 위에 후술한다는 $wr_id다.
$wr_id 데이터를 보내어서 해당 wr_id와의 비교를 한 뒤 수정 처리인지, 새 글인지 검증 처리하면 된다. 만약에 wr_id가 존재하지 않는다면 url을 기반으로 검증하기로 했다.
$wr_id = isset($post_data['wr_id']) ? $post_data['wr_id'] : '';
if (!empty($wr_id)) {
$unique_id = $post_data['site'].'_'.$post_data['board_id'].'_'.$wr_id.'_'.$current_time;
} else {
// wr_id가 없으면 URL 기반 (RSS 등)
$unique_id = $current_time.'_'.substr(md5($post_data['url']), 0, 8);
}
이렇게 처리한 뒤 문제는 깔끔히 해결되었다.
'개발일지 > 작업일지' 카테고리의 다른 글
| [개인 의뢰] 크로퍼를 사용한 이미지 크롭 · 미리보기 연동 (0) | 2026.02.10 |
|---|---|
| [개인 의뢰] form을 이용하여 페이지 이동 (0) | 2026.01.08 |
| [개인 의뢰] PHP에서의 json 데이터 활용 (0) | 2026.01.02 |
