이번 나의 산행 기록을 관리하는 프로젝트에서 지도는 필수적으로 요구되는 기능이기에, kakao
에서 제공하는 지도 API
를 활용해 사용하는 과정을 기록합니다.
시작하기에 앞서 저는 프로젝트를 typescript
로 진행하였으며, 카카오에서는 공식적으로 지도 API의 type
을 지원하고 있지 않기에 링크의 오픈소스를 사용하였습니다.
먼저 기본적인 사용법은 카카오 지도의 Docs
에 샘플과 함께 상세히 설명되어 있습니다.
기본적인 Map
의 샘플코드는 아래와 같습니다.
var container = document.getElementById('map'),
options = {
center: new kakao.maps.LatLng(33.450701, 126.570667),
level: 3
};
var map = new kakao.maps.Map(container, options);
위 코드의 경우 React
가 아닌 바닐라 자바스크립트에서 활용하는 방법이기에 React
에서 사용하기 적합하지 않았습니다.
<Map center={{ lat: 30, lng: 30 }} level={3}>
<Marker position={{ lat:30, lng: 30 }}/>
<Polyline paths={[[x, y], [x, y]]}/>
</Map>
위와 같이 컴포넌트 형태로 사용할 수 있으며, center
, level
과 같은 지도의 속성들을 Marker
, Polyline
와 같은 하위 컴포넌트에서 컨트롤할 수 있으면 좋겠다고 생각하였습니다.
const MapContext = createContext<MapContextValue | null>(null);
export const Map = ({ children, ...props }: MapProps & PropsWithChildren) => {
const [map, setMap] = useState<kakao.maps.Map | null>(null);
const [options, _setOptions] = useState<MapProps>({
...props,
smooth: false
});
const containerRef = useRef<HTMLDivElement>(null);
// 복수의 옵션 변경시 사용
const setOption: MapContextController["setOption"] = useCallback(updateOptions => {
_setOptions(prev => ({ ...prev, ...updateOptions }));
}, []);
// ... 생략
// 최초 지도 렌더링
useMountLayout(() => {
if (containerRef.current) {
const _map = new kakao.maps.Map(containerRef.current, {
...props,
center: toCoords(options.center)
});
setMap(_map);
}
});
const values = useMemo(
() => ({
map: map as kakao.maps.Map,
controller: {
setOption,
}
}),
[map, setCenter, setOption, setSmooth]
);
return (
<MapContext.Provider value={values}>
<SizeBox ref={containerRef} fullScreen>
{map && children}
</SizeBox>
</MapContext.Provider>
);
};
export const useMap = () => {
const mapContext = useContext(MapContext);
if (!mapContext) {
throw new Error("useMap is only available within Map");
}
return mapContext;
};
하위 컴포넌트에서 Map
의 옵션을 컨트롤 하기 위한 방법으로 context
를 사용해주었습니다.
옵션을 컨트롤할 수 있는 controller
를 context
를 통해 공급해주어 useMap
훅을 통해서 접근할 수 있게 되었습니다.
// App.tsx
const App = () => {
return (
<Map center={{lat: 30, lng: 120}} smooth={false}>
<MountainMap/>
</Map>
)
}
위와 같은 구조로 마크업을 구성할 경우
// MountainMap.tsx
const MountainMap = () => {
const { controller } = useMap();
const handleMarkerClick = () => {
controller.setOption({center: { lat: 31, lng: 122 }, smooth: true })
}
return <>
<Marker onClick={handleMarkerClick}/>
</>
}
하위 컴포넌트인 MountainMap
에서 controller
를 통해 Map
에 option
에 관여할 수 있게 됩니다.
// MountainMap.tsx
const MountainMap = () => {
const { map, controller } = useMap();
const handleMarkerClick = () => {
controller.setOption({center: { lat: 31, lng: 122 }, smooth: true })
}
useEffect(() => {
const handleMapClick = () => {
console.log("map click")
}
kakao.maps.event.addListener(map, 'click', handleMapClick)
return () => {
kakao.maps.event.removeListener(map, 'click', handleMapClick)
}
},[])
return <>
<Marker onClick={handleMarkerClick}/>
</>
}
useMap
에서는 target
이되는 map
도 반환하고 있기에 하위 컴포넌트에서 Map
에 이벤트 리스너를 부착하는 행위도 가능해집니다.
카카오 지도의 경우 이미 React
에서 사용하기 편리하게끔 제공하는 라이브러리가 존재하나, 이런 바닐라 자바스크립트 환경에 맞춰 제공되는 API를 React
환경에 맞춰 변환해보는 과정을 경험해보고 싶었기에 라이브러리 없어 직접 진행해 보았습니다.