애드센스 때문에 날씨 API를 갈아엎었다
2026.08.06
같은 30도라도 습도 30%와 80%는 완전히 다른 날이다. 그런데 숫자 두 개로는 그 차이가 안 보인다.
그래서 오늘 날씨를 찜질방 방 이름으로 바꿔주는 걸 만들었다. 체감 35도에 습하면 열탕지옥, 건조하면 불가마. 20도대면 소금방이나 황토방. 10도대면 안개방이나 수면실.
만드는 건 하루면 됐다. 그런데 약관 한 줄 때문에 데이터 소스를 통째로 갈아엎었고, 그 뒤로 계산과 배포에서 차례로 걸렸다.
약관에 걸렸다
처음엔 Open-Meteo를 썼다. 키가 없어서 서버 라우트를 만들 필요도 없고, 현재값과 시간대를 한 응답에 준다. 편했다.
그런데 약관을 읽어보니 광고를 게재하는 사이트를 상업적 이용으로 규정하고 무료 등급에서 금지하고 있었다. 이 사이트는 AdSense를 싣는다.
그래서 기상청으로 옮겼다. 그리고 거기서부터 일이 시작됐다.
기상청은 체감온도를 안 준다
단기예보는 기온·습도·풍속만 준다. 이 페이지의 방 판정이 체감온도에 걸려 있으니 직접 계산해야 했다.
기상청이 쓰는 식을 그대로 옮겼다. 여름은 습구온도를 거치는 개정식(2022-06-02), 겨울은 바람 냉각식이다.
arctan을 도(°)로 넣었다
습구온도는 Stull(2011) 추정식을 쓴다. 여기서 바로 걸렸다.
export function wetBulb(tempC: number, humidity: number): number {
return (
tempC * Math.atan(0.151977 * Math.sqrt(humidity + 8.313659)) +
...Math.atan은 라디안을 돌려준다. 이걸 도(°)로 바꿔서 넣었더니 첫 항이 기온의 57배가 됐다. 수천 단위 값이 나왔다.
잡아준 건 테스트 한 줄이었다.
습도 100%면 습구온도가 기온과 같다
물리적으로 당연한 성질인데, 그게 유일하게 값을 검증할 수 있는 지점이었다. 지금은 20°C·50%에서 13.70이 나오고 문헌값과 맞는다.
반올림을 두 번 했다
화면에는 정수로 보여주는데 방 판정은 원래 값으로 하고 있었다.
서울이 34.97°일 때 화면에는 **「35°」**라고 써 놓고, 판정은 34.97로 해서 35° 미만이니까 건식사우나로 갔다. 판정표에는 35°부터 불가마라고 적혀 있는데도.
보여주는 수와 판정하는 수를 같게 만들어서 고쳤다. 실제 호출로 확인했다 — 서울 35°/54% 불가마, 부산 32°/56% 건식사우나, 제주 32°/70% 습식사우나.
경계에 절벽이 있었다
이게 제일 오래 걸렸다.
기온 10.0도에서 체감 7.6도, 10.2도에서 10.2도가 나왔다. 기온이 0.2도 올랐는데 체감이 2.6도 뛰었다. 그리고 그 0.2도 차이로 얼음방과 수면실이 갈렸다.
원인은 기상청이 여름 식과 겨울 식만 정해 두고 그 사이를 비워 뒀다는 것이었다. 나는 그 구간에 기온을 그대로 썼고, 그러니 겨울 식이 끝나는 지점에서 값이 계단처럼 끊겼다.
10~20도를 두 식의 이음매로 삼아 선형 보간했다.
const towardWarm = (tempC - COLD_EDGE) / (WARM_EDGE - COLD_EDGE);
return (
coldSide(tempC, windMs) * (1 - towardWarm) + summerFeelsLike(tempC, humidity) * towardWarm
);양 끝에서 각각의 식과 정확히 만나므로 어디에서도 끊기지 않는다.
풍속 1.3m/s 문턱도 같은 종류의 절벽이었다. 냉각식은 바람이 약할 때 기온보다 높은 값을 내서, 기상청은 그 아래에서 식을 안 쓴다. 나는 문턱 대신 기온으로 잘라냈다.
function coldSide(tempC: number, windMs: number): number {
return Math.min(tempC, winterFeelsLike(tempC, windMs));
}"바람은 시원하게만 만든다"는 목적은 같고, 값은 끊기지 않는다.
그리고 기온과 풍속을 훑으며 값이 튀는지 보는 검사를 넣었다. 처음부터 있었어야 했다. 경계 절벽은 특정 입력을 넣어보기 전까지 안 보이는데, 훑는 테스트는 그걸 자동으로 찾는다.
배포판만 「탈의실」이 떴다
로컬에서는 멀쩡한데 배포판만 계속 기본값이 나왔다.
@cloudflare/next-on-pages에서 대시보드에 넣은 값은 process.env가 아니라 요청 컨텍스트에 실려 온다. process.env가 자동으로 채워지는 건 compatibility date 2025-04-01 이상인데, 이 프로젝트의 wrangler.toml은 빌드가 아예 무시하고 있었다 (빌드 로그: does not appear to be valid ... Skipping file).
로컬은 next dev가 .env.local을 process.env로 읽어줘서 통과했다. edge 런타임 때와 똑같은 종류의 함정이다 — 로컬 빌드로는 절대 안 잡힌다.
여기서 또 하나 막혔다. 정석대로면 getRequestContext()를 쓰면 된다. 그런데 그 패키지는 peer로 next@<=15.5.2를 요구하고 이 프로젝트는 next 16이다. 의존성으로 넣으면 npm ci가 실패해서 배포가 통째로 죽는다. 실제로 넣어보고 확인했다.
그래서 의존성을 늘리지 않고, 그 패키지가 읽는 심볼을 직접 봤다.
/** next-on-pages 라우터가 요청마다 여기에 { env, cf, ctx }를 꽂는다 */
const REQUEST_CONTEXT = Symbol.for("__cloudflare-request-context__");그런데도 키를 못 찾았다
컨텍스트는 읽히고, 변수도 9개가 묶여 있고, "kma"가 든 이름도 있는데 KMA_SERVICE_KEY로는 안 잡혔다.
이름 끝의 공백이나 대소문자 차이였다. 대시보드에서는 똑같아 보여서 눈으로는 못 찾는다.
환경변수 이름에서 공백과 대소문자를 구분해서 얻을 게 없다. 정확히 맞는 쪽을 먼저 쓰고, 없으면 다듬어서 한 번 더 찾게 했다. 이름이 아예 다르면 여전히 못 찾는다 — 구제이지 추측이 아니다.
빈 문자열도 없는 것으로 본다. 대시보드에서 값을 지우면 키가 사라지는 게 아니라 빈 문자열로 남는데, 그대로 들고 가면 기상청이 인증 오류를 돌려줘서 원인이 한 겹 멀어진다.
진단을 응답에 담을 때도 선을 뒀다. 비슷한 이름만 추려서 넣고 다른 비밀들의 이름은 내보내지 않는다. 따옴표를 씌워야 앞뒤 공백이 보인다.
edge 런타임을 빠뜨렸다
새 API 라우트에 runtime = "edge"를 안 붙였더니 Cloudflare 빌드가 깨졌다. next-on-pages는 정적이 아닌 라우트가 전부 edge이기를 요구하고, 하나라도 빠지면 느려지는 게 아니라 배포 자체가 실패한다.
로컬 next build는 이걸 못 잡는다. 확인은 Cloudflare가 실제로 돌리는 pages:build로 해야 한다.
사진이 온도를 거꾸로 말했다

방 이름만으로는 「습식사우나」가 얼마나 습한지 안 와닿는다. 그래서 아홉 개 방을 각각 사진으로 만들어 화면 끝까지 깔았다. 김이 찬 타일, 불이 든 가마, 성에가 낀 벽을 보면 온도를 읽기 전에 몸이 먼저 안다.
그런데 습식사우나 사진이 청록색이었다. 28~35도짜리 더운 방인데 화면에서 제일 시원해 보였다. 색이 온도 순서를 거꾸로 말하고 있었다. 따뜻한 톤으로 다시 뽑았다.
여기서 캐시에 걸렸다. 파일 이름을 그대로 두고 내용만 바꿨더니 주소가 같아서 브라우저가 옛 청록 사진을 계속 들고 있었다 (정적 파일 캐시 4시간). 이름을 바꿔 주소를 다르게 만들었다.
그러면서 검사를 하나 더 넣었다. 방 사진 파일이 실제로 있는지 보는 것이다. 경로가 문자열이라 파일 이름만 바꾸고 rooms.ts를 안 고치면 배경이 통째로 비는데, 타입 검사도 린트도 아무 말을 하지 않는다.
사진은 원본 PNG로 83MB였다. 1080px 폭 WebP로 줄여 아홉 장 합쳐 1.4MB.
습도 60%는 한국 여름에 너무 높았다
습식/건식을 가르는 기준을 처음엔 60%로 잡았다. 그런데 습도 54%의 한여름 서울이 「건식사우나」로 갔다. 그건 서울을 건조하다고 부르는 셈이었다.
55%로 내렸다.
남은 것
시작은 약관 한 줄이었다. Open-Meteo를 못 쓰게 되면서 기상청으로 옮겼고, 기상청이 체감온도를 안 줘서 직접 계산하게 됐고, 직접 계산하니 단위·반올림·경계에 차례로 걸렸다.
돌아보면 잡아준 건 전부 물리적으로 당연한 성질을 적어둔 테스트였다. 습도 100%면 습구온도는 기온과 같다. 기온이 조금 오르면 체감도 조금 오른다. 바람은 시원하게만 만든다. 값을 하나씩 찍어보는 테스트로는 이런 걸 못 잡는다.
그리고 배포 쪽에서 걸린 것들은 로컬에서 재현이 아예 안 되는 종류였다. 환경변수가 어디 실려 오는지, edge 런타임이 붙었는지, 이름 끝에 공백이 있는지 — 전부 pages:build나 프로덕션 응답을 봐야만 알 수 있었다.
오늘 서울은 수면실이다. 오늘의 찜질방
같이 읽으면 좋은 글
애드센스 심사를 올리기 전에 내 사이트에서 찾아낸 것들
글 45편에 동작하지 않는 광고 태그가 실려 나가고 있었다. 애드센스 심사를 준비하며 실제로 발견한 문제 네 가지와, 각각을 어떻게 고쳤는지 정리했다. 광고 단위 ID를 승인 전에 만들 수 없다는 것부터 얇은 페이지를 다루는 방법까지.
애드센스BlogSEO2026.08.03개발자 사이드 프로젝트 수익화: 애드센스부터 유료 SaaS까지 단계별 정리
사이드 프로젝트로 돈을 버는 5가지 길을 단계별로 정리한다. 애드센스, 유료 멤버십, 스폰서십, 디지털 제품, 유료 SaaS. 각 단계의 기대 수익과 진입 장벽을 솔직하게 비교한다.
수익화사이드프로젝트애드센스2026.04.24Cloudflare Pages 배포 시 빌드 캐시 이슈와 해결기
Cloudflare Pages 빌드가 매번 처음부터 도는 문제, 캐시가 적용 안 되는 이유, _next 캐싱 충돌, 무효화 시점까지 — 실제로 부딪힌 빌드 캐시 이슈와 해결법을 정리한다.
Cloudflare배포Next.js2026.04.20