위도와 경도를 입력하면 해당 좌표의 국가와 도시 정보가 화면에 출력되는 예제로 아래의 기능을 수행한다.
- API와 통신해서 좌표 값으로 국가 정보 가져와 화면에 렌더링하기
- navigator Web API 사용해서 내 위치 정보로 국가 정보 가져오기
비동기
, 프로미스 객체
, API
, fetch API
, 에러 핸들링
, 프로미스 체이닝
, REST
, Geocoding
, Promisifying
Jonas Schmedtmann - Asynchronous JS in The Complete JS Course
Get information about countries via a RESTful API
식별자로 특정 국가 정보를 요청하면 국기 이미지 주소를 포함한 개략적인 정보가 담긴 객체를 받을 수 있는 API.
⚠️ V3가 최신 버전이지만 CORS 이슈로 당장은 V2 버전 ‘eu’ 도메인을 사용할 것 (국가 이름은 URL에 쿼리 명시)
https://restcountries.eu/rest/v2/name/{국가 이름}?fullText=true
https://restcountries.eu/rest/v2/alpha/{알파 코드}
This API receives text input and outputs location information in XML, CSV, JSON, GeoJSON Formats.
Geocoding이란?
고유명칭(주소나 산, 호수의 이름 등)을 가지고 위도와 경도의 좌표값을 얻는 것을 말하며, 반대로 위도와 경도값으로부터 고유 명칭을 얻는 것은 Reverse Geocoding이라고 한다.
https://geocode.xyz/{위도},{경도}?outputformat
https://geocode.xyz/41.3189957000,2.0746469000?json=1)
- 유저가 함수를 좌표 정보(경도, 위도)를 인자로 함수를 호출한다.
- 2번 단계에선 다음의 Tasks를 수행한다.
- URI에 경도와 위도 정보를 담아 JSON 형식으로 받을 것을 API에 요청한다.
- API에서 성공적으로 정보를 받게 되면 Response 객체를 (Fulfilled Promise) 받는다.
- Response의 json() 메소드로 Response Body의 정보를 JS 객체 형식으로 변환하여 반환
- 변환된 객체에서 좌표에 해당하는 국가의 이름을 인자로 Promise Fetch를 반환
- 3번 단계에선 다음의 Tasks를 수행한다.
- URI에 국가 이름을 넣어 REST Countries API로 해당 국가 정보 요청
- API에서 성공적으로 정보를 받게 되면 Response 객체를 받는다.
- Response Body의 정보를 JS 객체 형식으로 변환하여 반환.
- 변환된 객체에서 국기 이미지 주소, 국가 이름, 지역, 인구, 통화를 추출하여 HTML 코드에 삽입
- 전달된 국가의 정보를 표현하는 Country Info Card를 DOM을 통해 화면에 출력.
간단히 두 개의 파일로 모듈을 나누었다. API와 통신을 위한 모듈(2, 3번 수행)과 가져온 프로퍼티를 사용하여 화면에 정보를 렌더링하는 모듈(4번)이다.
⚠️ Node js 그 자체로는 ES6의 import 기능을 지원하지 않는다. 따라서
package.json
에“type” : “module”
을 명시해주어야 한다. (또는 esm 모듈 사용)
import { renderCountry, countriesContainer } from './rendering.js'
renderCountry 함수와 countriesContainer라는 DOM 요소를 담은 변수를 가져온다.
const getCountryData = (lat, lng) => {
fetch(`https://geocode.xyz/${lat},${lng}?geoit=json`) // 1-1
.then(res => res.json()) // 1-2
.then(data => {
return fetch(`https://restcountries.eu/rest/v2/name/${data.country}?fullText=true`) // 1-3
})
.then(response => response.json()) // 1-4
.then(([data]) => renderCountry(res)) // 1-5
.finally(() => {
countriesContainer.style.opacity = 1; // 1-6
})
};
👉 1-1
위도(latitude)와 경도(longitude)를 인자를 받은 함수는 위도와 경도를 URI에 넣어 API에 해당 위치의 지역 정보를 요청하게 된다. (GET method)
👉 1-2
해당 정보가 있다면 ReadableStream이라는 컨텐츠를 담은 Response 객체가 들어오게 되는데, ReadableStream을 우리가 활용할 수 있는 데이터 형식으로 바꾸어주어야 한다.
위 정보는 Response.json() 메소드로 객체화 시켜준 정보의 본문이다. 해당 지역에 대한 갖가지 정보가 들어있으며 신뢰율은 90%라고 명시되어 있다. (confidence 프로퍼티)
여기서 국가 이름(country 프로퍼티) 값을 다시 URL 프로퍼티로 REST countries API에 해당 국가의 개략적인 정보를 요청한다.
👉 1-3
국가명 ‘Germany’로 REST countries API에서 가져온 Reponse 객체 (fulfilled promise)다. 위와 마찬가지로 json()
메소드로 객체화 하여 활용해야 한다.
⚠️ 또 다른 콜백 지옥을 만들지 않도록 주의!
.then(data => { return fetch(`https://restcountries.eu/rest/v2/name/${data.country}?fullText=true`) }) .then(response => response.json()) .then(([data]) => renderCountry(res))
Promise의 메소드를 연결해서 사용하는 것을 Promise Chaining이라고 한다. 국가 정보를 요청히기 위한 새로운 fetch 함수 다음에 then 메소드를 이어붙이지 않고 return을 한 것을 볼 수 있다.
.then(res => res.json()) .then(data => { return fetch(`https://restcountries.eu/rest/v2/name/${data.country}?fullText=true`) .then(response => response.json()) .then(([data]) => renderCountry(res)) })
만약 위 코드처럼 then 메소드 안에 또 다른 체이닝을 이어나가면 콜백 지옥을 피하기 위해 메소드 체이닝을 사용하는 이유가 없어진다. fetch 함수는 fulfilled이건 rejected 이건 Promise 객체를 반환하기 때문에 fetch 함수의 반환 값을 리턴하면 체이닝을 이어갈 수 있다.
👉 1-4
REST countries API는 알파 코드와 인접 국가, 통화, 언어 등 해당 국가의 개요를 한 눈에 볼 수 있는 JSON을 보내준다. 여기서 우리가 사용할 프로퍼티는 렌더링 함수에서 처리할 것이기 때문에 import된 렌더링 함수에 국가 정보가 담긴 객체만 인자로 넘겨준다.
👉 1-5
국가 정보를 객체로 받아 화면에 정보 카드로 출력하기 위한 DOM Manipulating 함수. 받아온 객체의 구조에 맞게 정보를 분리하여 HTML 코드에 데이터를 바인딩한다.
그 후 정보가 출력될 위치(countriesContainer)의 자식 요소로 추가해준다.
const countriesContainer = document.querySelector('.countries');
const renderCountry = (obj) => {
const html = `<article class="country">
<img class="country__img" src="${obj.flag}" />
<div class="country__data">
<h3 class="country__name">${obj.name}</h3>
<h4 class="country__region">${obj.region}</h4>
<p class="country__row"><span>👫</span>${Math.floor(
obj.population / 1000000
).toFixed(1)} Millions</p>
<p class="country__row"><span>🗣️</span>${obj.languages[0].name}</p>
<p class="country__row"><span>💰</span>${obj.currencies[0].name}</p>
</div>
</article>`;
countriesContainer.insertAdjacentHTML('beforeend', html);
};
👉 1-6
finally()
메소드는 전달된 Promise가 fulfilled인지 reject인지 여부와 관계없이 무조건 실행시킬 로직을 담는 메소드이다.
국가 정보가 출력되는 컨테이너의 opacity값이 0으로 설정되어 있고 정보가 표시될 때 transition 효과를 나타내기 위해 정보 또는 오류가 출력되려면 opacity는 무조건 1로 변경되어야 한다.
통신을 통해 외부 데이터를 받아와야 정상적으로 작동되기 때문에 통신 여부가 중요한 변수가 된다. API에 올바른 요청을 하지 않았거나, API의 오류이거나, 통신을 위한 각 단계마다 에러 발생의 여지가 있다.
통신에 실패했을 경우를 염두하여 발생되는 에러를 대처하기 위한 로직도 미리 만들어놓아야 하는데 이것을 에러 핸들링이라고 한다. 예외 처리 로직이 엉성해서 Uncaught Error가 노출되는 프로그램은 Bad Practice라고 할 수 있다.
fetch 함수의 결과로서 우리는 다음과 같은 상황을 맞이할 수 있다.
catch()
메소드가 에러를 인지하고 잡아낼 순 있지만, 통신 과정에서 Reject되지 못한 에러 (e.g. 404 등)는 메소드 하나로만 처리할 순 없다.
새로운 에러 객체를 발생시켜서 다시 catch 메소드로 에러를 핸들링하는 패턴을 사용해야 한다. 에러 또한 객체로 취급되어 인스턴스 생성시 new 키워드를 사용한다.
👉 2-1
.then(res => {
if (!res.ok) throw new Error(`Problem with geocoding (${res.status})`);
return res.json();
})
위 코드는 Geocode API에 위치 정보를 요청하는 fetch 함수 바로 다음에 위치한다.
만약 Response 객체의 ok 프로퍼티가 false라면 ‘Problem with geocoding’이라는 메시지를 담은 새로운 에러를 생성한다. 이렇게 특정 위치마다 예상할 수 있는 에러 메세지를 남겨놓으면 어디서 무엇이 잘못되었는지 직관적으로 인지할 수 있다.
👉 2-2
.then(response => {
if (!response.ok)
throw new Error(`Country not found (${response.status})`);
return response.json();
})
마찬가지로 REST countries API에 국가 정보를 요청하는 fetch 함수 뒤에 에러 예외 처리 코드를 두어 에러 발생 원인을 인지시킨다.
👉 2-3
.catch(err => console.error(`${err.message} 🚨`))
체이닝에서 발생된(throw) 모든 에러는 catch 메소드에서 잡아낸다.
에러 발생이 예상되는 곳에 에러 핸들링을 해놓으면 Uncaught Error를 예방할 수 있다.
getCountryData(52.508, 13.381);
getCountryData(19.037, 72.873);
getCountryData(-33.933, 18.474);
Syntax
navigator.geolocation.getCurrentPosition(success, error, [options])
내 디바이스의 위치 정보를 가져오는 내장 API로 정보를 가져오는데 성공했을때(fulfilled) 실행될 콜백 함수와 실패했을(rejected) 때 실행될 콜백 함수를 인자로 받는다.
getCurrentPosition()
메소드는 다른 비동기 함수와 같이 동작한다. 하지만 정보를 받아오는 타이밍에 맞춰 다음 코드와 실패했을때의 코드를 프로미스화 해서 코드 블럭이 중첩되지 않도록 해야 한다.
비동기 함수를 프로미스의 콜백(resolve, reject) 처럼 만들어 .then
이나 .catch
메소드 체이닝을 사용할 수 있도록(thenable) 하는 것을 Promisifying이라고 한다.
const getPosition = () => {
return new Promise((resolve, reject) => {
navigator.geolocation.getCurrentPosition(resolve, reject);
});
};
getCurrentPosition()
메소드 또한 fulfill callback
과 reject callback
을 받기 때문에 간단하게 결과를 프로미스 객체로 반환하도록 만들 수 있다.
Response 값을 결과로 받아보면 아래와 같은 객체가 들어온다.
내 위치의 위도와 경도 정보를 사용해서 예제의 함수에 인자로 넘겨주면 내 위치의 국가 정보를 알 수 있다.
getPosition()
.then(res => {
const { latitude, longitude } = res.coords;
getMyLocation(latitude, longitude);
})
.catch(console.log);
심지어 지역명과 행정 단위를 해당 국가의 언어로 얻을 수 있다. 단 REST countries API에서 'South Korea'는 국가 검색 키워드에 적합하지 않다. 대신 alpha-2 코드를 사용한다. 대한민국의 alpha-2 코드는 'KR'이다. (alpha-3 코드는 'KOR')
.then(data => {
return fetch(`https://restcountries.eu/rest/v2/alpha/${data.state}`);
})
알파 코드로 국가 정보를 요청하도록 URI와 바인딩 될 데이터의 프로퍼티를 변경한다.
아래는 실행되는 모습
글
ⓒ Wonkook Lee
예제
ⓒ Jonas Schmedtmann
References
Network REST란? REST API란? RESTful이란? - Heee’s Development Blog
🙏🏻 잘못된 정보가 있다면 지적해주세요
크... 이렇게 실전지향(?)적인 모습 항상 멋있게 생각합니다.