인터넷에 특정 장소를 검색하면 위와 같이 지도가 뜨는걸 보실 수 있으십니다.
이렇게 지도를 띄워줌으로써 사용자에게 조금 더 좋은 편의성을 제공해줄 수 있습니다.
지도 API는 구글, 네이버, 카카오를 주로 사용합니다.
이번에는 카카오맵 API를 이용하여 지도를 띄워보도록 하겠습니다.
💡 구글 vs 네이버 vs 카카오
지도 API에는 다양한 종류가 있습니다. 그 중 우리나라에서 가장 많이 사용하는 것은 구글, 네이버, 카카오에서 제공하는 지도 API입니다. 우리는 이중 카카오 맵 API를 이용하여 실습을 진행하게 됩니다.
세 가지 API 간에는 제공하는 기능의 종류, 비용 문제 등의 차이가 있습니다.
차후 여러분이 서비스를 직접 개발하게 되면, 이러한 차이점을 고려하여 어떤 API를 사용할 지 선택하시면 됩니다.
구글, 네이버, 카카오 등의 대형 포털은 보통 개발자 사이트를 따로 가지고 있습니다.
카카오도 카카오 개발자 페이지를 운영하고 있습니다.
카카오 개발자 페이지에 접속합니다.
카카오 개발자 페이지에 들어가보시면 카카오 맵 뿐만 아니라 카카오 로그인, 카카오톡 소셜 등 다양한 API가 제공되는 것을 확인할 수 있습니다.
🔔 모든 API가 100% 무료로 제공되는 것은 아닙니다.
일정 사용량을 초과하면 유료로 전환되는 경우도 있으니 API 사용 전에 관련 사항을 꼼꼼히 살펴보세요!
카카오에서 제공하는 개발자 API를 사용하기 위해서는 애플리케이션 추가가 필요합니다.
로그인 후 내 애플리케이션 메뉴에 접속해보겠습니다.
애플리케이션 추가하기 버튼을 클릭하면, 추가할 애플리케이션 정보를 입력하는 모달이 열립니다.
여기에 앱 이름
과 사업자명
을 필수로 입력해주신 후 저장 버튼을 클릭하면 새로운 애플리케이션이 생성된 것을 확인하실 수 있습니다.
💡 애플리케이션 추가는
새로운 프로젝트 폴더를 생성하는 것
과 비슷합니다.
앱 이름과 사업자명은 자유롭게 기입해주세요.
카카오 맵을 구현하기 위해 필요한 카카오 맵 API 를 먼저 받아와 보겠습니다.
카카오 개발자 페이지에서 다음과 같은 경로를 통해 지도 페이지에 접속합니다.
문서 > 지도 항목에서, 좌측 사이드바의 지도
를 클릭하면
카카오에서 제공하고 있는 카카오 지도 API 페이지에 접속할 수 있습니다.
페이지에 접속했다면, 어떤 브라우저에서 카카오 맵 API를 사용할 건지 선택해야 합니다.
우리는 웹 서비스를 개발하는 중이므로 가운데에 있는 Web을 클릭하면 됩니다.
Web 홈페이지에 접속한 후, 왼쪽 하단의 메뉴바에서 키 발급 메뉴를 클릭해서 발급 페이지로 들어갑니다.
키 발급 메뉴에 들어가 보면 아까 생성한 애플리케이션 목록을 확인하실 수 있습니다.
그 중 방금 생성한 애플리케이션을 클릭해봅시다.
그러면 해당 애플리케이션의 앱 키를 목록을 볼 수 있습니다.
여러가지 종류의 앱 키가 생성된 걸 볼 수 있습니다.
각각의 용도에 맞는 키를 사용하면 되는데, 우리는 이 중 JavaScript Key
를 이용할 것입니다.
이제 방금 생성한 JavaScript 앱 키
를 이용해서 내 프로젝트에 카카오 맵을 구현해보도록 하겠습니다.
Kakao maps API 페이지의 Guide 페이지로 이동하면 카카오 맵을 적용하는 방법이 상세하게 정리되어 있습니다
카카오 맵 왼쪽 탭에서 Guide 탭을 클릭해서도 이동 가능합니다.
Guide 페이지로 들어왔다면, 지도를 띄우는 코드 작성 부분에서 카카오 맵을 설정하는 가이드 라인을 찾을 수 있습니다.
다음 코드를 이용하면 프로젝트에 카카오 맵을 적용할 수 있습니다.
위의 가이드 라인은 바닐라 (Original) JavaScript와 HTML 기준으로 만들어진 코드입니다.
React 에서, 특히 Next JS
에서는 HTML에 접근할 수 있는 방법이 한정적
이기 때문에 위의 가이드라인 만으로는 구현할 수 없습니다.
무엇보다 가이드라인에서 사용하고 있는 document 객체는 서버사이드 렌더링을 지원하는 Next.js 에서는 화면을 렌더하기 전까지 생성되지 않은 값 (=undefiend)
을 가집니다.
그렇기 때문에 가이드라인의 코드를 그대로 입력하면 오류가 발생하게 됩니다.
그렇다면, 어떻게 해야 Next.js에서 오류 없이 카카오 맵을 불러올 수 있을까요?
차근차근 Next.js 에서 카카오 맵을 구현해보도록 하겠습니다.
먼저 설정해야 되는 부분이 있습니다.
바로 카카오 맵에서 제공하는 스크립트를 내 프로젝트 안에 적용
시켜줘야 합니다.
아래의 코드를 프로젝트 최상위에 호출해주세요.
import Head from 'next/head';
Head 컴포넌트는 Next.js 에서 기본적으로 제공하는 기능입니다.
HTML 에서 Head 태그 안으로 다른 기능들을 호출해서 사용할 수 있는 것처럼 Next.js 의 Head 컴포넌트를 이용해 동일한 기능을 사용
할 수 있습니다.
Head 컴포넌트를 불러오셨다면, Head 컴포넌트 안에 카카오 맵 스크립트를 호출할 수 있는 코드를 추가해줍니다.
이 때, 미리 만들어둔 JavaScript 앱 키가 필요합니다.
script 태그의 src 속성 값 안의 appkey=
부분에 자신의 계정의 JavaScript App Key 값
을 넣어주시면 됩니다.
return(
<>
<Head>
<script type="text/javascript" src="//dapi.kakao.com/v2/maps/sdk.js?appkey='JavaScript 앱 키 입력'"></script>
</Head>
</>
)
앱 키까지 넣어줬다면 이제 내 프로젝트에서 카카오 맵의 스크립트 파일을 호출할 수 있게 됩니다.
호출한 스크립트과 HTML을 결합해서 화면에 카카오 맵을 출력시켜 볼까요?
카카오 맵을 호출할 수 있는 JS 코드와 카카오 맵을 호출 받아 출력할 영역이 되는 div 태그를 하나 생성해줍니다.
// 지도를 담을 영역의 DOM 레퍼런스
const container = document.getElementById('map');
// 지도를 생성할 때 필요한 기본 옵션
const options = {
center: new kakao.maps.Latlng(33.350701, 126.570667), // 지도의 중심좌표
level: 3 // 지도의 레벨(확대, 축소 정도)
}
// 지도 생성 및 객체 리턴
new kakao.maps.Map(container, options);
return (
<div id='map' style={{'width': '500px', 'height': '400px'}}></div>
)
container
라는 상수 값에 'map' 의 이름을 가지는 id 태그를 가져와 담아준 후,
new kakao.maps.Map
이라는 카카오 스크립트의 기능을 이용해
container 상수에 담아놓은 id 태그에 지도를 생성
해줍니다.
options
상수는 카카오 맵을 호출할 때의 초기값을 설정해주는 역할을 합니다.
위 JavaScript 코드는 'map' 이라는 id 태그에 지도를 생성해주는 역할을 하므로, return 부분에도 'map' 이라는 id를 가지는 div 태그도 하나 생성해주어야 합니다.
document가 정의되지 않았다며 에러가 발생했습니다.
이전에도 설명 드렸다시피, 프론트엔드 서버에서 페이지가 그려지는 시점에는 document 가 undefined 한 값
을 가지고 있기 때문입니다.
이것은 서버사이드 렌더링의 특징 중 하나 입니다.
그러면, document 를 사용하는 시점을 document 가 생성된 시점 이후로 변경
해주는 코드가 필요할 것 같습니다.
useEffect
를 이용해서 페이지가 마운트되고 document 객체가 생성된 이후에 카카오맵을 호출할 수 있도록 변경해줍니다.
그리고 글로벌 스코프에 위치한 kakao라는 객체의 타입은 다음과 같이 지정해줄 수 있습니다.
declare const window: typeof globalThis & {
kakao: any;
}
실제 내 서비스에서 카카오 스크립트가 동작하게 하기 위해서는 개발자 페이지에서 사이트 도메인 등록도 해주어야 합니다.
이런 식으로 제공되는 API들은 대부분 보안을 위해 해당 기능을 끌어다 사용할 도메인을 등록해야만 이용할 수 있는 구조로 되어있습니다.
카카오 API 역시 마찬가지입니다.
키 발급 > 내 애플리케이션 > 플랫폼 페이지에서 Web 사이트 도메인
을 등록해주시면 됩니다.
여기까지 완료했으면 가이드라인에서 지시하는 카카오 맵 출력 방법은 전부 따라한 것입니다.
yarn dev 해서 localhost:3000을 실행한 뒤 해당 경로로 이동해봅시다.
💡 JavaScript API Key와 같은 민감 정보는 github에 올리면 안됩니다.
env
를 이용한 환경변수 설정 등의 방법 등으로 최대한 숨겨주시고, 절대 노출되면 안되는 중요 민감 정보의 경우에는 프론트엔드 서버에 두지 말고 백엔드 서버에 놓고 사용하는 편이 안전합니다.
이번에는, 버튼을 클릭해서 페이지를 이동하는 경우에도 카카오 맵이 정상적으로 출력 되는지 한 번 확인해 보겠습니다.
먼저 페이지를 새로 만들고, 맵으로 이동하기 버튼을 만들어줍니다.
그리고 버튼을 누르면 지도 스크립트가 입력되어있는 페이지로 이동하도록 함수를 연결해줍니다.
import { useRouter } from "next/router";
const KakaoMapRoutingPage = () => {
const router = useRouter();
const onClickMoveToMap = () => {
router.push("/29-03-kakao-map-routed");
};
return (
<div>
<button onClick={onClickMoveToMap}>맵으로 이동하기 !</button>
</div>
);
}
export default KakaoMapRoutingPage;
서버를 실행한 뒤, 버튼을 클릭해서 지도가 있는 페이지로 이동해봅시다.
kakao를 찾을 수 없다며 에러가 뜹니다.
이번에는 router
를 사용하지 않고 a
태그를 이용하여 페이지를 이동해 보겠습니다.
import { useRouter } from "next/router";
const KakaoMapRoutingPage = () => {
// const router = useRouter();
// const onClickMoveToMap = () => {
// router.push("/29-03-kakao-map-routed");
// };
return (
<div>
{/* <button onClick={onClickMoveToMap}>맵으로 이동하기 !</button> */}
<a href="/29-03-kakao-map-routed">맵으로 이동하기 !!</a>
</div>
);
}
export default KakaoMapRoutingPage;
아무런 문제 없이 지도가 출력되는 것을 확인할 수 있습니다.
router
와 a 태그
가 무엇이 다르길래 이러한 차이가 발생하는 걸까요?
앞으로 router.push
대신 a
태그를 사용해야 하는 걸까요?
a
태그를 이용해서 페이지를 이동하는 것과 같은 방식을 채택하는 웹 서비스를 MPA(Multi Page Application)
라고 합니다.
MPA에서 서로 다른 url을 가진 페이지들은 각각 독립적으로 존재합니다.
그렇기 때문에 프론트엔드 서버에서 페이지를 그린 뒤 브라우저로 HTML/CSS/JS를 보내주는 작업을 매 페이지 이동 시마다 거치게 됩니다.
이 경우 주소를 직접 입력해서 들어가는 것과 a태그를 통해 페이지를 이동하는 것은 본질적으로 동일합니다.
하지만 MPA 의 경우, 페이지 이동 시마다 서버에 요청해서 데이터를 받아와야 하기 때문에 성능은 좋지 않습니다.
router
를 이용해서 페이지를 이동하는 것과 같은 방식을 채택하는 웹 서비스를 SPA(Single Page Application)
라고 합니다.
SPA에서는 서비스에 처음 접속할 때 모든 페이지의 데이터를 다 받아옵니다.
그리고 router를 통해 페이지를 이동할 때, 실제로는 페이지의 일부에 해당하는 컴포넌트만 교체한 뒤 페이지를 다시 그려오게(re-rendering) 됩니다.
SPA의 경우 최초 로딩에는 시간이 다소 걸릴 수 있으나 페이지를 이동할 때 걸리는 시간이 MPA에 비하여 압도적으로 짧습니다.
💡 MPA, SPA
MPA는전통적인 의미의 홈페이지
이고,
SPA는 홈페이지보다는애플리케이션
의 느낌이 강합니다.
이제까지 설명드린 내용을 바탕으로 생각해봅시다.
a
태그를 이용하면 오류는 해결되지만, 페이지를 이동했을 때 페이지 자체가 새로 로딩되기 때문에 SPA 프레임워크인 Next.js를 사용하는 의미가 없어집니다.
그렇다면 어떻게 해야 할까요?
Next.js에서 제공하는 Link 태그
를 이용하면 됩니다.
Next.js에서는 Link
라는 태그를 제공합니다.
router
로 연결해두었던 기존 코드를 다음과 같이 변경해주시면 됩니다.
import { useRouter } from "next/router";
import Link from "next/link";
const KakaoMapRoutingPage = () => {
// const router = useRouter();
// const onClickMoveToMap = () => {
// router.push("/29-03-kakao-map-routed");
// };
return (
<div>
{/* <button onClick={onClickMoveToMap}>맵으로 이동하기 !</button> */}
<Link href="/29-03-kakao-map-routed">
<a>맵으로 이동하기 !!</a>
</Link>
</div>
);
}
export default KakaoMapRoutingPage;
Link 안에 a 태그를 넣으면 시맨틱 요소를 가지고 있는 html 태그로 렌더링이 되기 때문에
웹 표준이나 검색 엔진 최적화 차원에서도 이점을 가지고 있습니다.
그렇기 때문에 가능한 부분에서는 가급적 Link 태그를 사용하는 것이 좋습니다.
💡 Next.js 13부터는
a
태그를 넣지않아도Link
가 알아서a
태그로 인식해줍니다.
Head
태그 안에 넣어 놓은 script 태그로 카카오 맵 script를 불러올 경우, script 다운로드와 지도를 로드하는 작업이 동시로 진행되기 때문에, 사용자 PC의 다운로드 속도가 느릴 경우 지도가 정상적으로 보이지 않는 문제가 발생할 수 있습니다.
이러한 경우에는 script 다운로드를 완료하고, 카카오 맵 로드도 완료된 뒤에 지도를 불러오도록 코드를 추가해주면 됩니다.
useEffect(() => {
// 여기서 직접 다운로드 받고, 다 받을때까지 기다렸다가 그려줍니다
const script = document.createElement("script"); // html에 script라는 태그(Element)를 만든다
script.src = "//dapi.kakao.com/v2/maps/sdk.js?appkey='JavaScript API Key'&autoload=false";
document.head.appendChild(script);
script.onload = () => {
window.kakao.maps.load(() => {
const container = document.getElementById("map");
const options = {
center: new window.kakao.maps.LatLng(33.450701, 126.570667),
level: 3,
};
const map = new window.kakao.maps.Map(container, options);
}
}
}, []);
카카오 맵도 웹 에디터와 동일하게 원하는 기능을 추가해서 커스텀 할 수가 있습니다.
📖 카카오맵 API 페이지 - Sample 페이지를 참고해보세요
📖 카카오맵의 지도 생성하기 페이지를 참고보세요
적용한 카카오 맵 페이지를 실행시켜 보면 카카오 본사의 위치가 초기 위치로 잡혀 있습니다.
카카오 맵을 적용한 페이지의 useEffect
부분을 한번 살펴볼까요?
options 라는 이름으로 선언된 객체 안에 center
라는 키 값으로 주소값이 지정되어 있는 부분이 있습니다.
첫번째 인자는 초기 위치에 대한 경도값
, 두번째 인자는 초기 위치에 대한 위도값
을 받아옵니다.
설정된 경도와 위도값에 따라서 처음 카카오 맵을 호출할 때 초기 위치 값을 변경
시킬 수 있습니다.
center
부분을 아래와 같이 수정해 볼까요?
center: new kakao.maps.LatLng(37.4848929702844, 126.89537799629241)
페이지를 새로 고침한 후, 초기 위치 값이 잘 변경되었는지 확인해보세요!
특정 위치의 경도와 위도 값은 구글 지도에서 오른쪽 마우스 클릭으로 쉽게 확인할 수 있습니다.
📖 카카오 맵의 마커 생성하기 페이지를 참고해보세요!
지도에서 표시를 고정하고 싶을 때는 마커를 이용합니다.
우리가 추가했던 카카오 맵에서도 마커 기능을 제공하고 있습니다.
한번, 마커도 추가해볼까요?
일단 추가할 마커의 초기 위치 값을 잡아주겠습니다.
const markerPosition = new kakao.maps.LatLng(37.4848929702844, 126.89537799629241);
지도의 초기 위치 값을 설정해주는 방법과 동일하게 마커에 대한 초기 위치값을 경도, 위도로 설정
해줄 수 있습니다.
카카오 맵을 호출해주는 useEffect 함수 안으로 추가
해주시면 됩니다.
초기 위치 값을 설정했으니, 화면에 출력해줄 마커에 대한 정보를 설정해줍니다.
const marker = new kakao.maps.Marker({
position: markerPosition
});
마커를 생성할 때, 옵션으로 받는 position 에 마커의 위치값
을 넣어주면 설정된 초기 위치 값에 마커를 생성하게 됩니다.
여기까지가 마커에 대한 기본 설정 부분입니다.
아직 화면에 출력 되는 건 아무것도 없습니다.
마커를 생성했으니, 생성한 마커를 화면에 출력 시켜줘야겠죠?
marker.setMap(map);
생성된 마커의 .setMap
메소드를 이용하는데
어떠한 맵에 해당 마커를 생성할 건지 에 대한 지도 정보를 넣어줘야 합니다.
그렇다면 지도에 대한 정보는 어디서 받아올 수 있을까요?
이미 우리는 앞의 과정에서 만들어 놓은 게 있습니다.
kakao.maps.Map
을 이용해서 지도를 담을 영역인 container 와 초기 위치값을 설정하는 options 로 카카오 맵을 화면에 출력했었습니다.
이 정보 값을 map 이라는 상수에 할당 하고 생성할 마커에게 '이 지도에 마커를 생성해 줘'
라는 의미로 map 상수를 setMap
메소드의 인자값으로 넣어줄 수 있습니다.
Callback 함수란 무엇일까요?
함수의 인자로 들어가는 함수를 말합니다.
function aaa(qqq) {
// 함수 로직
}
aaa(function(){})
위와 같은 코드에서, aaa 함수의 인자에 들어가는 function(){}
를 callback 함수라고 부릅니다.
표현식으로 작성된 callback 함수를 화살표 함수로 바꾸면 다음과 같이 표현할 수 있습니다.
aaa(() => {})
그렇다면, 이러한 callback 함수를 왜 사용하는 걸까요?
특정한 API 요청이 끝난 뒤, 그 결과 값을 가지고 다른 요청을 실행시켜야 하는 상황을 가정해봅시다.
그럴 때 이런 식으로 callback 함수를 사용해서 요청을 실행할 수 있습니다.
function aaa(qqq) {
// 외부 API에 데이터 요청하는 로직
// ...
// ...
// 요청 끝!
const result = "요청으로 받아온 데이터 결과값"
qqq(result) // 요청 끝나면 qqq 실행시키기
}
aaa(result) => {
console.log("요청이 끝났습니다.")
console.log("요청으로 받아온 데이터는" + result + "입니다")
}
async/await
나 promise
문법이 아직 존재하지 않았던 시기에는 callback 함수를 이용해 데이터를 요청하고 처리했습니다.
우리는 이미 async/await
를 익숙하게 사용하고 있지만, 자바스크립트 비동기 처리의 발전 과정을 이해하고 사용하는 것 또한 중요합니다.
callback 실습부터 시작해서 다양한 자바스크립트 비동기 처리 방법을 한 번 살펴봅시다.
다음과 같이 callback 함수를 한 번 만들어 보겠습니다.
function aaa(cb) {
console.log("로직 1번 실행됨!!")
console.log("로직 2번 실행됨!!")
console.log("로직 3번 실행됨!!")
cb()
}
function myfunction() {
console.log("모두 끝냈다!!")
}
aaa(myfunction)
그리고 콘솔창에서 코드를 실행하면 다음과 같이 로그가 출력됩니다.
위와 같은 기본 구조를 기억한 상태에서, 실무에서 callback 함수를 어떻게 사용하는지 실습해볼까요?
💡 이번 차시에는 다음 두 가지 API를 활용할 것입니다.
랜덤 수 생성 API : http://numbersapi.com/random?min=1&max=200
Korean JSON : https://koreanjson.com/
class 폴더에 새로운 페이지를 만들고 다음과 같은 코드를 작성합니다.
const CallbackPromiseAsyncAwaitPage = () => {
const onClickCallback = () => {
};
const onClickPromise = () => {
};
const onClickAsyncAwait = () => {
};
return (
<div>
<button onClick={onClickCallback}>Callback 요청하기</button>
<button onClick={onClickPromise}>Promise 요청하기</button>
<button onClick={onClickAsyncAwait}>AsyncAwait 요청하기</button>
</div>
);
}
export default CallbackPromiseAsyncAwaitPage;
먼저 Callback 함수부터 만들어볼까요?
Callback함수 내부에서는 XMLHttpRequest()
객체를 이용해서 데이터를 읽어올 수 있습니다.
💡 XMLHttpRequest 란?
XMLHttpRequest는서버와 상호 작용하기 위해 사용되는 객체
입니다.
전체 페이지의 새로고침 없이도 URL로부터 데이터를 받아올 수 있습니다.
Ajax 프로그래밍에 주로 사용되며, XHR이라고 줄여 부르기도 합니다.
MDN : https://developer.mozilla.org/ko/docs/Web/API/XMLHttpRequest
XHR을 이용해서 랜덤 수 생성 API를 요청해봅시다.
그리고 해당 요청에 대한 응답을 res라는 변수에 담아 콘솔에 찍어볼까요?
const onClickCallback = () => {
const aaa = new XMLHttpRequest();
aaa.open("get", "http://numbersapi.com/random?min=1&max=200");
aaa.send();
aaa.addEventListener("load", (res: any) => {
console.log(res)
});
};
이 중 우리에게 필요한 데이터는 target
안에 들어있습니다.
target
객체를 열어보면, 다음과 같이 response
가 들어온 것을 확인할 수 있습니다.
이 response를 가공해서 두 번째 API 요청을 보내줍니다.
다음과 같이 코드를 가공한 뒤 요청 버튼을 눌러볼까요?
const onClickCallback = () => {
const aaa = new XMLHttpRequest();
aaa.open("get", "http://numbersapi.com/random?min=1&max=200");
aaa.send();
aaa.addEventListener("load", (res: any) => {
const num = res.target.response.split(" ")[0]; // 랜덤 수 추출
const bbb = new XMLHttpRequest();
bbb.open("get", `https://koreanjson.com/posts/${num}`);
bbb.send();
bbb.addEventListener("load", (res: any) => {
console.log(res)
});
});
};
랜덤 수에 해당하는 게시글 데이터가 response에 들어온 것을 확인하실 수 있습니다.
하지만 응답받은 데이터는 문자열로 되어있기 때문에 가공하거나 활용하기 어렵습니다.
JSON.parse()
를 이용해 문자열을 객체로 바꿔주고, 해당 데이터의 작성자 정보를 이용해서 세 번째 요청을 보내줍시다.
const onClickCallback = () => {
const aaa = new XMLHttpRequest();
aaa.open("get", "http://numbersapi.com/random?min=1&max=200");
aaa.send();
aaa.addEventListener("load", (res: any) => {
const num = res.target.response.split(" ")[0];
const bbb = new XMLHttpRequest();
bbb.open("get", `https://koreanjson.com/posts/${num}`);
bbb.send();
bbb.addEventListener("load", (res: any) => {
const userId = JSON.parse(res.target.response).UserId;
const ccc = new XMLHttpRequest();
ccc.open("get", `https://koreanjson.com/posts?userId=${userId}`);
ccc.send();
ccc.addEventListener("load", (res: any) => {
console.log(res)
console.log("최종 결과값 !!!!!!!!!");
console.log(JSON.parse(res.target.response));
});
});
});
};
두 번째 요청 때와 마찬가지로 JSON.parse()
를 이용해 문자열을 객체로 바꿔줍니다.
그렇게 만들어낸 최종 결과값을 콘솔에 찍어봅시다.
그렇게 완성된 최종 코드의 모양을 살펴봅시다.
요청이 반복될수록 대각선으로 쑥 파이는 형태가 됩니다.
지금은 총 3회의 요청만 들어갔지만, API 요청이 2~3번 정도만 더 중첩되어도 코드의 가독성이 심각하게 떨어지게 됩니다.
이런 현상을 콜백 지옥 (Callback Hell)이라고 부릅니다.
콜백 지옥과 같은 현상을 막고자 나온 것이 바로 Promise
입니다.
이제 똑같은 작업을 Promise 객체를 이용해 진행해 보겠습니다.
promise 실습을 진행할 때에는 axios를 사용할 것입니다.
💡 promise 실습을 한다면서 왜 axios를 사용하나요?
promise란자바스크립트의 비동기 처리
, 그 중에서도 특히 외부에서 많은 양의 데이터를 불러오는 작업에 사용되는 객체입니다. 이런promise 객체를 이용해서 만든 라이브러리가 axios
입니다. 그렇기 때문에 우리는 promise 실습에 axios를 사용하려고 합니다.
axios.get()
위에 마우스를 올려보면
요청에 대한 반환 값이 Promise로 들어오는 것을 확인하실 수 있습니다.
axios 뿐만 아니라 데이터 통신에 사용되는 현대 라이브러리들은 대부분 Promise를 기반으로 만들어져 있습니다. 또한 Promise 객체에는 async/await
를 붙이는 것이 가능합니다.
axios를 사용하지 않고 Promise 객체를 직접 사용하고자 할 때에는 다음과 같이 할 수 있습니다.
const result = await new Promise((resolve, reject) => {
const aaa = new XMLHttpRequest();
aaa.open("get", "http://numbersapi.com/random?min=1&max=200");
aaa.send();
aaa.addEventListener("load", (res: any) => {
resolve(res)
});
})
// resolve : 요청이 성공했을 경우
// reject : 요청이 실패했을 경우
그렇다면 async/await가 아직 만들어지기 전에는 어떻게 Promise를 사용했을까요?
promise
객체에 제공되는 .then
이라는 기능을 사용했습니다.
axios
와 .then
을 이용해 순수 promise 객체 만으로 데이터를 요청하는 경우를 확인해봅시다.
이번에는 Callback Hell같은 지옥이 일어나지 않을까요?
const onClickPromise = () => {
axios
.get("http://numbersapi.com/random?min=1&max=200")
.then((res) => {
const num = res.data.split(" ")[0];
return axios.get(`https://koreanjson.com/posts/${num}`);
})
.then((res) => {
const userId = res.data.UserId;
// prettier-ignore
return axios.get(`https://koreanjson.com/posts?userId=${userId}`)
})
.then((res) => {
console.log(res.data);
});
};
callback에 비해 코드가 간단해진 것을 확인할 수 있습니다.
또한 콜백 지옥과 같은 현상도 일어나지 않습니다.
promise를 사용할 경우 각 요청들이 체인처럼 연결되는데, 이러한 것을 프로미스 체인(Promise chain) 또는 프로미스 체이닝(Promise chaining)이라고 부릅니다.
그런데 Promise에도 문제가 있습니다.
콜백 지옥은 해결했지만, 직관적이지 못합니다.
다음과 같이 호출에 대한 결과값을 상수에 담아 사용하는 것이 불가능합니다.
const onClickPromise = () => {
const result = axios
.get("http://numbersapi.com/random?min=1&max=200")
.then((res) => {
const num = res.data.split(" ")[0];
return axios.get(`https://koreanjson.com/posts/${num}`);
})
.then((res) => {
const userId = res.data.UserId;
// prettier-ignore
return axios.get(`https://koreanjson.com/posts?userId=${userId}`)
})
.then((res) => {
console.log(res.data);
});
console.log(result)
};
위와 같은 함수를 실행하면 결과 값이 완전한 데이터가 아니라 Promise의 형태로 들어오는 것을 확인하실 수 있습니다.
코드의 실행 순서가 직관적이지 못합니다.
다음과 같이 콘솔을 찍고 함수를 실행해볼까요?
const onClickPromise = () => {
console.log("여기는 1번입니다~");
axios
.get("http://numbersapi.com/random?min=1&max=200")
.then((res) => {
console.log("여기는 2번입니다~");
const num = res.data.split(" ")[0];
return axios.get(`https://koreanjson.com/posts/${num}`);
})
.then((res) => {
console.log("여기는 3번입니다~");
const userId = res.data.UserId;
// prettier-ignore
return axios.get(`https://koreanjson.com/posts?userId=${userId}`)
})
.then((res) => {
console.log("여기는 4번입니다~");
console.log(res.data);
});
console.log("여기는 5번입니다~");
};
위와 같은 순서로 콘솔이 출력되는 것을 확인하실 수 있습니다.
이것은 axios를 이용한 비동기 작업이 TaskQueue 안에 들어가, 실행 순서가 뒤로 밀렸기 때문입니다.
그렇다면 이런 문제를 어떻게 해결할 수 있을까요
async/await를 이용하면 코드가 직관적이고 심플해집니다.
const onClickAsyncAwait = async () => {
console.log("여기는 1번입니다~");
// prettier-ignore
const res1 = await axios.get("http://numbersapi.com/random?min=1&max=200");
const num = res1.data.split(" ")[0];
console.log("여기는 2번입니다~");
const res2 = await axios.get(`https://koreanjson.com/posts/${num}`);
const userId = res2.data.UserId;
console.log("여기는 3번입니다~");
// prettier-ignore
const res3 = await axios.get(`https://koreanjson.com/posts?userId=${userId}`)
console.log(res3.data);
console.log("여기는 4번입니다~");
};
💡 async / await는 promise에만 붙일 수 있습니다.
axios가 promise 객체를 이용해 만들어진 라이브러리이기 때문에
async/await를 사용할 수 있는 것입니다
Promise 실습을 하면서, Promise로 실행하는 함수는 태스크 큐에 들어간다는 이야기를 했습니다.
태스크 큐는 딱 하나만 있는 것이 아니라, 여러 개가 동시에 존재합니다.
우리는 그 중 대표적인 두 가지 태스크 큐를 살펴보겠습니다.
📖 매크로 태스크 큐(Macro Task Queue)
setTimeOut
,setInterval
등이 들어가는 큐입니다.
📖 마이크로 태스크 큐(Micro Task Queue)
Promise
등이 들어가는 큐입니다.
그렇다면 태스크 큐들 간의 실행 순서는 어떻게 결정되는 걸까요?
매크로 태스크 큐와 마이크로 태스크 큐가 부딪힐 경우에는
마이크로 태스크 큐가 우선순위를 가져옵니다.
Promise
와 setInterval
, setTimeout
를 함께 실행시켜서 확인해볼까요?
const EventLoopWithQueuePage = () => {
const onClickTimer = () => {
console.log("===============시작~~===============");
// 비동기 작업 (매크로큐에 들어감)
setTimeout(() => {
console.log("저는 setTimeout! 매크로 큐!! 0초 뒤에 실행될 거예요!!!");
}, 0);
new Promise((resolve) => {
resolve("철수");
}).then((res) => {
console.log("저는 Promise! - 1!! 마이크로 큐!! 0초 뒤에 실행될 거예요!!!");
});
// 비동기 작업 (매크로큐에 들어감)
setInterval(() => {
console.log("저는 setInterval! 매크로 큐!! 1초 마다 계속 실행될 거예요!!!");
}, 1000);
let sum = 0;
for (let i = 0; i <= 9000000000; i += 1) {
sum = sum + 1;
}
new Promise((resolve) => {
resolve("철수");
}).then((res) => {
console.log("저는 Promise! - 2!! 마이크로 큐!! 0초 뒤에 실행될 거예요!!!");
});
console.log("===============끝~~===============");
};
return <button onClick={onClickTimer}>시작</button>;
}
export default EventLoopWithQueuePage;