10월 마지막 주 어느 날, 서울의 저녁 6시 예보는 10℃ 였다. 국물 요리를 추천해야 할까?

7월이라면 고민할 것도 없는 숫자다. 하지만 전날 아침 최저가 10℃ 였던 서울이 그날 아침에는 2℃ 로 예보돼 있었다. 하루 사이에 아침 기온이 8℃ 떨어지는 날이다. 숫자로는 10℃ 하나지만, 그날 저녁 식탁에서 느끼는 추위는 같은 10℃ 의 봄날과 전혀 다르다.

식단서랍의 메뉴 추천에 날씨를 붙이면서 가장 오래 붙잡고 있던 것이 이 판단이었다. 1편에서 기상청 단기예보를 받는 법을, 2편에서 격자와 응답 파싱을 정리했다. 이번 편은 그 데이터로 「오늘 저녁은 국물이 당기는 날」을 고르는 이야기다.

요청 때는 기상청을 부르지 않는다

먼저 데이터를 언제 받을지부터 정했다. 메뉴 추천은 사용자가 검색창에 「오늘 저녁 뭐 먹지」를 칠 때 돌아간다. 그 순간 기상청을 부르면 응답이 기상청 속도에 묶이고, 기상청이 느리거나 오류를 내면 추천도 같이 느려진다.

그래서 서버가 시·도 16곳의 날씨를 백그라운드에서 받아 메모리에 들고 있고, 추천은 받아 둔 값만 읽는다. ASP.NET Core 의 BackgroundService 로 만들었다.

protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
    while (!stoppingToken.IsCancellationRequested)
    {
        try { await _weather.RefreshAllAsync(stoppingToken); }
        catch (Exception ex) when (ex is not OperationCanceledException)
        {
            _logger.LogWarning(ex, "[Weather] 날씨 갱신 중 오류");
        }

        // 다음 갱신 — 초단기예보 발표(매시 30분, 45분 이후 조회)를 지난 50분에 맞춘다
        var kst = DateTime.UtcNow.AddHours(9);
        var next = kst.Date.AddHours(kst.Hour).AddMinutes(50);
        while (next <= kst) next = next.AddMinutes(60);
        await Task.Delay(next - kst, stoppingToken);
    }
}

앱이 켜지면 바로 한 번 받고, 그 뒤로는 매시 50분에 받는다. 정각마다 도는 게 아니라 50분인 이유는 초단기예보 때문이다. 초단기예보는 매시 30분에 발표되고 45분쯤부터 받을 수 있다. 50분에 받으면 늘 방금 나온 예보를 쥐게 된다.

받아 둔 날씨에는 유효기간을 뒀다. 기상청이 몇 시간 동안 응답하지 않으면 메모리에 남은 값이 점점 낡는다. 세 시간이 지난 값은 없는 것으로 치고, 그때는 날씨 없이 추천한다. 낡은 「비 소식」으로 엉뚱한 메뉴를 고르느니 날씨를 빼는 편이 낫다.

호출 수도 이 구조에서 정해진다. 초단기예보는 16곳 × 하루 24번으로 하루 384번 안팎이고, 단기예보는 날짜가 바뀐 뒤 지역마다 한 번, 하루 16번이다. 사용자가 몇 명이든 이 숫자는 변하지 않는다.

두 예보를 겹치기

기상청 단기예보 서비스에는 예보가 두 가지 있다. 지금까지 쓴 단기예보(getVilageFcst)는 하루 여덟 번 나오고 나흘 이상을 보여 준다. 초단기예보(getUltraSrtFcst)는 매시 나오고 앞으로 6시간만 보여 주는 대신 더 자주 고쳐진다. 실제로 받아 보면 10개 항목(기온 T1H, 1시간 강수량 RN1, 낙뢰 LGT 등) × 6개 시각, 60줄이 온다.

추천에는 둘 다 필요했다. 「지금 비가 오는지」, 「두세 시간 뒤 저녁 시간대가 어떤지」는 초단기예보가 정확하다. 하지만 「오늘 아침 최저와 낮 최고」, 「밤 10시 야식 시간」은 초단기예보의 6시간 밖에 있다. 그래서 단기예보의 오늘 시간별 값을 깔고, 겹치는 시각은 초단기예보로 덮었다.

var hours = (todayDay?.Hours ?? Array.Empty<WeatherHour>())
    .Where(h => now.Hours.All(n => n.Kst != h.Kst))   // 초단기에 있는 시각은 빼고
    .Concat(now.Hours)                                 // 초단기로 채운다
    .OrderBy(h => h.Kst)
    .ToList();

2편에서 기온 항목 이름을 인자로 받게 만든 것이 여기서 쓰인다. 단기예보는 TMP, 초단기예보는 T1H 를 읽어서 같은 WeatherHour 로 만들기 때문에 두 목록을 그냥 이어 붙일 수 있다.

「어제보다 추워졌는지」를 알려면 어제 최저·최고도 있어야 한다. 1편에서 썼듯 단기예보는 최근 사흘 발표만 주지만, 어제 받은 값을 그때 저장해 두면 된다. 날마다 지역별 최저·최고 한 줄씩을 DB 에 쓰고, 2주가 지나면 지운다.

12℃ 는 쌀쌀한가

