오늘(사실 어제)은 해커톤 프로젝트의 워크플로우와 기능 명세서를 보면서 데이터 모델링을 하고, 로직, 자원 이름을 생각해보며 어떻게 백엔드를 구성할 지 생각해보았다. 데이터 모델링을 하다 보니, 속성을 어떻게 구성해야 할까라는 고민이 생겼다. 나는 프론트 지식이 별로 없는 백엔드지만, 그래도 백엔드를 설계할 때 최대한 프론트와의 연계를 생각하면서 설계하려고 하는 편이다. 이번 프로젝트에서는 프로젝트의 특성 상 카카오맵 API를 이용할 예정이다. 그렇다 보니, 속성을 어떻게 구성해야 프론트가 백엔드의 데이터를 바탕으로 카카오맵 API를 이용할 때 원하는 정보를 잘 얻을 수 있을까? 라는 궁금증이 생긴 것이다.
그래서 나는 카카오맵 API를 직접 써보며 질문에 대한 답을 찾아보기로 했다.
일단 리액트 프로젝트를 만들자.
$ npm create vite@latest 프로젝트명 -- --template react
만들어진 프로젝트 폴더명에 가서 아래의 명령을 입력해주자.
$ npm install
개발 서버를 실행해서 잘 만들어졌는지 확인해보자.
$ npm run dev
일단 카카오 디벨로퍼스에 접속하여 카카오 계정으로 로그인한다.

상단의 앱을 클릭하면 전체 앱이 뜨는데, 나는 이미 테스트를 해보며 앱을 하나 생성해 둔 상태이다. 앱이 없다면 앱 생성을 클릭해서 앱을 생성해준다.
해당 앱을 클릭해서 앱의 대시보드로 이동한다.

좌측의 앱 설정에서 앱을 클릭하고 일반을 클릭한다.

