본문 바로가기
튜토리얼/웹

[React] 날씨 앱 만들기 (Open-Meteo API · API 키 불필요)

by 루까(Luka) 2026. 3. 2.
반응형

날씨 앱 만들기

왜 만들었나

날씨 앱은 API 연동 튜토리얼의 정석이다. 그런데 OpenWeatherMap 같은 서비스는 무료지만 회원가입과 API 키 발급이 필요하다. Open-Meteo는 API 키가 아예 없다. 완전 무료, 오픈소스, 상업 이외 용도에서 제약 없이 사용 가능하다. 날씨 데이터 품질도 기상 데이터 전문 기관 수준이다.

이번 튜토리얼에서 다루는 것:

  1. Open-Meteo Geocoding API — 도시명 → 위경도 변환
  2. Open-Meteo Forecast API — 현재 날씨 + 7일 예보
  3. Geolocation API — 브라우저 위치 정보
  4. 탭 기반 UI — 국내(도시 버튼 그리드) / 전세계(영문 검색)
  5. 디바운스 자동완성 검색
  6. WMO 날씨 코드 매핑
  7. 날씨 기반 동적 배경

구조

11-react-weather/
└── index.html

React 18 CDN + Babel Standalone. 단일 파일.


API 구조

Geocoding — 도시명 검색

GET https://geocoding-api.open-meteo.com/v1/search
  ?name=서울
  &count=5
  &language=ko
{
  "results": [{
    "name": "서울",
    "latitude": 37.5665,
    "longitude": 126.9780,
    "admin1": "Seoul",
    "country": "South Korea"
  }]
}

Forecast — 날씨 데이터

GET https://api.open-meteo.com/v1/forecast
  ?latitude=37.5665
  &longitude=126.9780
  &current=temperature_2m,apparent_temperature,relative_humidity_2m,precipitation,weather_code,wind_speed_10m
  &daily=weather_code,temperature_2m_max,temperature_2m_min,precipitation_sum
  &timezone=auto
  &wind_speed_unit=ms

timezone=auto를 붙이면 좌표 기반으로 타임존을 자동 적용한다. 서울 좌표면 KST로 시간이 반환된다.


핵심 구현

탭 기반 UI — 한글 IME 문제 해결

한글 검색을 텍스트 입력으로 구현하면 IME 조합 문제가 발생한다. 'ㅅ+ㅓ+ㅚ → 서울'을 입력하는 동안 onChange가 조합 완료 전에는 발화하지 않아 글자가 한 자씩 추가될 때마다 자동완성 목록이 뜨지 않는다. onCompositionStart / onCompositionEnd로 보완할 수 있지만, 더 간단한 해법은 애초에 텍스트 입력을 없애는 것이다.

국내 탭은 주요 도시 20개를 하드코딩한 버튼 그리드로 구현했다. 클릭 한 번으로 날씨를 불러오니 UX도 더 빠르다.

const KOREA_CITIES = [
  { name: '서울',  lat: 37.5665, lon: 126.9780 },
  { name: '부산',  lat: 35.1796, lon: 129.0756 },
  { name: '제주',  lat: 33.4996, lon: 126.5312 },
  // ... 20개
];
function KoreaTab({ onSelect, activeCity, loading }) {
  return (
    <div className="city-grid">
      {KOREA_CITIES.map(city => (
        <button
          key={city.name}
          className={`city-btn${activeCity === city.name ? ' active' : ''}`}
          onClick={() => onSelect(city)}
          disabled={loading}
        >{city.name}</button>
      ))}
    </div>
  );
}

전세계 탭은 영문 텍스트 검색만 허용한다. 영문은 IME 조합 문제가 없어 글자마다 자동완성이 정상 동작한다.

탭 전환 시 날씨 상태를 초기화한다.

function switchTab(t) {
  setTab(t);
  setWeather(null);
  setLocation(null);
  setError(null);
  setActiveCity(null);
}

WMO 날씨 코드

Open-Meteo는 WMO(세계기상기구) 날씨 코드를 사용한다. 숫자 코드를 직접 표시하면 아무 의미가 없으므로 이모지와 레이블로 매핑한다.

const WMO = {
  0:  { emoji: '☀️',  label: '맑음',    bg: 'clear'   },
  1:  { emoji: '🌤️', label: '대체로 맑음', bg: 'clear' },
  61: { emoji: '🌧️', label: '비',       bg: 'rain'    },
  71: { emoji: '🌨️', label: '눈',       bg: 'snow'    },
  95: { emoji: '⛈️',  label: '뇌우',    bg: 'thunder' },
  // ...
};

function getWmo(code) {
  return WMO[code] || { emoji: '🌡️', label: '알 수 없음', bg: 'clear' };
}

bg 필드는 날씨에 따른 배경 그라디언트 클래스에 쓰인다.

동적 배경

