경제 캘린더 API로 내 앱 안에 캘린더 보여주기

경제 캘린더를 제공하는 제품 팀에는 사용자별 국가 필터, 보는 사람의 시간대로 바꾼 발표 시각, 일정이 바뀐 발표를 보여주는 방법이 필요합니다. Narwhal은 UTC 시각, 발표 기관의 시간대, 대상 기간, 이전 값, 수정 카운터를 포함해 발표를 반환하고, 앱이 필터, 시계, 캐시를 더합니다.

앱이 캘린더 API에 요청하는 것

자체 앱에 경제 캘린더를 넣는 제품 팀에는 데이터 소스가 대신해 줄 수 없는 일이 세 가지 있습니다. 사용자가 국가를 고르게 하기, 모든 시각을 보는 사람의 시간대로 보여주기, 발표가 바뀔 때 화면을 정확하게 유지하기입니다. API는 발표를 제공하고, 앱이 필터, 시계, 캐시를 맡습니다. GET /v1/economics/calendar는 countries(국가마다 반복해서 지정하며 USA나 CAN 같은 세 글자 코드 사용), start_date, end_date, limit, cursor를 받습니다. 이 요청은 두 나라의 이틀치를 가져옵니다.

curl, 두 나라의 이틀치
curl -s -H "Authorization: Bearer $NARWHAL_API_KEY" \
  "https://api.narwhalapi.com/v1/economics/calendar?countries=USA&countries=CAN&start_date=2026-11-05&end_date=2026-11-06&limit=100"
응답, 처음 두 발표
{
  "releases": [
    {
      "release_id": "1b503ce9-69ee-4ec4-8dd9-deba7f1a6554",
      "revision": 2,
      "country": "USA",
      "series_id": "usa-employment-report",
      "name": "Employment report",
      "topic": "employment",
      "family": "labour",
      "period": "2026-10",
      "status": "scheduled",
      "at": "2026-11-06T13:30:00Z",
      "timezone": "America/New_York",
      "values": [
        {
          "measure": "nonfarm_payrolls_change",
          "concept": "payrolls",
          "unit": "thousands",
          "transformation": "change",
          "previous": "29"
        },
        {
          "measure": "unemployment_rate",
          "concept": "unemployment_rate",
          "unit": "percent",
          "previous": "4.2"
        }
      ]
    },
    {
      "release_id": "20c30c26-9a9a-4218-a8fb-6233ac8d291a",
      "revision": 1,
      "country": "USA",
      "series_id": "usa-productivity-and-costs",
      "name": "Productivity and costs",
      "topic": "other",
      "family": "labour",
      "period": "2026-Q3",
      "stage": "preliminary",
      "status": "scheduled",
      "at": "2026-11-05T13:30:00Z",
      "timezone": "America/New_York",
      "values": []
    }
  ],
  "next_cursor": null
}

전체 응답에는 발표 다섯 건이 들어 있습니다. at은 UTC 발표 시각이고 timezone은 발표 기관 자체의 시간대여서, 앱은 at을 보는 사람마다 변환하고 발표 기관의 시간대를 두 번째 라벨로 보여줄 수 있습니다. period는 기준 기간이고 values[].previous는 그 이전 수치입니다. release_id는 요청이 달라도 발표를 식별하며, 세부 내용이 바뀌면 revision이 올라갑니다. values는 항상 있으며 빈 목록일 수 있습니다.

국가별로 캐시하고 보는 사람의 시간대로 그리기

사용자마다 고르는 국가 조합이 다르므로 국가별로 따로 캐시하고 사용자마다 목록을 합칩니다. 둘 다 미국을 팔로우하는 두 사용자는 캐시된 목록 하나를 공유합니다.

  1. 한 번에 한 나라씩 가져오기 country_releases는 한 나라를 요청하고 next_cursor가 null이 될 때까지 따라갑니다. 한 페이지에는 발표가 최대 100건 들어 있기 때문입니다.
  2. 응답을 짧은 시간 동안 보관하기 TTL은 캐시된 국가가 최신으로 간주되는 시간(초)입니다. 사용자가 바뀐 시각을 얼마나 빨리 봐야 하는지에 맞춰 고르세요.
  3. 사용자별로 합치고 변환하기 user_calendar는 사용자의 국가를 모아 사용자의 시간대로 각 at을 변환하고 현지 시각순으로 정렬합니다. ZoneInfo가 보는 사람의 시계 변경(서머타임 등)을 적용하므로 앱은 오프셋을 저장하지 않습니다.
    Python, 캐시가 있는 사용자별 캘린더
    import os
    import time
    from datetime import datetime
    from zoneinfo import ZoneInfo
    import requests
    
    BASE = "https://api.narwhalapi.com"
    HEADERS = {"Authorization": f"Bearer {os.environ['NARWHAL_API_KEY']}"}
    TTL = 300  # seconds a cached country stays fresh
    cache = {}  # (country, start, end) -> (fetched_at, releases)
    
    
    def country_releases(country, start, end):
        key = (country, start, end)
        hit = cache.get(key)
        if hit and time.time() - hit[0] < TTL:
            return hit[1]
        params = {"countries": country, "start_date": start, "end_date": end, "limit": 100}
        releases = []
        while True:
            r = requests.get(f"{BASE}/v1/economics/calendar", params=params, headers=HEADERS, timeout=10)
            r.raise_for_status()
            body = r.json()
            releases += body["releases"]
            if not body["next_cursor"]:
                break
            params["cursor"] = body["next_cursor"]
        cache[key] = (time.time(), releases)
        return releases
    
    
    def user_calendar(user, start, end):
        zone = ZoneInfo(user["tz"])
        rows = []
        for country in user["countries"]:
            for x in country_releases(country, start, end):
                if "at" in x:
                    local = datetime.fromisoformat(x["at"].replace("Z", "+00:00")).astimezone(zone)
                    rows.append((local, x))
        return sorted(rows, key=lambda t: t[0])
    
    
    user = {"countries": ["USA", "CAN"], "tz": "Asia/Jakarta"}
    day = None
    for local, x in user_calendar(user, "2026-11-05", "2026-11-06"):
        if local.date() != day:
            day = local.date()
            print(day.isoformat())
        print(f"  {local:%H:%M} {x['country']} {x['name']} {x.get('period', '-')}")