밑으로 내리면 이렇게 앱 키라는 것이 있을 것이다. 이번 실습에서는 자바스크립트 키를 이용하게 된다.
그리고 한 가지 세팅을 더 해야 한다. 테스트에서 사용될 주소(http://localhost:5173)를 플랫폼에 있는 웹 부분의 사이트 도메인에 추가해주는 작업이 필요하다. 사이트 도메인에 추가되어 있는 주소에서만 API를 사용할 수 있기 때문이다.

해당 주소를 추가한 후, 주소가 잘 등록되어 있는지 확인한다. 이제 API를 쓸 준비는 끝났다.
일단 카카오맵 관련 스크립트를 삽입한다.
<script type="text/javascript" src="//dapi.kakao.com/v2/maps/sdk.js?appkey=자바스크립트 키"></script>
이 스크립트에 카카오맵 관련 기능이 담겨있다.
index.html
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite + React</title>
</head>
<body>
<script></script>
<div id="root"></div>
<script type="text/javascript" src="//dapi.kakao.com/v2/maps/sdk.js?appkey=자바스크립트 키"></script>
<script type="module" src="/src/main.jsx"></script>
</body>
</html>
index.html에서 모듈 스크립트 바로 위에 스크립트를 삽입해 주었다. 하지만 이렇게 할 경우 지도가 굳이 필요없는 상황에서도 스크립트를 로딩하게 되는데, 이것에 대한 해결법은 나중에 생각해 보기로..
그리고 같이 TIL 진행하는 프론트 분이 있는데, 그분도 카카오맵을 다루신 적이 있다. 그분은 앱 키까지 환경변수로 따로 떼어서 분리해내셨지만 나는 일단 귀찮아서 주소에 그대로 넣어두기로..
이제 스크립트를 삽입했으니 지도를 삽입해 보자. 일단 지도를 삽입할 컴포넌트를 하나 만들어 준다.
App.jsx
function App() {
return (
<div id='map' />
);
}
export default App;
공식 문서에서는 아래와 같이 지도를 삽입하라고 적혀 있다.
var container = document.getElementById('map'); //지도를 담을 영역의 DOM 레퍼런스
var options = { //지도를 생성할 때 필요한 기본 옵션
center: new kakao.maps.LatLng(33.450701, 126.570667), //지도의 중심좌표.
level: 3 //지도의 레벨(확대, 축소 정도)
};
var map = new kakao.maps.Map(container, options); //지도 생성 및 객체 리턴
그런데 이건 바닐라 JS의 코드잖아? 리액트에선 어떻게 해야 할까?
App.jsx
import { useEfect, useRef } from 'react';
function App() {
const mapRef = useRef(null);
useEffect(() => {
const container = mapRef.current;
const options = {
center: new kakao.maps.LatLng(33.450701, 126.570667),
level: 3
};
const map = new kakao.maps.Map(container, options);
}, []);
return (
<div id='map' style={{width: '80%', height: '700px'}} ref={mapRef} />
);
}
export default App;
useEffect의 setup 인자로 전달하면 된다. 그리고 useEffect의 두 번째 인자로 빈 배열을 주면, 컴포넌트가 마운트될 때에만 실행된다. 처음에 딱 한 번 실행하고 싶을 때 쓰는 느낌?useRef를 쓰는 것을 권장한다고 한다. document.getElementById는 실제 DOM을 조작한다. 하지만, 리액트는 가상 DOM을 이용하므로 가급적 사용을 자제하라고 한다. useRef를 사용해도 지도가 잘 삽입된다.width와 height를 정해주지 않으면, 지도가 로딩되더라도 화면에 나타나지 않는다.var를 const로 바꿨다. 재할당될 일이 없는 친구들이므로..이제 잘 작동하는지 한 번 테스트해보자.
$ npm run dev

잘 나타나고 있다.
이제 이 지도를 바탕으로 몇 가지 실습을 해 볼 것이다.
자바스크립트에서는 사용자의 동의 하에 현재 위치를 가져올 수 있는 Geolocation API를 제공한다. 이 API에서 Geolocation.getCurrentPosition 메소드를 이용하여 사용자의 현재 위치를 가져올 수 있다.
이 API를 이용해서 현재 위치를 가져온 후, 지도가 로딩될 때 중심 좌표를 현재 위치로 만들어 볼 것이다.
먼저, 지도의 중심 좌표가 어떻게 되어 있는지 보자.
const container = mapRef.current;
const options = {
center: new kakao.maps.LatLng(33.450701, 126.570667),
level: 3
};
const map = new kakao.maps.Map(container, options);
지도를 삽입하던 부분의 코드이다. 지도를 삽입할 때, options 인자로 객체를 전달한다. 전달된 객체의 center 속성의 kakao.maps.LatLng 객체의 정보(위도와 경도)가 지도의 중심 좌표를 결정한다.
즉, 현재 위치를 받아서 현재 위치의 위도 및 경도를 kakao.maps.LatLng를 생성할 때 전달하고, 그 LatLng 객체를 지도의 중심 좌표로 설정하여 지도를 생성하면 되는 것이다. 코드로 만들어 보자.
일단, 현재 위치를 가져와서 위도와 경도를 반환해주는 함수를 만들었다.
utils/geolocation.js
const getCurrentLatLng = () => {
return new Promise((resolve, reject) => {
navigator.geolocation.getCurrentPosition((pos) => {
const crd = pos.coords;
const latLng = {lat: crd.latitude, lng: crd.longitude};
resolve(latLng);
});
})
};
export default getCurrentLatLng;
getCurrentPosition 메소드는 비동기적으로 작동하여 사용자의 위치를 얻은 후 콜백 함수가 실행된다. 현재 위치를 가져온 후 이를 중심 좌표로 지정하기 위해서는 getCurrentPosition이 끝난 후에 다음 작업을 진행해야 오류가 발생하지 않는다.getCurrentPosition은 Promise를 반환하지 않으므로 then을 사용할 수 없다. 그래서, then을 사용할 수 있게 getCurrentPosition을 Promise로 감싸 주었다.이제 이 getCurrentPosition 함수를 호출하고, Promise가 이행된 후 중앙 좌표가 현재 위치의 좌표인 지도를 생성해서 삽입한다.
App.jsx
import { useEffect, useRef } from 'react';
import getCurrentLatLng from './utils/geolocation';
function App() {
const mapRef = useRef();
useEffect(() => {
getCurrentLatLng().then((latLng) => {
const container = mapRef.current;
const options = {
center: new kakao.maps.LatLng(latLng.lat, latLng.lng),
level: 3
};
return new kakao.maps.Map(container, options);
})
}, []);
return (
<div id='map' style={{width: '80%', height: '700px'}} ref={mapRef} />
);
}
export default App;
접속해보면, 브라우저가 현재 위치를 요청한 후, 요청을 수락하면 현재 위치가 지도의 중심인 지도를 볼 수 있을 것이다.
잠깐, 마커가 무엇인가?

마커는 여기 A처럼, 지도 위에 찍히는 핀 모양의 표식을 뜻한다.
카카오맵 API에는 지명을 바탕으로 좌표를 얻는 기능이 있다. 그리고 그 좌표를 바탕으로 마커를 만들어 주는 기능이 있다.
먼저, 서울역의 지명은 서울 중구 한강대로 405이다. 카카오맵에서 이 지명을 그대로 검색해보자.

지명에 해당하는 곳에 마커가 찍힌 것을 볼 수 있다. 이 마커의 위치를 기억해 두자.
지명을 좌표로 변환하려면 kakao.maps.services.Geocoder가 필요하다. 그런데, 이 Geocoder를 사용하려면 한 가지 작업을 해 주어야 한다. 아까 스크립트를 삽입한 index.html로 가서 src 부분을 수정해주자.
<script type="text/javascript" src="//dapi.kakao.com/v2/maps/sdk.js?appkey=자바스크립트 키&libraries=services"></script>
아까의 주소에서 끝에 &libaries=services를 추가해준다. Geocoder 객체는 services라는 라이브러리에 속해 있는데, 이 라이브러리는 기본으로 로딩되지 않기 때문에 Geocoder를 사용하려면 반드시 services 라이브러리를 불러와야 한다.
주소를 좌표를 변환하려면 addressSearch 메소드를 사용하면 된다. 한 번 주소를 좌표로 변환해보자. 아까 부분에서 다시 then 메소드를 사용해서 진행한다.
App.jsx
// ...
}).then((map) => {
const geocoder = new kakao.maps.services.Geocoder();
geocoder.addressSearch('서울 중구 한강대로 405', (result, status) => {
if (status === kakao.maps.services.Status.OK) {
console.log(result);
}
});
});
현재 위치를 바탕으로 지도 중심을 설정하는 부분은 잘라냈다.
Geocoder 객체의 인스턴스를 만들어서 geocoder 변수에 저장했다.addressSearch 메소드는 키워드, 콜백 함수, 옵션을 인자로 받는데, 옵션을 주지 않아도 동작한다.result, status, pagination을 인자로 하는데, status와 pagination을 생략해도 동작한다. 하지만 혹시 모르니까 status를 생략하는 일은 하지 말자.
콘솔에 이렇게 검색 결과가 출력되는 것을 볼 수 있다. 두 번 출력되는 것은 리액트의 Strict 모드 때문이다.

