만들면서 배운 JS Promise : Get Country Info with Coordinates

Wonkook Lee·2021년 9월 19일
22

Frontend Exercises

목록 보기
7/7


Get Country Info with Coordinates


개요

위도와 경도를 입력하면 해당 좌표의 국가와 도시 정보가 화면에 출력되는 예제로 아래의 기능을 수행한다.

  1. API와 통신해서 좌표 값으로 국가 정보 가져와 화면에 렌더링하기
  2. navigator Web API 사용해서 내 위치 정보로 국가 정보 가져오기

학습 목표 🚀

  • Fetch API를 이해하고 메소드를 사용할 수 있다.
  • Open API에 정보를 요청하고 응답을 받아 정보를 활용할 수 있다.
  • Fulfilled Promise와 Rejected Promise의 상황별 예외 처리를 할 수 있다.
  • 다른 비동기 함수를 Promise화(Primisifying) 할 수 있다.

학습 키워드 🔑

비동기, 프로미스 객체, API, fetch API, 에러 핸들링, 프로미스 체이닝, REST, Geocoding, Promisifying

예제 출처 ⓒ

Jonas Schmedtmann - Asynchronous JS in The Complete JS Course



사용할 APIs

1. REST Countries API 🌎

Get information about countries via a RESTful API
식별자로 특정 국가 정보를 요청하면 국기 이미지 주소를 포함한 개략적인 정보가 담긴 객체를 받을 수 있는 API.

⚠️ V3가 최신 버전이지만 CORS 이슈로 당장은 V2 버전 ‘eu’ 도메인을 사용할 것 (국가 이름은 URL에 쿼리 명시)

  • 국가 이름으로 찾기: https://restcountries.eu/rest/v2/name/{국가 이름}?fullText=true
  • 알파-2, 알파-3 코드로 국가 찾기: https://restcountries.eu/rest/v2/alpha/{알파 코드}

2. Geocode XYZ 📍

This API receives text input and outputs location information in XML, CSV, JSON, GeoJSON Formats.

Geocoding이란?
고유명칭(주소나 산, 호수의 이름 등)을 가지고 위도와 경도의 좌표값을 얻는 것을 말하며, 반대로 위도와 경도값으로부터 고유 명칭을 얻는 것은 Reverse Geocoding이라고 한다.

  • Reverse Geocoding: https://geocode.xyz/{위도},{경도}?outputformat
  • JSON 포맷 요청 예시: https://geocode.xyz/41.3189957000,2.0746469000?json=1)



REST와 REST API, RESTful이란? 📚

REST (REpresentational State Transfer)

  • HTTP URI(Uniform Resource Identifier)를 통해 자원(Resource)을 명시하고, HTTP Method(POST, GET, PUT, DELETE)를 통해 해당 자원에 대한 CRUD Operation을 적용하는 것을 의미한다.

REST API

  • REST 기반으로 서비스 API를 구현한 것
  • 최근 OpenAPI, 마이크로 서비스 등을 제공하는 업체 대부분은 REST API를 제공한다.

RESTful

  • REST라는 Architecture를 구현하는 웹 서비스를 나타내기 위해 사용되는 용어. (‘REST API’를 제공하는 웹 서비스를 ‘RESTful’하다고 할 수 있다)
  • 특정 서비스를 RESTful이라고 정의한 것은 아니며, REST 원리를 따르는 시스템을 명명하기 위해 사용되는 용어이다.
  • RESTful한 API를 구현하는 근본적인 목적은 성능 향상이 아닌, 일관적인 컨벤션을 통한 API의 이해도 및 호환성을 높이는 것이다.



