meta charset utf-8: 애드센스 크롤러가 한글 이미지 주소를 깨뜨린 404와 php-fpm 꽉 참 문제

HTML 앞부분에서 meta charset utf-8 선언을 찾지 못하고 응답 머리에도 charset이 없으면, 읽는 쪽은 문서의 문자 인코딩을 짐작하거나 기본값으로 읽게 됩니다. Nginx와 Apache 구성의 워드프레스 서버에서 애드센스 크롤러가 깨진 한글 이미지 주소로 요청을 몰아 보내 404 처리에 PHP 워커가 모두 찬 일을 로그로 추적한 결과, 캐시본 응답 머리에 charset을 붙이고 없는 정적 파일은 nginx가 바로 404로 처리하게 해 같은 요청이 다시 워커를 채우지 않게 했습니다.
1️⃣ meta charset utf-8: 앞 1,024바이트 안에 있어야 하는 까닭
서버는 문서의 문자 인코딩을 두 곳에서 알릴 수 있습니다. 응답 머리 [Content-Type] 뒤에 붙는 charset과, HTML 안에 적는 meta charset 줄입니다.
HTML 표준은 문자 인코딩 선언이 앞 1,024바이트 안에만 오도록 정하고, 읽는 쪽에도 앞 1,024바이트만 미리 읽어 찾도록 권합니다. 그래서 head 영역 안에 있더라도 1,024바이트 뒤로 밀린 meta charset은 찾지 못하는 읽는 쪽이 생깁니다.
인코딩을 정할 때는 BOM(바이트 순서 표시)을 먼저 보고, 다음이 응답 머리의 charset, 그다음이 앞 1,024바이트 미리 읽기로 찾은 meta charset입니다. 셋 다 없으면 바이트 모양으로 짐작하거나 정해 둔 기본값으로 읽게 되며, HTML 표준이 제안하는 기본값 표에서 대부분의 locale은 windows-1252입니다.
windows-1252로 UTF-8 문서를 읽으면 한글처럼 여러 바이트로 된 글자 하나가 여러 개의 다른 글자로 바뀝니다.
2️⃣ 애드센스 크롤러 Mediapartners-Google이 깨뜨린 한글 이미지 주소
앞 1,024바이트 기준으로 고치기 전 캐시본을 확인한 결과, 응답 머리의 Content-Type에는 charset이 없었고 meta charset 줄은 15,199바이트 자리에 있었습니다. 그래서 애드센스 크롤러 Mediapartners-Google이 받은 캐시본은 두 자리 모두에서 인코딩을 찾을 수 없는 상태였습니다.
curl -sI https://example.com/sample-post/ | grep -i '^content-type'content-type: text/html이 구성에서는 주소 뒤에 쿼리 문자열을 붙이면 캐시본을 건너뛰고 PHP가 바로 그린 응답이 오며, 그 응답에는 charset이 붙어 있었습니다.
curl -sI 'https://example.com/sample-post/?nocache=1' | grep -i '^content-type'content-type: text/html; charset=UTF-8캐시 파일 안에서 meta charset이 시작하는 자리는 [grep -b]로 바이트 단위까지 확인할 수 있습니다.
grep -bo '<meta charset' /var/www/html/wp-content/cache/wp-rocket/example.com/sample-post/index-https.html | head -115199:<meta charset같은 캐시 파일의 img 태그에는 UTF-8 다국어 파일 이름(이 경우 한글)이 퍼센트 인코딩 없이 그대로 들어 있었습니다. 이 HTML을 windows-1252로 읽으면 [대시보드-화면-예시.png]라는 이름이 [대시보드-화면-예시.png] 같은 글자로 바뀌고, 그 글자를 다시 UTF-8로 퍼센트 인코딩해 요청하면 서버에 없는 파일을 찾게 됩니다.
액세스 로그에서 이 크롤러가 받은 그림 404를 뽑으면 깨진 이름의 주소가 이어서 나옵니다.
grep 'Mediapartners-Google' /var/log/nginx/access.log | grep ' 404 ' | grep 'GET /wp-content/uploads/' | cut -d' ' -f1,4,7,9 | head -2203.0.113.3 [03/Nov/2026:20:40:51 /wp-content/uploads/2025/07/%C3%AB%C5%92%E2%82%AC%C3%AC%E2%80%B9%C5%93%C3%AB%C2%B3%C2%B4%C3%AB%E2%80%9C%C5%93-%C3%AD%E2%84%A2%E2%80%9D%C3%AB%C2%A9%C2%B4-%C3%AC%CB%9C%CB%86%C3%AC%E2%80%B9%C5%93-1024x682.png 404
203.0.113.1 [03/Nov/2026:20:40:51 /wp-content/uploads/2025/07/%C3%AD%CB%9C%C2%B8%C3%AC%C5%A0%C2%A4%C3%AD%C5%92%E2%80%A6-%C3%AB%C2%A9%E2%80%9D%C3%AB%E2%80%B0%C2%B4-%C3%AC%CB%9C%CB%86%C3%AC%E2%80%B9%C5%93-300x159.jpg 404로그에 남은 주소는 원래 파일 이름의 UTF-8 바이트를 windows-1252 글자로 읽고 다시 UTF-8로 인코딩한 값과 바이트까지 같았습니다. 같은 꼴로 깨진 그림 주소 요청은 그날 로그에서 이 크롤러가 202건, 스크립트 하나(python-requests)가 6건이었고, 방문자에게서 온 요청은 0건이었습니다.
깨진 이름 [대시보드-화면-예시.png]를 UTF-8 변환기로 되돌리면 UTF-8 바이트를 windows-1252로 읽어 깨진 경우로 판정되고, 원래 이름 [대시보드-화면-예시.png]가 나옵니다.
3️⃣ 없는 이미지 404가 php-fpm을 채운 과정
깨진 주소의 그림은 서버에 없으므로, nginx는 이미지 위치 규칙에서 try_files의 마지막 항목에 따라 요청을 Apache로 넘겼고 워드프레스가 404 화면을 PHP로 그렸습니다. 같은 날 낮 시간대에 기록된 404 587건을 보면, 캐시가 없는 404 화면 하나에 중앙값 1.4초가 소요되었습니다.
깨진 이름의 그림 요청은 20:40부터 3분 동안 200건 남짓 몰렸고, 그중 133건이 404로 끝났습니다. php-fpm 워커는 요청 하나에 하나씩 할당되며, 404 하나에 4~6초가 소요되는 동안 요청이 이어지자 20:40:55에 풀 상한에 닿았습니다.
풀 상한에 닿은 순간은 php-fpm 오류 기록에 php-fpm max_children 경고로 남습니다.
grep 'max_children' /var/log/php-fpm/error.log[03-Nov-2026 20:40:55] WARNING: [pool www] server reached pm.max_children setting (20), consider raising it풀이 찬 뒤 들어온 요청은 빈 워커를 기다렸습니다. 가장 오래 기다린 404는 58초 만에 끝났고, 크롤러가 60초를 기다리다 끊은 요청은 499로, 백엔드 응답을 받지 못한 요청 하나는 504로 기록되었습니다.
같은 시각에 돌던 캐시 미리 채우기와 예약 작업(Action Scheduler), 1분마다 실행되는 wp-cron은 확인 결과 원인에서 제외했습니다. 미리 채우기는 하루 내내 같은 낮은 속도로 돌고 있었고, 예약 작업은 같은 시간에 미리 채우기 작업만 처리하고 있었으며, wp-cron은 명령줄 PHP로 실행되어 php-fpm 워커를 사용하지 않습니다.
4️⃣ nginx charset utf-8과 없는 정적 파일 바로 404
캐시본을 내보내는 nginx 위치에 [charset utf-8;] 한 줄을 더해 응답 머리에 charset이 붙게 했고, 업로드 폴더의 그림과 정적 파일은 디스크에 없으면 워드프레스로 넘기지 않고 nginx가 바로 404를 반환하게 했습니다.
# http 블록: 404 응답에만 Cache-Control 값을 정함
map $status $up404_cc { 404 'no-store'; default ''; }
# 캐시본 HTML을 바로 내보내는 위치
location / {
try_files $cache_file @backend;
charset utf-8;
}
# 업로드 폴더: 없는 그림은 백엔드로 넘기지 않고 바로 404
location ~* ^/wp-content/uploads/.+[.](jpg|jpeg|png)$ {
try_files $uri =404;
add_header Cache-Control $up404_cc always;
}map은 http 블록에 두며, 값이 빈 문자열이면 nginx는 그 머리를 보내지 않으므로 200 응답에는 no-store가 붙지 않습니다. 정규식 위치는 위에서부터 먼저 맞는 것이 적용되므로 기존 이미지 위치보다 앞에 배치했고, css와 js, 글꼴 같은 정적 파일에도 같은 방식의 위치를 하나 더 추가했습니다.
고친 뒤 같은 주소를 다시 요청한 결과, 캐시본 응답 머리에 charset이 붙었습니다.
curl -sI https://example.com/sample-post/ | grep -i '^content-type'content-type: text/html; charset=utf-8curl -sI https://example.com/wp-content/uploads/2025/07/no-such-image.png | grep -i -E '^(HTTP|content-type|cache-control)'# 고치기 전: 워드프레스가 그린 404
HTTP/2 404
content-type: text/html; charset=UTF-8
cache-control: no-cache, must-revalidate, max-age=0, no-store, private
# 고친 뒤: nginx가 바로 반환한 404
HTTP/2 404
content-type: text/html
cache-control: no-store없는 그림의 404는 319,058바이트를 1.01초에 그리던 워드프레스 화면에서, 555바이트를 0.015초에 반환하는 nginx 기본 화면으로 바뀌었습니다. 404에만 [Cache-Control: no-store]를 붙여 CDN이 없는 그림의 404를 저장하지 않게 했습니다. PHP가 이미 charset을 붙여 보낸 응답에는 nginx가 charset을 다시 붙이지 않아, 응답 머리에 한 번만 나갔습니다.
| 확인한 요청 | 고치기 전 | 고친 뒤 |
|---|---|---|
| 없는 그림 | 319,058B, 1.01초(워드프레스 404) | 555B, 0.015초(nginx 404) |
| 깨진 이름의 그림 | 1.05초(워드프레스 404) | 0.014초(nginx 404) |
| 캐시본 HTML 응답 머리 | text/html | text/html; charset=utf-8 |
| PHP가 바로 그린 응답 머리 | text/html; charset=UTF-8 | 같음(charset 한 번) |
| 있는 그림 | 200 | 200 그대로 |
요청이 들어온 뒤에 PHP로 썸네일이나 WebP를 생성하는 이미지 플러그인을 사용하는 사이트에는 이 404 처리가 맞지 않습니다. nginx가 먼저 404를 반환하므로 그림이 생성되지 않습니다. WebP를 업로드할 때 파일로 미리 만들어 두는 구성이라면 그대로 적용할 수 있습니다.
5️⃣ meta charset이 뒤로 밀린 까닭: WP Rocket Delay JS
응답 머리에 charset을 붙인 뒤에도 캐시본 HTML의 meta charset은 15,199바이트 자리에 그대로 남아 있습니다. PHP가 바로 그린 페이지에서는 135바이트 자리에 있던 줄입니다.
WP Rocket의 Delay JS를 켜면 캐시 파일을 만들 때 head 바로 뒤에 lazyload 같은 지연 실행용 스크립트를 넣고, 이어서 meta charset을 다시 head 맨 앞으로 옮기는 단계를 거칩니다. 이 옮기는 단계는 문서 전체에 정규식을 거는 방식이라, 약 1.2MB인 큰 글에서는 PHP의 backtrack 한계를 넘어 실패합니다.
정규식이 실패하면 WP Rocket은 스크립트만 들어가고 meta charset은 옮기지 못한 HTML을 그대로 캐시하게 되며, 그 결과 meta charset이 앞 1,024바이트 밖으로 밀렸습니다. 같은 PHP 설정으로 따로 재현한 결과, backtrack 한계가 기본값 1,000,000일 때와 5,000,000일 때는 실패했고 10,000,000부터 옮기기에 성공했습니다.
이번에는 응답 머리에 charset을 붙이는 쪽으로 해결했고, meta charset을 앞으로 옮기는 고침은 하지 않았습니다. backtrack 한계를 올리면 WP Rocket의 원래 동작은 살아나지만, 같은 PHP에서 도는 모든 정규식의 한계가 10배로 늘어나므로 권하지 않습니다.
🔢 FAQ & 추천 콘텐츠
HTTP 404
HTTP 404 에러(클라이언트 에러) 원인과 해결
HTTP 상태 코드 중 하나인 HTTP 404 에러 페이지는 페이지를 찾을 수 없을 때 발생합니다.
애드센스
워드프레스 ads.txt 설정: 애드센스 경고 해결
ads.txt는 사이트 루트에 두는 텍스트 파일로, 이 도메인의 광고 지면을 판매할 수 있는 공식 판매자가 누구인지 공개하는 목록입니다.
WP Rocket 캐시
WP Rocket 한글 URL: nginx가 캐시 파일을 찾지 못하는 원인과 njs 해결
영문 슬러그 글은 nginx가 캐시 파일을 바로 응답하지만, 한글 슬러그 글은 같은 캐시 파일이 있어도 PHP가 한 번 실행된 뒤에 그 파일을 응답하게 됩니다.
ℹ️ 제휴 안내
본 사이트의 콘텐츠에는 제휴 링크가 포함되어 있습니다. 방문자가 이 링크를 통해 상품 또는 서비스를 구매하면 본 사이트는 판매처로부터 수수료를 지급받습니다. 이 과정에서 구매자가 지불하는 금액(이벤트 할인 시 금액이 내려갑니다↓)은 오르지 않습니다. 게시된 가격·할인·재고 정보는 작성 시점 기준이며 실제와 다를 수 있으므로, 구매 전 판매처에서 최종 확인하시기 바랍니다. 상품 선정과 평가는 자체 기준에 따라 작성되며, 수수료 지급 여부가 소개 순서나 평가 내용에 영향을 주지 않습니다.