아래 출력은 미국과 캐나다를 팔로우하는 자카르타 사용자의 예입니다. Credit statistics 발표는 11월 6일 20:00 UTC이고, 자카르타에서는 11월 7일 03:00이므로 그 사용자 화면에서는 다른 날짜에 놓입니다. UTC 날짜가 아니라 반드시 보는 사람의 현지 날짜로 묶으세요.

Asia/Jakarta 사용자의 출력
2026-11-05
  20:30 USA Productivity and costs 2026-Q3
  20:30 USA Jobless claims 2026-10-31
2026-11-06
  20:30 USA Employment report 2026-10
  20:30 CAN Labour force survey 2026-10
2026-11-07
  03:00 USA Credit statistics 2026-09

발표가 바뀌어도 화면을 정확하게 유지하기

보여주는 각 행과 함께 release_id와 revision을 저장하세요. 새로 고친 뒤 저장된 값보다 revision이 높은 행은 바뀐 것이므로 다시 그리고 새 시각을 표시하세요. 상태가 cancelled인 발표는 취소선을 긋거나 숨기고, 해당 기간에 더 이상 돌아오지 않는 행은 다른 날짜로 옮겨졌을 수 있으니 그 화면에서 제거하세요.

일부 발표는 정확한 시각이 없고 at 대신 date, between, by가 있습니다. 위 스크립트는 이를 건너뜁니다. 실제 앱에서는 그날의 맨 위에 종일 항목으로 나열하세요. 푸시 알림에는 webhook이 release.scheduled, release.rescheduled, release.cancelled를 알려줍니다. webhook 수신하기를 참고하세요.

제품 팀이 한 번만 정하는 사항

결정선택필드 또는 매개변수
사용자별 국가 필터사용자의 국가를 보내고, 국가마다 캐시된 목록 하나를 둔다countries
표시 시각UTC 시각을 보는 사람의 시간대로 변환하고, 발표 기관의 시간대는 라벨로 표시한다at, timezone
날짜별 묶기보는 사람의 현지 날짜로 묶는다at
변경 감지저장된 revision과 새 revision을 비교한다release_id, revision
토픽 또는 패밀리 칩요청에 토픽 매개변수가 없으므로 앱에서 필터링한다family, topic
속도 제한과 재시도캐시를 사용하고 429 응답에는 백오프한다. 응답 캐시와 429 오류 처리 참조응답 헤더

사용자가 토픽이나 패밀리로 필터링할 수 있나요?

요청은 국가, 날짜, limit, cursor를 받으며 토픽 필터는 없습니다. 모든 발표에는 family가 있고 대부분 topic이 있으므로 직접 코드에서 필터링하세요. 일부 발표에는 토픽이 없고 단순 조회는 키 오류를 일으키므로 x.get("topic")을 쓰세요.

날짜 범위는 UTC를 따르나요, 발표 기관의 현지 날짜를 따르나요?

발표 기관의 현지 날짜를 따릅니다. UTC 23:30이나 23:50에 나오는 일본의 발표는 도쿄에서 다음 날 아침이므로 다음 날 범위에 들어갑니다. 앱이 보는 사람의 시간대로 하루를 만들 때는 앞뒤로 하루씩 더 요청하고 at으로 필터링하세요.

화면 하나에 요청이 몇 번 필요한가요?

캐시된 국가마다 한 번, next_cursor에 값이 있으면 추가 페이지마다 한 번 더입니다. 캐시된 국가는 유효 시간이 끝날 때까지 비용이 들지 않습니다.

에 게시되었습니다.

다음 단계