서버사이드 측정(요청이 서버에 도착한 시점에 기록하는 방식)의 PHP 사이트 설치에서는 웹서버 접근 기록을 집계 기준으로 둡니다. PHP 코드 기록은 페이지 정보를 보태는 보조 수단입니다. 확인할 설정은 네 가지입니다. 캐시·프록시 구성, PHP 기록 코드의 실행 조건, 기록 항목과 형식, 시험 요청 검증입니다.
단계마다 완료 기준을 충족한 뒤 다음 단계로 넘어갑니다.
측정 지점은 웹서버와 PHP 중 어디에 두나요?
PHP 사이트에서 기록을 남길 수 있는 곳은 두 군데입니다. nginx 같은 웹서버의 접근 기록과 PHP 코드 안의 기록입니다. 두 곳은 잡아내는 요청의 범위가 다릅니다.
| 구분 | 웹서버 접근 기록 | PHP 코드 기록 |
|---|---|---|
| 기록되는 요청 | 웹서버가 받은 모든 요청(정적 파일·robots.txt 포함) | PHP가 실행된 요청만 |
| nginx 캐시가 직접 낸 응답 | 기록됨 | 기록되지 않음 |
| HEAD 요청 | 기록됨 | 출력 뒤에 둔 코드는 실행되지 않을 수 있음 |
| 상태 코드 | 실제로 보낸 값 | 코드 위치에 따라 달라짐 |
| 페이지 식별 정보 | 요청 경로 문자열만 | 글 번호·대표 URL 등 추가 가능 |
PHP 매뉴얼의 $_SERVER 문서는 요청 방식이 HEAD이면 헤더를 보낸 뒤 스크립트가 끝난다고 설명합니다. 출력 버퍼를 쓰지 않으면 첫 출력이 곧 그 시점입니다. 같은 문서는 $_SERVER 값을 웹서버가 만들기 때문에 모든 서버가 같은 항목을 준다는 보장이 없다고 적습니다.
그래서 실무에서는 웹서버 기록을 집계 기준으로 삼는 방법을 권합니다. PHP 기록은 경로만으로는 알 수 없는 페이지 정보를 붙일 때 씁니다. 두 기록을 요청 시각과 경로로 대조하면 어느 쪽에서 요청이 빠졌는지 찾을 수 있습니다.
아래 설정 이름은 nginx와 PHP-FPM(PHP를 웹서버와 분리된 프로세스로 실행하는 방식) 구성을 기준으로 합니다. 다른 웹서버를 쓰면 같은 역할을 하는 설정을 찾아 확인합니다.
단계 1. 캐시와 프록시 뒤에서는 무엇을 먼저 확인하나요?
캐시 응답이 기록에 남는지
nginx의 fastcgi_cache나 proxy_cache가 응답을 내보내면 PHP는 실행되지 않습니다. 이 요청은 PHP 코드 기록에 남지 않습니다. 캐시 적중이 잦은 페이지일수록 PHP 기록만으로 센 수치가 실제보다 작아집니다.
nginx 문서는 $upstream_cache_status 변수에 응답 캐시 접근 결과가 담긴다고 설명합니다. 값은 MISS, BYPASS, EXPIRED, STALE, UPDATING, REVALIDATED, HIT 가운데 하나입니다. 이 값을 접근 기록에 넣으면 PHP 기록에서 빠진 요청의 이유를 설명할 수 있습니다.
PHP 캐시 플러그인만 쓰는 구성에서는 캐시 상태 칸이 비어 있어도 오류가 아닙니다. 다만 플러그인이 웹서버 규칙으로 정적 HTML 파일을 직접 내보내는 방식이면 그 요청은 PHP 기록에서 빠집니다. 두 기록을 비교할 때는 정적 파일을 뺀 페이지 요청끼리 맞춥니다.
CDN(여러 지역에서 콘텐츠를 대신 전달하는 서비스)을 쓰면 원 서버까지 오지 않는 요청도 생깁니다. 이 경우 CDN 쪽 요청 기록을 받을 수 있는지 이용 중인 상품의 제공 범위를 먼저 확인합니다.
기록된 주소가 프록시 주소가 아닌지
로드밸런서나 프록시 뒤에서는 웹서버가 보는 연결 주소가 프록시 주소일 수 있습니다. 그러면 요청이 프록시 주소로 찍혀 봇 신원을 확인할 수 없습니다.
nginx realip 모듈은 신뢰하는 주소에서 온 연결에 한해 요청 헤더의 값으로 클라이언트 주소를 바꿉니다. 신뢰 주소는 set_real_ip_from으로, 읽을 헤더는 real_ip_header로 지정합니다. real_ip_header의 기본값은 X-Real-IP입니다.
real_ip_recursive를 켜면 헤더의 주소 중 신뢰 주소를 건너뛴 마지막 비신뢰 주소를 씁니다. 기본값은 꺼짐입니다. 바뀌기 전 원래 연결 주소는 $realip_remote_addr 변수에 남습니다.
X-Forwarded-For 같은 헤더는 요청을 보내는 쪽이 임의로 넣을 수 있습니다. 실무에서는 신뢰 주소를 실제 프록시 대역으로만 좁힙니다. 바뀐 주소와 원래 연결 주소를 함께 남기면 설정 오류를 나중에 추적할 수 있습니다.
완료 기준: nginx 캐시를 쓰는 경우 접근 기록에 캐시 상태 값이 찍힙니다. 프록시를 거친 요청의 클라이언트 주소가 프록시 대역이 아닙니다.
단계 2. PHP 기록 코드는 어떤 실행 조건을 확인하나요?
PHP 코드로 기록을 남긴다면 요청 처리 흐름의 어느 시점에 쓰는지가 결과를 좌우합니다. 다음 여섯 가지를 차례로 확인합니다.
- 값이 없을 때의 처리: $_SERVER 항목은 서버 구성에 따라 없을 수 있습니다. 항목이 없어도 기록이 멈추지 않고 빈 값으로 남게 합니다.
- 기록 위치: HEAD 요청은 첫 출력 뒤 끝날 수 있습니다. 출력 이후에만 기록하는 코드라면 HEAD 요청은 웹서버 기록으로 보완합니다.
- 종료 함수 등록: PHP 매뉴얼은 register_shutdown_function으로 등록한 함수가 정상 종료·연결 끊김·시간 초과 때 호출된다고 설명합니다. 기본 시간 제한은 30초입니다. 시간 초과로 호출되면 connection_status가 2를 돌려줍니다.
- 응답 지연 방지: fastcgi_finish_request는 PHP-FPM으로 실행할 때만 쓸 수 있는 함수입니다. PHP 매뉴얼은 이 함수가 응답을 모두 보내고 요청을 끝낸 뒤 남은 작업을 이어 가게 한다고 설명합니다. Apache 모듈 방식 등에서는 이 함수가 없으므로 종료 함수로 기록합니다.
- 작업 프로세스 점유: fastcgi_finish_request 뒤에도 PHP-FPM 작업 프로세스는 기록이 끝날 때까지 묶여 있습니다. 실무에서는 기록 작업을 파일 한 줄 쓰기 정도로 짧게 유지합니다. 세션을 쓰는 페이지는 session_write_close로 세션을 먼저 닫습니다.
- 신원 확인 분리: 요청마다 DNS를 조회하면 응답이 느려집니다. 요청 중에는 주소만 기록하고, 신원 확인은 단계 4의 일괄 처리로 넘깁니다.
응답 시간은 접근 기록의 $request_time으로 확인합니다. nginx 문서는 이 값을 요청 처리 시간으로 설명합니다. 단위는 초이고 밀리초까지 표시됩니다.
기록 코드 추가 전후로 같은 페이지의 값을 비교합니다. 캐시 적중 요청은 PHP를 거치지 않으므로 비교에서 뺍니다. fastcgi_finish_request 뒤의 작업은 $request_time에 드러나지 않을 수 있습니다. 그래서 PHP-FPM 상태 화면의 활성 프로세스 수도 함께 확인합니다.
완료 기준: 정상 요청·HEAD 요청·도중에 끊긴 요청이 PHP 기록에 남거나 웹서버 기록으로 대조됩니다. 캐시를 거치지 않은 같은 페이지의 $request_time과 활성 프로세스 수가 기록 코드 추가 전과 같은 범위에 머뭅니다.
단계 3. 기록 항목과 형식은 어떻게 정하나요?
nginx 기본 combined 형식에는 주소, 시각, 요청 줄, 상태 코드, 전송 바이트, 참조 주소, 사용자 에이전트가 들어 있습니다. 캐시 상태·원래 연결 주소·처리 시간은 없으므로 측정용 형식을 따로 만듭니다.
| 항목 | 웹서버 기록 값 | PHP 기록 값 | 쓰임 |
|---|---|---|---|
| 요청 시각 | $time_iso8601 | REQUEST_TIME_FLOAT(마이크로초 단위) | 두 기록 대조 |
| 클라이언트 주소 | $remote_addr(realip 적용 뒤) | REMOTE_ADDR | 봇 신원 확인 |
| 원래 연결 주소 | $realip_remote_addr | 해당 없음 | 프록시 설정 점검 |
| 요청 방식·경로 | $request_method, $request_uri | REQUEST_METHOD, REQUEST_URI | 페이지 단위 집계 |
| 상태 코드 | $status | 웹서버 값 사용 | 오류·리다이렉트 구분 |
| 사용자 에이전트 | $http_user_agent | HTTP_USER_AGENT | 봇 분류 |
| 캐시 상태 | $upstream_cache_status | 해당 없음 | PHP 기록 누락 설명 |
| 처리 시간 | $request_time | 해당 없음 | 기록 코드의 응답 지연 점검 |
| 페이지 식별 | 해당 없음 | 글 번호·대표 URL | 주소 변형 묶기 |
형식은 한 요청당 JSON 한 줄로 정하는 방법을 권합니다. nginx 문서는 log_format에 escape=json을 주면 JSON 문자열에 쓸 수 없는 문자를 이스케이프한다고 설명합니다. 이 매개변수는 1.11.8 버전에 추가되었습니다.
사용자 에이전트처럼 요청 쪽이 정하는 값에 줄바꿈 문자가 섞여도 기록 한 줄이 둘로 갈라지지 않습니다. 표의 항목을 그대로 옮긴 설정은 다음 두 줄입니다.
log_format geo escape=json '{"time":"$time_iso8601","addr":"$remote_addr","real_addr":"$realip_remote_addr","method":"$request_method","uri":"$request_uri","status":$status,"ua":"$http_user_agent","cache":"$upstream_cache_status","rt":$request_time}';
access_log /var/log/nginx/geo.log geo;
이 설정으로 남는 가상의 기록 한 줄은 다음과 같습니다. 값은 모두 설명을 위한 예입니다.
{"time":"2026-10-20T09:00:00+09:00","addr":"203.0.113.10","real_addr":"10.0.0.5","method":"GET","uri":"/guide/setup","status":200,"ua":"Mozilla/5.0 (compatible; GPTBot/1.4)","cache":"MISS","rt":0.012}
PHP 쪽 기록도 같은 키 이름으로 JSON 한 줄씩 씁니다. 키 이름이 같으면 두 파일을 한 표로 합쳐 대조하기 쉬워집니다.
access_log 설정에서는 두 가지를 더 확인합니다. 첫째, if= 조건이 있으면 조건값이 0이거나 빈 문자열인 요청은 기록되지 않습니다. nginx 문서의 예시처럼 2xx·3xx 응답을 빼는 조건이 걸려 있으면 봇 요청 대부분이 기록에서 빠집니다.
둘째, buffer=나 flush=를 쓰면 기록을 모았다가 한꺼번에 씁니다. 시험 요청을 보낸 직후에는 파일에 바로 나타나지 않을 수 있습니다. 검증할 때 이 지연을 감안합니다.
봇 분류는 전체 문자열이 아니라 봇 이름 토큰으로 합니다. OpenAI 문서는 GPTBot과 OAI-SearchBot의 예시 문자열에 버전 숫자가 바뀔 수 있다고 적습니다. 또 robots.txt를 가져올 때 문자열에 robots.txt 표시를 덧붙일 수 있다고 설명합니다. 전체 문자열이 일치해야 분류하는 규칙이면 이런 요청이 분류에서 빠집니다.
완료 기준: 표의 항목이 요청마다 JSON 한 줄로 남습니다. 조건부 기록 때문에 빠지는 응답 코드가 없습니다.
단계 4. 설치가 끝났는지 어떻게 검증하나요?
설정을 마친 뒤 시험 요청을 보내 기대 결과와 비교합니다. 운영 중인 페이지 하나와 없는 주소 하나를 정합니다. 운영 페이지는 이미 캐시에 저장되어 있을 수 있으므로, 시험 전에 해당 주소의 캐시를 비우거나 캐시를 거치지 않는 시험용 경로를 씁니다.
시험용 사용자 에이전트에는 Googlebot이나 GPTBot처럼 공개 IP 대역 파일이 있는 봇 토큰을 넣습니다. 공개 대역이 없는 봇 이름을 쓰면 신원 확인 결과가 대조 대상 아님으로 나와 검증이 되지 않습니다.
| 시험 요청 | 기대 결과 |
|---|---|
| 캐시를 비운 운영 페이지 첫 요청 | 웹서버·PHP 기록에 각 한 줄, 캐시 상태는 MISS 등 HIT가 아닌 값 |
| 같은 주소 재요청(nginx 캐시 사용 시) | 캐시 대상 응답이고 우회 조건(쿠키·Cache-Control 등)과 최소 사용 횟수 설정에 걸리지 않으면 웹서버 기록에 HIT, PHP 기록 없음 |
| 같은 주소 HEAD 요청 | 웹서버 기록에 HEAD와 상태 코드 |
| 응답 도중 연결을 끊은 요청 | PHP 기록에 종료 함수가 남긴 한 줄 |
| 없는 주소 요청 | 상태 코드 404 기록 |
| robots.txt 요청 | 웹서버 기록에 남고 봇 토큰으로 분류 |
| 외부망에서 프록시를 거친 요청 | 클라이언트 주소는 시험 장비 공인 IP, 원래 연결 주소는 프록시 대역 |
시험 요청은 봇 이름만 흉내 낸 것이고 해당 회사의 IP 대역에서 오지 않습니다. 따라서 다음 신원 확인 처리에서 불일치로 분류되어야 정상입니다. 이 결과로 신원 확인 처리까지 함께 점검할 수 있습니다.
신원 확인 처리는 어떻게 붙이나요?
Google 문서는 두 가지 확인 방법을 안내합니다. 하나는 역방향 DNS 조회입니다. 기록된 IP의 조회 결과가 googlebot.com, google.com, googleusercontent.com 도메인인지 봅니다.
이어서 그 이름을 다시 정방향 조회해 원래 IP와 같은지 확인합니다. 다른 하나는 Google이 크롤러 유형별로 공개한 IP 대역 JSON 파일과 대조하는 방법입니다. 파일은 일반 크롤러, 특수 크롤러, 사용자 요청 가져오기 등으로 나뉘어 있습니다.
OpenAI도 OAI-SearchBot, GPTBot, ChatGPT-User의 IP 대역 JSON 파일을 봇별로 공개합니다. 실무에서는 이 파일들을 하루 한 번 내려받아 전날 기록과 대조하는 방법을 권합니다. 결과는 확인됨·불일치·대조 대상 아님 세 값으로 남깁니다.
완료 기준: 표의 시험 요청이 모두 기대 결과와 같습니다. Googlebot·GPTBot 토큰을 넣은 시험 요청은 신원 확인에서 불일치로 분류됩니다.
정리
PHP 사이트의 서버사이드 측정은 PHP 코드보다 웹서버 기록을 먼저 갖추는 데서 시작합니다. 다음 순서로 진행합니다.
- 접근 기록 형식에 캐시 상태·원래 연결 주소·$request_time을 넣어 escape=json 형식으로 바꾸고, access_log의 if= 조건을 점검합니다.
- 프록시 뒤라면 set_real_ip_from을 실제 프록시 대역으로만 지정하고, 원래 연결 주소를 함께 기록합니다.
- PHP 기록은 종료 함수에 두거나, PHP-FPM 환경이면 fastcgi_finish_request 뒤에 두고 페이지 식별 정보만 보탭니다.
- 캐시를 비운 뒤 Googlebot이나 GPTBot 토큰을 넣은 시험 요청 일곱 가지를 보내 기대 결과와 하나씩 비교합니다.
- 공개 IP 대역 파일과 대조하는 일괄 처리를 설정하고, 첫 결과에서 시험 요청이 불일치로 나오는지 확인합니다.
참고 자료
- Verifying Googlebot and other Google crawlers (Google Search Central, 확인일 2026-10-20)
- Overview of OpenAI Crawlers (OpenAI, 확인일 2026-10-22)
- $_SERVER — Server and execution environment information (PHP Manual, 확인일 2026-10-20)
- Connection handling (PHP Manual, 확인일 2026-10-20)
- fastcgi_finish_request (PHP Manual, 확인일 2026-10-22)
- Module ngx_http_realip_module (nginx 문서, 확인일 2026-10-20)
- Module ngx_http_log_module (nginx 문서, 확인일 2026-10-22)
- Module ngx_http_upstream_module — Embedded Variables (nginx 문서, 확인일 2026-10-20)