useEffect(() => {
  if (!weather) return;
  const wmo = getWmo(weather.current.weather_code);
  document.body.className = `bg-${wmo.bg}`;
  return () => { document.body.className = ''; };
}, [weather]);
body.bg-clear   { background: linear-gradient(160deg, #0f2027, #203a43, #2c5364); }
body.bg-rain    { background: linear-gradient(160deg, #0d1b2a, #1b2838, #1e3a5f); }
body.bg-thunder { background: linear-gradient(160deg, #0d0d1a, #1a0d2e, #2d1b4e); }

날씨 코드가 바뀔 때마다 body 클래스를 교체해 배경이 전환된다. transition: background 0.8s ease로 자연스럽게 전환된다.

디바운스 자동완성 (전세계 탭)

const debounceRef = useRef(null);

function handleInput(val) {
  setQuery(val);
  clearTimeout(debounceRef.current);
  if (!val.trim()) { setSuggests([]); return; }

  debounceRef.current = setTimeout(async () => {
    const results = await geocode(val);
    setSuggests(results.slice(0, 5));
  }, 350);
}

타이핑할 때마다 API를 호출하면 불필요한 요청이 생긴다. 마지막 입력 후 350ms가 지나야 요청이 나간다. useRef로 타이머 ID를 저장해 이전 타이머를 매번 취소한다.

Geolocation API

navigator.geolocation.getCurrentPosition(
  async ({ coords }) => {
    const data = await fetchWeather(coords.latitude, coords.longitude);
    setLocation({
      name: '현재 위치',
      latitude: coords.latitude,
      longitude: coords.longitude,
    });
    setWeather(data);
  },
  () => setError('위치 접근이 거부되었습니다.')
);

브라우저 위치 API는 https:// 또는 localhost에서만 동작한다. 위치 허용/거부 두 경우를 모두 처리해야 한다. Open-Meteo는 역지오코딩(좌표 → 도시명)을 지원하지 않아서 "현재 위치"로 고정 표시한다.

섭씨/화씨 전환

function toF(c) { return Math.round(c * 9/5 + 32); }

function fmtTemp(c, isFahr) {
  return isFahr ? `${toF(c)}°F` : `${Math.round(c)}°C`;
}

function fmtSpeed(ms, isFahr) {
  return isFahr
    ? `${Math.round(ms * 2.237)} mph`   // m/s → mph
    : `${Math.round(ms * 3.6)} km/h`;   // m/s → km/h
}

API에서 항상 °Cm/s로 받고, 표시 시점에 변환한다. 데이터를 두 벌 저장하지 않아도 된다.


비교 테이블

Open-Meteo OpenWeatherMap
API 키 불필요 필요 (무료 발급)
무료 한도 무제한 (비상업) 1,000 req/day
날씨 코드 WMO 표준 자체 코드
역지오코딩 미지원 지원
한국어 지원 Geocoding만 전체
예보 기간 16일 5일 (무료)

삽질 기록

한글 IME 자동완성 문제

한글로 검색 가능하게 만들었더니 글자가 하나씩 추가될 때마다 자동완성 목록이 뜨지 않았다. 한글은 자모를 조합하는 IME 방식이라 onChange가 조합 완료 전에는 제대로 발화하지 않기 때문이다. onCompositionEnd로 보완할 수 있지만 이 방식도 조합 중 중간 결과가 목록에 반영되지 않는 한계가 있다. 결국 국내 탭은 텍스트 입력 대신 도시 버튼 그리드로 전환했다. 입력 문제도 사라지고 UX도 더 직관적이다.

timezone=auto 없으면 UTC 기준 시간 반환

timezone 파라미터를 빠뜨리면 응답의 current.time이 UTC 기준으로 온다. 서울 기준 오후 3시를 오전 6시로 표시하는 버그가 생긴다. timezone=auto로 좌표 기반 자동 적용하면 해결된다.

자동완성 외부 클릭 닫기

document.addEventListener('mousedown', handler)로 클릭 이벤트를 감지한다. click 대신 mousedown을 쓰는 이유는 자동완성 항목 클릭 시 blur 이벤트보다 먼저 발생해야 항목 선택이 정상 동작하기 때문이다.

Geolocation은 HTTP에서 안 됨

navigator.geolocation.getCurrentPosition은 보안 컨텍스트(HTTPS 또는 localhost)에서만 동작한다. GitHub Pages는 HTTPS이므로 문제없지만, 로컬 file:// 프로토콜로 열면 동작하지 않는다.


마무리

Open-Meteo는 API 키 없이 날씨 앱을 만들 수 있는 최선의 선택이다. WMO 날씨 코드 매핑, 디바운스 자동완성, Geolocation API, 단위 변환까지 하나의 앱에 실용적인 패턴이 많이 담겨 있다. 날씨 앱 하나만 제대로 만들어봐도 API 연동에 자신감이 붙는다.


기술 스택: React 18 CDN · Babel Standalone · Open-Meteo API · Geolocation API · Vanilla CSS
소스 코드: GitHub
데모: GitHub Pages

반응형