날씨 앱 만들기
왜 만들었나
날씨 앱은 API 연동 튜토리얼의 정석이다. 그런데 OpenWeatherMap 같은 서비스는 무료지만 회원가입과 API 키 발급이 필요하다. Open-Meteo는 API 키가 아예 없다. 완전 무료, 오픈소스, 상업 이외 용도에서 제약 없이 사용 가능하다. 날씨 데이터 품질도 기상 데이터 전문 기관 수준이다.
이번 튜토리얼에서 다루는 것:
- Open-Meteo Geocoding API — 도시명 → 위경도 변환
- Open-Meteo Forecast API — 현재 날씨 + 7일 예보
- Geolocation API — 브라우저 위치 정보
- 탭 기반 UI — 국내(도시 버튼 그리드) / 전세계(영문 검색)
- 디바운스 자동완성 검색
- WMO 날씨 코드 매핑
- 날씨 기반 동적 배경
구조
11-react-weather/
└── index.htmlReact 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
¤t=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=mstimezone=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에서 항상 °C와 m/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
'튜토리얼 > 웹' 카테고리의 다른 글
| [Next.js] 마크다운 블로그 만들기 (정적 생성) (1) | 2026.03.02 |
|---|---|
| [React] 무한 스크롤 구현하기 (커스텀 훅 + IntersectionObserver) (0) | 2026.03.02 |
| [React] 영화 검색 앱 만들기 (TMDB API) (0) | 2026.03.01 |
| [React] GitHub 프로필 검색기 만들기 (GitHub API) (0) | 2026.03.01 |
| [HTML/CSS/JS] 뽀모도로 타이머 만들기 (0) | 2026.03.01 |