프로세스 ⚙️

  1. 유저가 함수를 좌표 정보(경도, 위도)를 인자로 함수를 호출한다.
  1. 2번 단계에선 다음의 Tasks를 수행한다.
    • URI에 경도와 위도 정보를 담아 JSON 형식으로 받을 것을 API에 요청한다.
    • API에서 성공적으로 정보를 받게 되면 Response 객체를 (Fulfilled Promise) 받는다.
    • Response의 json() 메소드로 Response Body의 정보를 JS 객체 형식으로 변환하여 반환
    • 변환된 객체에서 좌표에 해당하는 국가의 이름을 인자로 Promise Fetch를 반환
  1. 3번 단계에선 다음의 Tasks를 수행한다.
    • URI에 국가 이름을 넣어 REST Countries API로 해당 국가 정보 요청
    • API에서 성공적으로 정보를 받게 되면 Response 객체를 받는다.
    • Response Body의 정보를 JS 객체 형식으로 변환하여 반환.
    • 변환된 객체에서 국기 이미지 주소, 국가 이름, 지역, 인구, 통화를 추출하여 HTML 코드에 삽입
  1. 전달된 국가의 정보를 표현하는 Country Info Card를 DOM을 통해 화면에 출력.



기능 구현 🛠

간단히 두 개의 파일로 모듈을 나누었다. API와 통신을 위한 모듈(2, 3번 수행)과 가져온 프로퍼티를 사용하여 화면에 정보를 렌더링하는 모듈(4번)이다.

⚠️ Node js 그 자체로는 ES6의 import 기능을 지원하지 않는다. 따라서 package.json“type” : “module”을 명시해주어야 한다. (또는 esm 모듈 사용)

fetching.js

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)의 자식 요소로 추가해준다.


rendering.js

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 함수의 결과로서 우리는 다음과 같은 상황을 맞이할 수 있다.

Geocode API와의 통신 과정에 문제가 생긴 경우

REST countries API와의 통신 과정에 문제가 생긴 경우

catch() 메소드가 에러를 인지하고 잡아낼 순 있지만, 통신 과정에서 Reject되지 못한 에러 (e.g. 404 등)는 메소드 하나로만 처리할 순 없다.


throw new Error(‘error message’) 🚨

새로운 에러 객체를 발생시켜서 다시 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);




내 위치를 기반으로 국가 정보 가져오기 🇰🇷

Geolocation.getCurrentPosition()

Syntax

navigator.geolocation.getCurrentPosition(success, error, [options])

내 디바이스의 위치 정보를 가져오는 내장 API로 정보를 가져오는데 성공했을때(fulfilled) 실행될 콜백 함수와 실패했을(rejected) 때 실행될 콜백 함수를 인자로 받는다.


Promisifying: 비동기 함수의 프로미스 콜백 함수화

getCurrentPosition() 메소드는 다른 비동기 함수와 같이 동작한다. 하지만 정보를 받아오는 타이밍에 맞춰 다음 코드와 실패했을때의 코드를 프로미스화 해서 코드 블럭이 중첩되지 않도록 해야 한다.

비동기 함수를 프로미스의 콜백(resolve, reject) 처럼 만들어 .then이나 .catch 메소드 체이닝을 사용할 수 있도록(thenable) 하는 것을 Promisifying이라고 한다.

const getPosition = () => {
  return new Promise((resolve, reject) => {
    navigator.geolocation.getCurrentPosition(resolve, reject);
  });
};

getCurrentPosition() 메소드 또한 fulfill callbackreject 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

🙏🏻 잘못된 정보가 있다면 지적해주세요

profile
© 가치 지향 프론트엔드 개발자

5개의 댓글

comment-user-thumbnail
2021년 9월 20일

크... 이렇게 실전지향(?)적인 모습 항상 멋있게 생각합니다.

1개의 답글
comment-user-thumbnail
2021년 9월 23일

오늘도 잘 읽고 갑니다! 재밌으면서도 배울 것 많은 주제를 잘 선정하셔서 주제 선정 과정이 궁금하네요. 그건 그렇고 마포구면 이웃 주민이셨네요.

1개의 답글
comment-user-thumbnail
2021년 10월 6일

이원국님 연락이 두절 되셔서요.
다자인 파일 누락된 것 보내주신다고 했는데 몇달간 연락이 안되셔서 댓글 남깁니다 연락주세요.

답글 달기