정보를 펼쳐보면 이렇게 나오는데, 여기에서 x와 y가 각각 경도, 위도를 의미한다. 이 정보를 가지고 지도에 마커를 찍으면 되는 것이다.
마커는 kakao.maps.Marker를 생성해서 만들 수 있다. 그러면 이제 마커를 찍어보자.
App.jsx
// ...
}).then((map) => {
const geocoder = new kakao.maps.services.Geocoder();
geocoder.addressSearch('서울 중구 한강대로 405', (result, status) => {
if (status === kakao.maps.services.Status.OK) {
const coords = new kakao.maps.LatLng(result[0].y, result[0].x)
const marker = new kakao.maps.Marker({
map: map,
position: coords
});
}
});
});
Marker는 생성 인자로 옵션이 들어 있는 객체를 받는다. 객체에 지도 객체 map, 좌표 객체 position을 넣고 그 객체를 전달해서 Marker를 생성하면 해당 지도의 해당 좌표 위에 마커가 찍힌다.
이렇게 서울 중구 한강대로 405 위치에 마커가 찍힌 것을 볼 수 있다.

실제 카카오맵에서 주소를 검색해서 찍힌 위치와 비교해 봤다. 약간의 오차가 있긴 해도 거의 비슷하게 나오는 것을 볼 수 있다!
이번에는 서울역이라는 키워드로 검색해서, 그 검색 결과가 나타내는 좌표에 마커를 만들어 볼 것이다.
장소 검색에는 kakao.map.services.Places 객체를 사용한다. 한 번 서울역을 검색해서 첫 번째 결과의 정보를 콘솔에 출력해 보자.
App.jsx
// ...
}).then((map) => {
// ...
const ps = new kakao.maps.services.Places();
ps.keywordSearch('서울역', (result, status) => {
if (status === kakao.maps.services.Status.OK) {
console.log(result[0]);
}
});
});

