JS LAB
JS 8.2 · 8부. 종합 프로젝트

환율·날씨 조회기

fetch 와 async/await 로 외부 데이터를 화면에 냅니다.

예상 학습 시간 25분 실행 단추가 있는 예제는 고쳐서 실행해 볼 수 있습니다 난이도 실전
요구사항

무엇을 만드나

도시를 고르면 지금 날씨를, 통화를 고르면 원화 환율을 보여 주는 화면을 만듭니다. 둘 다 실제 서버에서 받아 옵니다.

화면에 있을 것

  • 도시를 고르는 자리와 조회 단추
  • 결과를 보여 줄 칸
  • 불러오는 중·실패 표시

지켜야 할 규칙

  • 요청 중에는 단추를 잠급니다.
  • 서버가 실패를 돌려주면 사람이 읽을 수 있는 문구로 알립니다.
  • 끝나면 성공이든 실패든 단추를 다시 풉니다.

5부(fetch · async/await · 오류 처리) · 4부(DOM) · 3부(객체)를 사용합니다.

단계별 구현

1단계 — 무엇이 오는지 먼저 봅니다

남의 서버가 어떤 모양으로 돌려주는지 모르면 코드를 쓸 수 없습니다. 먼저 받아서 그대로 살펴봅니다.

script.js
const url = "https://api.open-meteo.com/v1/forecast"
	+ "?latitude=37.57&longitude=126.98"
	+ "&current=temperature_2m,relative_humidity_2m";

const response = await fetch(url);
const data = await response.json();

console.log(response.ok);
console.log(Object.keys(data.current));
console.log(typeof data.current.temperature_2m);
console.log(data.current_units.temperature_2m);
출력
true ["time", "interval", "temperature_2m", "relative_humidity_2m"] number °C

온도 값 자체는 적어 두지 않았습니다. 실행할 때마다 달라지기 때문입니다. 대신 있는지·어떤 타입인지를 확인했습니다.

Object.keys 는 객체의 속성 이름을 배열로 돌려줍니다. 처음 보는 응답을 살필 때 편합니다.

단계별 구현

2단계 — 필요한 것만 꺼냅니다

받은 것을 그대로 쓰지 말고 내 화면이 쓰는 모양으로 바꿉니다. 서버가 모양을 바꿔도 이 함수 하나만 고치면 됩니다.

script.js
const CITIES = {
	서울: { lat: 37.57, lon: 126.98 },
	부산: { lat: 35.18, lon: 129.08 }
};

async function loadWeather(city) {
	const { lat, lon } = CITIES[city];

	const url = `https://api.open-meteo.com/v1/forecast`
		+ `?latitude=${lat}&longitude=${lon}`
		+ `&current=temperature_2m,relative_humidity_2m`;

	const response = await fetch(url);
	if (!response.ok) throw new Error(`서버가 ${response.status} 를 돌려주었습니다`);

	const data = await response.json();

	return {
		city: city,
		temperature: data.current.temperature_2m,
		humidity: data.current.relative_humidity_2m
	};
}

const weather = await loadWeather("서울");

console.log(Object.keys(weather));
console.log(weather.city);
console.log(typeof weather.temperature, typeof weather.humidity);
출력
["city", "temperature", "humidity"] 서울 number number

3.7 의 구조 분해로 좌표를 꺼냈고, ok 를 확인해 실패를 throw 로 올렸습니다(5.5).

단계별 구현

3단계 — 환율도 같은 모양으로

script.js
async function loadRate(code) {
	const response = await fetch(`https://open.er-api.com/v6/latest/${code}`);
	if (!response.ok) throw new Error(`서버가 ${response.status} 를 돌려주었습니다`);

	const data = await response.json();

	return {
		code: code,
		krw: data.rates.KRW,
		updated: data.time_last_update_utc
	};
}

const rate = await loadRate("USD");

console.log(Object.keys(rate));
console.log(rate.code);
console.log(typeof rate.krw);
console.log(rate.krw > 0);
출력
["code", "krw", "updated"] USD number true

두 함수의 생김새가 같습니다 — 받아서, 확인하고, 내 모양으로 바꿔 돌려줍니다. 서버가 달라도 쓰는 쪽은 같게 다룰 수 있습니다.

단계별 구현

4단계 — 세 상태를 화면에

script.js
const btn = document.querySelector("#load");
const box = document.querySelector("#box");

async function loadWeather() {
	const url = "https://api.open-meteo.com/v1/forecast"
		+ "?latitude=37.57&longitude=126.98&current=temperature_2m";

	const response = await fetch(url);
	if (!response.ok) throw new Error("날씨를 받아 오지 못했습니다");

	const data = await response.json();
	return data.current.temperature_2m;
}

