게임 사이트에 왜 날씨가 있는지 물어보는 사람이 있었다.
솔직히 처음엔 나도 이유를 정확히 설명하기 어려웠다. 그냥 만들고 싶었다. 밖이 비 오면 "오늘은 테트리스가 어울리는 날이에요" 한 줄이 떠 있으면 좋겠다 정도의 감정이었다. 게임을 권하는 작은 핑계.
그 한 줄을 띄우려고 결국 한국 지도까지 그리게 됐다.
시작은 작은 위젯이었다
처음 커밋 (92eea6e feat: 오늘의 날씨 서비스 추가) 은 정말 단순했다. 위경도 받아서 Open-Meteo API 던지고, 온도와 날씨 코드 받아서 이모지 하나 띄우는 거.
const WEATHER_INFO: Record<number, { label: string; emoji: string }> = {
0: { label: '맑음', emoji: '☀️' },
3: { label: '흐림', emoji: '☁️' },
61: { label: '가는 비', emoji: '🌧️' },
// ...
};
WMO 날씨 코드 표 보고 한국어 라벨 + 이모지 매핑한 게 끝. 30분이면 됐다.
그러고 며칠 만에 30개 가까운 커밋이 쌓였다.
도시 검색이 한 줄이 아니더라
"서울" 입력하면 서울 날씨가 나와야 한다. 당연한 거 같지만 의외로 어려웠다.
Open-Meteo의 지오코딩 API는 영문/현지어 도시명은 잘 찾는데, 한글 "서울"을 못 찾는 경우가 있었다. 한글로 검색 안 되는 작은 도시도 많았다. 그래서 최후 폴백을 3단으로 깔았다.
// 1. 한글 도시명 직접 매핑 (자주 검색되는 도시는 좌표 하드코딩)
// 2. Nominatim (OpenStreetMap) — 한국어 + 전 세계 지명, 읍/면/리까지
// 3. Open-Meteo 지오코딩 — 영문 도시명 최후 수단
서울 같은 메인 도시는 하드코딩, 이천 같은 작은 도시는 OpenStreetMap, Tokyo나 New York은 Open-Meteo. 셋 다 실패하면 404. 처음엔 1번만 있었는데, "왜 우리 동네 안 나와?" 하는 본인 입에서 나오는 한 마디로 폴백이 한 단계씩 늘어났다.
한국은 기상청 데이터가 더 정확하다
Open-Meteo도 한국 기상청(KMA) 모델을 일부 쓰지만, 한국에 있을 땐 기상청 공공 API가 체감상 가장 정확하다. 특히 체감 온도가 그렇다.
그래서 좌표가 한국 영역이면 KMA를 먼저 부르고, 안 되면 Open-Meteo로 폴백.
async function getWeatherData(lat: number, lon: number, cityName: string) {
if (isKorea(lat, lon)) {
const kma = await buildKMAResponse(lat, lon, cityName);
if (kma) {
// KMA 단기예보는 최대 3일 → Open-Meteo 7일로 보완
if (kma.daily.length < 7) {
const om = await buildResponse(lat, lon, cityName);
if (om) kma.daily = om.daily;
}
return kma;
}
}
return buildResponse(lat, lon, cityName);
}
근데 KMA 단기예보는 3일까지밖에 안 준다. 주간 예보를 7일로 보여주고 싶었는데. 그래서 1~3일은 KMA, 4~7일은 Open-Meteo로 채우는 식으로 섞었다. 사용자는 모르지만 안에서는 두 API가 같이 돌아간다.
한국 지도가 진짜 일이었다
날씨 정보만 잘 보여줘도 됐는데, 어느 날 갑자기 "지도가 있으면 좋겠다" 는 생각이 들었다. 도시 카드 18개 나열보다 지도 위에 온도가 떠 있는 게 훨씬 직관적일 것 같았다.
처음엔 SVG로 직접 그릴까 하다가 (이건 빨리 포기), react-simple-maps와 한국 행정구역 GeoJSON 데이터를 붙였다.
<ComposableMap
projection="geoMercator"
projectionConfig={{ scale: 4400, center: [128.0, 35.9] }}
width={490}
height={560}
>
scale이랑 center 값 맞추는 게 고역이었다. 너무 작으면 본토가 안 보이고, 너무 크면 제주도가 화면 밖으로 나가고. 결국 4400 / [128.0, 35.9] 에서 안착.
다크 모드에서 바다와 육지 색을 분리하는 데도 한참 걸렸다. 처음엔 다 비슷한 회색이라 지도인지 도형인지 분간이 안 됐다.
울릉도·독도는 거짓말이 필요했다
이게 가장 재밌었다.
울릉도(37.4889, 130.8702)와 독도(37.2425, 131.8644)는 본토에서 한참 떨어져 있다. 실제 좌표로 마커를 찍으면 둘이 지도 오른쪽 끝, 거의 화면 밖에 박힌다. 게다가 둘이 너무 가까이 있어서 마커가 겹친다.
근데 데이터는 실제 좌표여야 한다. KMA한테 "독도 날씨 줘" 라고 할 때는 진짜 독도 좌표를 넘겨야 진짜 독도 기온이 오니까.
그래서 데이터용 좌표와 시각용 좌표를 분리했다.
{ id: 'ulleungdo', name: '울릉',
lat: 37.4889, lon: 130.8702, vizLon: 130.05 },
{ id: 'dokdo', name: '독도',
lat: 37.2425, lon: 131.8644, vizLon: 130.55, vizLat: 36.85 },
lat/lon은 API 호출용, vizLat/vizLon은 지도 표시용. 화면에서는 본토에서 살짝 떨어진 위치에 보여서 사용자가 마커를 누를 수 있고, 실제 데이터는 진짜 그 섬의 기온이 뜬다.
지도에서 한 사용자한테 작은 거짓말을 했지만, 데이터에서는 정직했다. 둘을 분리할 수 있는 게 코드의 좋은 점이라고 생각한다.
자잘한 보완들 (이게 또 며칠)
- 야간 아이콘: 밤 8시 ~ 새벽 6시에는 ☀️ 대신 🌙. 시간별 막대마다 그 시간을 기준으로 분기.
- 소수점 기온:
22°보다는22.4°가 정밀한 느낌이라서. - 시간별 48시간 확장: 처음엔 24시간이었는데, 모레 비 오면 미리 알고 싶어서 48시간으로.
- 주간 예보 7일: 처음엔 5일이었는데 일주일 단위 계획이 더 자연스러워서.
- 온도 막대: 주간 예보에서 최저~최고를 그라디언트 막대로 시각화. 7일 동안 더운 날·추운 날이 한눈에 보임.
이런 거 하나하나가 사실 다 commit 한 개씩이다. feat: 시간별 날씨 48시간으로 확장 같은 게 그것.
결국 핵심은 한 줄
이 모든 게 결국 화면 가운데 작은 글씨 한 줄을 위한 거다.
function getComment(code: number, temp: number): string {
if (code >= 95) return '뇌우가 치네요. 안전하게 집에서 게임 한 판?';
if ([80,81,82,61,63,65].includes(code)) return '비 오는 날엔 테트리스가 최고죠.';
if ([45,48].includes(code)) return '안개 낀 날엔 미스틱 로또 운세가 좋을지도요.';
if (temp >= 30) return '폭염이네요. 시원한 실내에서 게임 한 판!';
// ...
}
날씨가 게임을 추천한다. 비 오는 날 테트리스, 안개 낀 날 미스틱 로또, 폭염엔 그냥 시원한 실내. 데이터 분석도 아니고 그냥 if-else 몇 줄인데, 이게 사이트에 날씨를 넣은 진짜 이유다.
게임 사이트 안에서 "오늘 이게 어울리는 것 같아" 라고 한마디 건네는 동네 친구 같은 존재. 정확한 추천이 아니라 가벼운 권유. 그게 하고 싶었다.
만들면서 배운 것
이 프로젝트로 알게 된 게 몇 가지 있다.
- 외부 API는 한 개로 안 끝난다. 한국 데이터는 KMA, 해외는 Open-Meteo, 지오코딩은 Nominatim + Open-Meteo + 하드코딩. 사용자가 보는 한 화면 뒤에 보통 3~4개의 API가 돈다.
- 지도 그리기는 좌표계 싸움이다. 실제 좌표와 시각 좌표를 분리해야 할 때가 있다. 데이터의 정확성과 화면의 가독성이 서로 다른 문제일 때.
- 자잘한 보완이 시간을 다 먹는다. 처음 30분에 만든 위젯이 며칠짜리 프로젝트가 된 건 자잘한 폴리시 때문이었다. 그래도 그것 없으면 안 쓰게 된다.