서울역에 대한 정보를 볼 수 있다. 이번에도 역시 x와 y를 이용해서 마커를 찍으면 된다.
App.jsx
// ...
}).then((map) => {
// ...
const ps = new kakao.maps.services.Places();
ps.keywordSearch('서울역', (result, status) => {
if (status === kakao.maps.services.Status.OK) {
const coords = new kakao.maps.LatLng(result[0].y, result[0].x)
const marker = new kakao.maps.Marker({
map: map,
position: coords
});
});
});
});

서울역에 생긴 마커가 새로 생긴 마커이다.
이렇게 해서 3가지 실습을 해보았다. 그런데, 사실 이미 어떤 능력자가 리액트에서 카카오 맵을 편하게 사용할 수 있도록 패키지를 만들어 두었다! react-kakao-maps-sdk는 리액트에서 카카오맵 API를 편리하게 이용할 수 있도록 해주는 패키지이다. 이번엔 이것을 이용해서 지도와 마커를 만들어보고, 패키지의 편리함을 느껴보도록 하자.
$ npm install react-kakao-maps-sdk
App.jsx
import { Map } from 'react-kakao-maps-sdk';
function App() {
return (
<Map
center={{lat: 33.450701, lng: 126.570667}}
style={{width: '80%', height: '700px'}}
level={3}
/>
);
}
export default App;
지도 삽입을 위해 노드 지정하고, 거기에 옵션 만들고, 지도 객체 생성까지 해야 했던 아까를 생각하면 훨씬 간단하고 편리해졌음을 느낄 수 있다! react-kakao-maps-sdk에서 Map 컴포넌트 하나만 불러오면 끝난다!
App.jsx
import { Map, MapMarker } from 'react-kakao-maps-sdk';
function App() {
return (
<Map
center={{lat: 33.450701, lng: 126.570667}}
style={{width: '80%', height: '700px'}}
level={3}
>
<MapMarker
position={{lat: 33.450701, lng: 126.570667}}
/>
</Map>
);
}
export default App;
마커도 MapMarker 컴포넌트로 구현이 되어 있어서, 그 컴포넌트를 Map 안에 넣어주기만 하면 된다! 이 얼마나 편리한가.
사실 이 패키지는 위에서 언급한 그 프론트 분이 알려주셨다. 이 패키지를 알려주신 그분께 다시 한 번 감사를..
이렇게 해서 오늘은 카카오맵 API를 사용해보고, 리액트에서 사용할 수 있는 패키지 react-kakao-maps-sdk에 대해서 알아보았다. 새벽 5시가 다 되어 글을 마무리하다니..
아마 이렇게 생각할 수도 있을 것이다.
하던 것이나 잘하지, 왜 프론트를 찍먹하는 거냐? 프론트가 만만하냐?
그렇지만 나는 프론트가 만만해서 프론트를 찍먹하고 있는 건 절대 아니라고 말하고 싶다. 프론트도 프론트 나름대로의 고충이 있다는 사실을 나도 잘 알고 있다.
내가 프론트를 찍먹하고 있는 이유는 두 가지이다. 먼저, 나는 토이 프로젝트를 만들어 사람들에게 배포해보고 싶다. 그런데, 사람들이 백엔드 서버에 직접 요청을 보내게 할 수는 없지 않는가? 그렇다고 프로젝트를 위해 프론트 개발자를 따로 구하자니 그것도 쉽진 않을 것 같다. 그래서 나는 그냥 내가 프론트를 공부해서 프론트와 백을 모두 만들어야겠다는 생각을 했다.
그리고 프론트를 찍먹하는 것은 프론트와의 협업에도 도움이 될 것이라고 생각했다. 프론트에서 내 데이터가 어떻게 쓰이는지를 알고서 백엔드를 설계하는 것과, 모르고서 백엔드를 설계하는 것은 분명 차이가 있다고 생각한다. 물론 API 문서가 프론트와 백엔드 사이의 간격을 좁혀주긴 하지만, 그래도 역지사지라는 말이 있듯이 프론트의 입장을 경험해본다면 더 좋지 않을까? 라고 생각했다.
오늘 낮에 어떻게 디자인할 것이고 전체적인 플로우를 잡기 위한 회의가 있어서 실습은 이 정도로만 해두고자 한다. 카카오맵 API를 다뤄본 김에, 간단한 시연을 준비해서 회의 때 기획 및 디자이너 분들에게 보여드리면서 백엔드와의 연결까지 같이 설명해 드려봐야겠다.