async function show() {
	btn.disabled = true;
	box.textContent = "불러오는 중...";
	box.className = "loading";

	try {
		const temperature = await loadWeather();

		box.textContent = `서울 ${temperature}°C`;
		box.className = "done";
	} catch (error) {
		box.textContent = `${error.message} 잠시 뒤에 다시 눌러 주세요.`;
		box.className = "error";
	} finally {
		btn.disabled = false;
	}
}

btn.addEventListener("click", show);

await show();

console.log(box.className);
console.log(btn.disabled);
console.log(box.textContent.endsWith("°C"));
출력
done false true

온도는 바뀌므로 끝나는 글자만 확인했습니다. 미리보기 탭에서 실제 값을 볼 수 있고, 단추를 눌러 다시 받아 올 수도 있습니다.

완성 코드

모두 합치기

도시를 고르고 조회하면 날씨와 환율을 함께 받아 옵니다. 서로 기다릴 까닭이 없으므로 Promise.all 을 씁니다(5.3).

script.js
const CITIES = {
	서울: { lat: 37.57, lon: 126.98 },
	부산: { lat: 35.18, lon: 129.08 }
};

const select = document.querySelector("#city");
const btn = document.querySelector("#load");
const box = document.querySelector("#box");

async function loadWeather(city) {
	const { lat, lon } = CITIES[city];

	const response = await fetch(`https://api.open-meteo.com/v1/forecast`
		+ `?latitude=${lat}&longitude=${lon}`
		+ `&current=temperature_2m,relative_humidity_2m`);

	if (!response.ok) throw new Error("날씨");

	const data = await response.json();

	return {
		temperature: data.current.temperature_2m,
		humidity: data.current.relative_humidity_2m
	};
}

async function loadRate() {
	const response = await fetch("https://open.er-api.com/v6/latest/USD");
	if (!response.ok) throw new Error("환율");

	const data = await response.json();
	return Math.round(data.rates.KRW);
}

function draw(city, weather, krw) {
	box.replaceChildren();

	const lines = [
		`${city} ${weather.temperature}°C`,
		`습도 ${weather.humidity}%`,
		`1달러 = ${krw}원`
	];

	lines.forEach(line => {
		const div = document.createElement("div");
		div.textContent = line;
		box.append(div);
	});
}

async function show() {
	btn.disabled = true;
	box.textContent = "불러오는 중...";
	box.className = "loading";

	try {
		const city = select.value;
		const [weather, krw] = await Promise.all([loadWeather(city), loadRate()]);

		draw(city, weather, krw);
		box.className = "done";
	} catch (error) {
		box.textContent = `${error.message} 정보를 받아 오지 못했습니다. 잠시 뒤에 다시 눌러 주세요.`;
		box.className = "error";
	} finally {
		btn.disabled = false;
	}
}

btn.addEventListener("click", show);

await show();

console.log(box.className);
console.log(box.children.length);
출력
done 3

Promise.all하나라도 실패하면 통째로 실패합니다(5.6). 그래서 catch 하나로 둘을 함께 다룹니다. 실패한 쪽 이름을 오류 메시지에 담아 두었습니다.

미리보기 탭에서 도시를 바꿔 다시 조회해 보세요.

확장 과제

더 해 볼 것

1. 도시 늘리기 난이도 하

도시를 셋 이상으로 늘리세요. 코드를 거의 고치지 않아도 됩니다.

CITIES 에 좌표를 더하고, 고르는 자리도 그 객체로 그리면 한 곳만 고치면 됩니다 — Object.keys(CITIES).forEach(...)option 을 만듭니다(4.6).
2. 일부만 실패해도 보여 주기 난이도 중

환율이 실패해도 날씨는 보여 주게 고치세요.

Promise.allSettled 는 실패해도 통째로 실패하지 않고 각각의 결과를 돌려줍니다(5.6). status"fulfilled" 인 것만 그리면 됩니다.
3. 자동으로 다시 받기 난이도 상

1분마다 저절로 다시 받아 오게 하고, 화면을 벗어나면 멈추게 하세요.

setInterval 로 되풀이하고 번호를 담아 둡니다. 멈출 때 clearInterval 을 부릅니다(5.1). 요청이 끝나기 전에 다음 것이 시작되지 않도록 지금 받는 중인지도 함께 봐야 합니다.
요약

이 프로젝트에서 배운 것

  • 남의 서버를 쓰기 전에 무엇이 오는지 먼저 살핍니다.
  • 받은 것을 그대로 쓰지 말고 내 화면이 쓰는 모양으로 바꿔 돌려줍니다.
  • 불러오는 중 · 받았음 · 실패 세 상태를 모두 화면에 냅니다.
  • 서로 상관없는 요청은 Promise.all 로 함께 시작합니다.
  • 값이 계속 바뀌는 것은 고정된 출력으로 적지 않습니다.