이제 판단이다. 처음에는 기온 하나로 정했다. 몇 ℃ 이하면 국물. 그런데 실제 예보를 넣어 보면 어색한 결과가 계속 나왔다. 바람이 센 날의 8℃ 와 잔잔한 날의 8℃ 가 같게 나왔고, 10월의 12℃ 와 4월의 12℃ 도 같게 나왔다. 사람이 저녁 식탁에서 느끼는 추위는 그렇지 않다.

그래서 기준을 「기온」에서 「먹을 때 느끼는 추위」로 바꿨다. 다섯 가지를 봤다.

첫째, 끼니 시간대의 기온이다. 지금 기온이 아니라, 사용자가 저녁을 물었다면 1820시, 야식이면 2223시 칸의 예보를 보고 그중 가장 낮은 값을 쓴다. 끼니가 정해지지 않았거나 이미 지났으면 앞으로 세 시간을 본다.

둘째, 바람이다. 기상청이 쓰는 겨울철 체감온도 식은 기온과 풍속으로 계산한다. 기온 10℃ 이하, 풍속 시속 4.8km 이상일 때만 쓰는 식이라 그 밖에서는 기온을 그대로 둔다.

/// <summary>기상청 체감온도(겨울철) — 기온 10℃ 이하·풍속 4.8km/h 이상에서만, 그 밖에는 기온 그대로.</summary>
public static double FeelsLike(double tempC, double windMs)
{
    var v = windMs * 3.6;   // 예보의 풍속은 m/s, 식은 km/h
    if (tempC > 10 || v < 4.8) return tempC;
    var p = Math.Pow(v, 0.16);
    return 13.12 + 0.6215 * tempC - 11.37 * p + 0.3965 * p * tempC;
}

처음 장면의 그날 아침 7시 예보는 3℃ 에 풍속 1.5m/s 였다. 시속으로 5.4km 라 식이 적용되고, 체감온도는 1.7℃ 가 나온다. 단위를 놓치기 쉽다. 예보의 WSD 는 m/s 인데 식은 km/h 를 받는다. 3.6 을 곱하지 않으면 대부분의 날이 「바람 약함」으로 빠져서 체감온도가 계산되지 않는다.

셋째, 흐리거나 비가 오는지다. 하늘상태가 흐림이거나 강수형태가 있는 날은, 체감 17℃ 이하일 때 2℃ 더 춥게 쳤다.

넷째, 계절이다. 같은 기온이라도 가을에는 몸이 아직 추위에 익지 않아 더 춥게 느끼고, 봄에는 겨울을 지나온 뒤라 덜 춥게 느낀다. 911월은 기준을 2℃ 올리고 35월은 2℃ 내렸다.

다섯째, 어제와의 차이와 일교차다. 처음 장면의 그날처럼, 숫자는 그럭저럭인데 어제보다 부쩍 추워진 날이 제일 국물이 당긴다.

이걸 모으면 판단은 위에서부터 차례로 내려온다.

판단 조건 (계절 보정 전 기준)
추움 체감 5℃ 이하
추움 어제보다 낮 최고나 아침 최저가 5℃ 이상 낮고, 체감 17℃ 이하
쌀쌀 체감 12℃ 이하
쌀쌀 일교차 10℃ 이상인 날의 아침·저녁·야식, 체감 17℃ 이하

판단 결과는 추천 문장 조각과 화면에 붙는 작은 칩으로 나간다. 「저녁이 쌀쌀해서(체감 10℃) 따뜻한 국물을」, 「서울 부쩍 추워짐」 같은 식이다.

여기서 하나 더 고친 것이 있다. 처음에는 계산한 체감온도 소수값으로 판단했더니, 화면에는 똑같이 「체감 7℃」라고 찍힌 두 지역 중 한 곳은 쌀쌀, 한 곳은 추움으로 갈렸다. 6.6℃ 와 7.4℃ 가 둘 다 7 로 보였기 때문이다. 사용자는 화면의 숫자를 본다. 그래서 판단도 화면에 보이는 반올림 값으로 하게 바꿨다.

// 화면에 보이는 반올림 값으로 판단한다 — 같은 '체감 7℃' 가 지역마다 쌀쌀·추움으로 갈리지 않게
var f = Math.Round(feels, MidpointRounding.AwayFromZero);

Math.Round 의 기본값은 은행가 반올림이라 6.5 가 6 이 된다. 화면 표시와 판단이 어긋나지 않게 AwayFromZero 로 맞췄다.

마치며

기상청 API 를 붙이는 일 자체는 1편과 2편으로 끝났다. 키를 한 번만 인코딩하고, 발표 시각을 맞추고, resultCode 를 확인하고, 격자를 고르고, 세로 목록을 펼치면 된다. 시간이 오래 걸린 것은 그다음, 숫자를 사람이 느끼는 말로 바꾸는 부분이었다. 기온 하나로 시작한 판단이 끼니 시간, 바람, 하늘, 계절, 어제를 거치며 다섯 갈래가 됐다. 그래도 결과는 칩 한 줄과 문장 한 조각이다. 날씨가 없으면 그 한 줄이 빠질 뿐, 추천은 그대로 돈다.

공공데이터포털에는 날씨 옆에 붙이기 좋은 데이터가 하나 더 있다. 에어코리아의 실시간 대기오염 정보다. 다음 편에서는 같은 키로 미세먼지 API 를 붙이며 만난 것들을 다룬다.


자료 출처: 기상청, 공공데이터포털 「기상청_단기예보 조회서비스」. 글에 실은 예보 값은 2026년 10월 말 서울 격자(60, 127)로 받은 응답이다.