프로젝트를 진행하던 중 페이지에 구글 지도를 띄워야 하는 일이 생겼다. React를 쓰며 구글 지도를 띄우는 것은 처음이라 이참에 정리해보고자 하였다.
구글 개발자 콘솔에 가서 google maps javascript를 활성화 한 후, 개인 map API Key를 발급받자.
https://developers.google.com/maps/documentation/javascript?hl=ko
키를 발급받는 방법은 위 문서를 참고하자.
(다른 블로그에도 잘 정리되어 있어서 참고하자! 귀찮아서 그러는거 아님..)
npm install @types/googlemaps -D
@react-google-maps/api가 더 다운로드 수가 많아 원래 이걸 사용하려고 했다. 그런데 실제로 사용할 때 geocoding을 위해 발급받은 map API Key를 넣으니 인가되지 않은 API Key라고 떠서 좀 옛날 라이브러리인 googlemaps를 사용했다.
이유는 나중에 찾아 다시 포스팅을 해야겠다..
전체 코드는 다음과 같다.
import { setLatitude, setLongitude } from '@/atom/selector';
import { Box } from '@chakra-ui/react';
import React, { useEffect, useRef } from 'react';
import { useRecoilState } from 'recoil';
declare global {
interface Window {
google: any;
initMap: () => void;
}
}
const loadMapScript = () => {
return new Promise<void>((resolve) => {
const script = document.createElement('script');
script.src = `https://maps.googleapis.com/maps/api/js?key=${process.env.NEXT_PUBLIC_GOOGLE_MAP_API_KEY}&callback=initMap`;
script.defer = true;
document.head.appendChild(script);
script.onload = () => {
resolve();
};
});
};
function Maps() {
const mapRef = useRef<HTMLDivElement>(null);
const [lat, setLat] = useRecoilState(setLatitude);
const [lng, setLng] = useRecoilState(setLongitude);
const loadMap = async () => {
await loadMapScript();
};
window.initMap = () => {
// 지도 부르기
if (mapRef.current) {
const map = new window.google.maps.Map(mapRef.current, {
center: {
lat: lat || 37.5,
lng: lng || 126.97,
},
zoom: 14,
});
const marker = new window.google.maps.Marker({
position: {
lat: lat || 37.5,
lng: lng || 126.97,
},
map,
});
}
};
useEffect(() => {
loadMap();
}, []);
return (
<Box w="487px" h="280px" mt="24px">
<div style={{ width: '100%', height: '100%' }} ref={mapRef} />
</Box>
);
}
export default Maps;
하나씩 살펴보자.
declare global {
interface Window {
google: any;
initMap: () => void;
}
}
window 객체에 google과 initMap() 속성을 추가한다. 구글 지도는 브라우저에서 로드가 되고, window 객체는 브라우저 환경에서 전역 객체로 사용되는 객체인데 window에는 google과 initMap() 속성이 없어 typescript에게 알려줘야 한다.
참고로 tsx 파일에 declare global로 interface를 선언하면 이 파일 내에서만 window.google, window.initMap()을 사용할 수 있다.
속성을 추가하면 각 속성은 다음과 같은 역할을 한다.
const loadMapScript = () => {
return new Promise<void>((resolve) => {
const script = document.createElement('script');
script.src = `https://maps.googleapis.com/maps/api/js?key=${process.env.NEXT_PUBLIC_GOOGLE_MAP_API_KEY}&callback=initMap`;
script.defer = true;
document.head.appendChild(script);
script.onload = () => {
resolve();
};
});
};
구글 지도를 나타내는 것은 외부 js 파일을 url로 가져오는 것이다. 그런데 가져오기 전에 구글 지도 객체에 접근하고 초기화할 수 없기때문에 로드되기 전까지 기다리도록 Promise를 이용한다.
script.src로 외부 js 파일의 경로를 설정하고, 페이지가 모두 로드된 이후 외부 js 파일을 가져오도록 src.defer = true로 설정한다. 그 후 head 태그에 이 script을 덧붙인다.
script가 로드되면 resolve()를 통해 작업이 완료되었음을 알린다.
resolve에 매개변수가 없으면 단순히 작업이 완료되었음을 알리는 것이고, resolve에 매개변수를 넘겨주면 작업이 완료되면 넘긴 매개변수가 반환된다.
function Maps() {
const mapRef = useRef<HTMLDivElement>(null);
const [lat, setLat] = useRecoilState(setLatitude);
const [lng, setLng] = useRecoilState(setLongitude);
const loadMap = async () => {
await loadMapScript();
};
window.initMap = () => {
// 지도 부르기
if (mapRef.current) {
const map = new window.google.maps.Map(mapRef.current, {
center: {
lat: lat || 37.5,
lng: lng || 126.97,
},
zoom: 14,
});
const marker = new window.google.maps.Marker({
position: {
lat: lat || 37.5,
lng: lng || 126.97,
},
map,
});
}
};
useEffect(() => {
loadMap();
}, []);
return (
<Box w="487px" h="280px" mt="24px">
<div style={{ width: '100%', height: '100%' }} ref={mapRef} />
</Box>
);
}
export default Maps;
html div element에 직접 접근해서 DOM을 조작하며 구글 지도를 추가하기 위해 useRef를 이용한다. mapRef.current로 직접 DOM에 접근할 수 있다.
지도를 초기화하기 전 구글 지도를 불러와야 하므로 async-await으로 불러온 후 초기화한다.
나머지는 공식 문서를 따랐다. 지도를 생성하고 마커를 추가한다.
페이지가 최초로 로드될 때 지도를 한 번만 불러오면 되므로 useEffect(() => {}, [])로 loadMap()을 호출한다.
나는 현재 위치만 마커에 표시하면 되어서 마커를 이동하는 것은 구현하지 않았지만, 필요하다면 다음과 같이 할 수 있다.
const marker = new window.google.maps.Marker({
position: {
lat: lat || 37.5,
lng: lng || 126.97,
},
map,
});
map.addListener("center_changed", throttle(() => {
const changeLat = map.getCenter().lat();
const changeLng = map.getCenter().lng();
marker.setPosition({lat: changeLat, lng: changeLng});
// recoil atom state 업데이트
setLat(changeLat);
setLng(changeLng);
}, 100);
마커를 움직일 때마다 계속 업데이트 되는 것은 불필요하므로 이벤트가 계속 호출되어도 100ms 간격으로만 호출되게 throttle()을 사용하였다.
오랜만에 Promise 객체를 생성하다 보니 기억이 가물가물했다. 구글 지도를 사용하는 방법과 Promise에 대해 다시한 번 복습하는 좋은 기회였는듯
끗!
https://www.japaaan.com/user/39691
https://hanson.net/users/viwadoll