최근 진행했던 서울산책 서비스는 지도를 가진 기능들이 많았다.
오늘은 4번 장소들에 대한 코스가 어떻게 이어지는지 보여주는 지도 기능에 대해 정리해보려고 한다.
참고로 나는 이번 프로젝트에서 지도 기능들을 만들 때, kakao map API를 활용하여 제작했다.
내가 사용하게 될 데이터는 응답의 places 배열 부분이었다.
이 배열을 활용하여 order_number
순서대로 마커를 생성하고 선을 이어줘야 했다!
kakao map API를 세팅해놨다는 가정하에 정리해보려고 한다.
const CourseExplainPage = () => {
/* 생략 */
return (
/* 생략 */
<li style={{ display: 'flex', flexDirection: 'column', alignItems: 'center' }}>
<CourseMapButton type="button" onClick={() => isFindAddress(!findAddress)}>
{findAddress ? '코스 지도 닫기' : '코스 지도 열기'} // 지도를 열고 닫고 할 수 있는 버튼
</CourseMapButton>
{findAddress ? <CourseRouteMap id="map" /> : null} // 지도
</li>
/* 생략 */
)
/* 생략 */
}
const CourseExplainPage = () => {
const [findAddress, isFindAddress] = useState(false);
const { isLoading, data } = useGetInfoByCourseId(Number(location.pathname.match(/\/course\/(\d+)/)?.[1])))
const courseInfo = data?.data.result as CourseType;
const coursePlaceInfo = courseInfo?.places;
if (coursePlaceInfo) {
coursePlaceInfo.sort((a,b) => a.order_number - b.order_number);
}
useEffect(() => {
if (findAddress && courseInfo && coursePlaceInfo) {
/* 지도와 관련된 코드들 */
}
}, [findAddress])
/* 생략 */
}
지도와 관련된 코드를 작성하기 위해 findAddress 상태값과 courseInfo, coursePlaceInfo 데이터 그리고 useEffect 훅을 사용하려고 한다.
data
는 내가 만들어둔 API 훅을 이용하여 받아온다는 것을 참고하고 봐주면 좋을 것 같다!
useEffect(() => {
window.kakao.maps.load(() => {
const mapContainer = document.getElementById('map');
const locationImageInfo = {
imageSrc: Marker, // 본인의 마커 이미지 값
imageSize: new window.kakao.maps.Size(24, 24) // 마커 이미지 사이즈
};
const locationImage = new window.kakao.maps.MarkerImage(
locationImageInfo.imageSrc,
locationImage.imageSize
);
/* 아래에서 더 이어질 예정 */
})
}, [findAddress]);
지도에 표시하고 싶은 마커 이미지를 불러와 imageSrc에 넣고, 사이즈도 지정해준다!
window.kakao.maps.MarkerImage
생성자를 이용하여 저장해두었던 마커 정보를 다시 넣어 추후 아래에서 마커를 생성하는 함수를 만들 때 다시 값을 사용할 수 있도록 만들어준다.
if (findAddress && courseInfo && coursePlaceInfo) {
window.kakao.maps.load(() => {
/* 위 코드 생략 */
// 마커 생성과 마커를 잇는 함수
const drawMarkerAndLine = (mapInstance: any) => {
const markers: any = [];
const linePath: any = [];
coursePlaceInfo.forEach(course => {
const markerPosition = new window.kakao.maps.LatLng(
course.place_latitude,
course.place_longitude,
);
const marker = new window.kakao.maps.Marker({
position: markerPosition,
image: locationImage,
});
// order_number를 표시할 customOverlay 생성
const orderNumberOverlay = new window.kakao.maps.CustomOverlay({
position: markerPosition,
content: `<div style="width: 50px; height: 25px; background-color: #fff; border: 1px solid #19bb35;
border-radius: 10px; font-size: 12px; font-weight: bold; text-align: center;
position: absolute; bottom: 20px; left: -24px; z-index: 5;" >
${course.order_number + 1} 코스
</div>`,
});
markers.push(marker);
linePath.push(markerPosition);
orderNumberOverlay.setMap(mapInstance);
});
markers.map((item: any) => item.setMap(mapInstance));
// kakao map API에서 제공하는 Polyline 함수
const polyline = new window.kakao.maps.Polyline({
path: linePath,
strokeWeight: 3,
strokeColor: '#001aff',
strokeOpcaity: 1,
strokeStyle: 'solid',
});
polyline.setMap(mapInstance);
};
// 코스 루트를 표시할 지도의 중앙값 계산!
const calculateMapCenter = (places: CoursePlaceType[]) => {
const latitudes = places.map(place => parseFloat(place.place_latitude));
const longitudes = places.map(place => parseFloat(place.place_longitude));
const averageLatitude =
latitudes.reduce((sum, value) => sum + value, 0) / latitudes.length;
const averageLongitude =
longitudes.reduce((sum, value) => sum + value, 0) / longitudes.length;
return new window.kakao.maps.LatLng(averageLatitude, averageLongitude);
};
const center = calculateMapCenter(coursePlaceInfo);
const options = {
center: center,
level: 5,
};
const map = new window.kakao.maps.Map(mapContainer, options);
drawMarkerAndLine(map); // 지도 생성 후 마커-선 표시 함수 실행
return map;
});
}
coursePlaceInfo 배열의 길이만큼 반복하며, 마커를 생성하고 배열에 담아준다.
그리고 마커의 위치값을 linePath 배열에 담아 Polyline 함수를 활용할 때 또 사용할 예정이다.
어떤 코스의 마커인지 정보를 넣기 위해 커스텀오버레이도 추가했다.
마커 간 선을 잇기 위해서는 kakao map API의 Polyline
메소드를 사용하면 된다.
사용법은 다음과 같다.
const polyline = new window.kakao.maps.Polyline({
path: 장소들의 위치값(위도, 경도)이 담긴 배열
strokeWeight: 선의 두께값
strokeColor: 선의 색상
strokeOpacity: 선의 투명도
strokeStyle: 선 스타일
})
polyline.setMap(mapInstance);
Polyline이 가진 속성값은 더 다양해서, 필요한 게 있다면 공식문서를 참고해보면 좋을 것 같다~
그리고 코스의 위치가 모두 떨어져있기 때문에, coursePlaecInfo에 담긴 모든 장소들의 중앙값을 계산해야 했다.
calculateMapCenter 함수를 통해 코스 루트를 표시할 지도의 중앙값을 계산하여 map의 center값으로 지정했다!
한 코스 페이지에 들어와 예시본을 캡쳐했다.
이렇게 유저가 코스 지도를 열게 되면 코스 순서대로 마커를 표시하고, 선을 잇게되어 코스를 한 눈에 볼 수 있게 된다!
이걸 구현하며 약간의 아쉬웠던 점은,
원래는 해당 페이지들에 들어오면 지도가 바로 보이도록 하고 싶었는데 그렇게 구현하려고 하니 useEffect와 관련된 코드가 의도대로 수정되질 않고, 엉키는 게 생기는 것 같아 쉽게 구현할 수가 없었다..😭
시간이 부족하여 어쩔 수 없이 유저가 궁금할때 열고 닫고 할 수 있는 지도로 구현했는데, 다음에는 원래 의도대로 만들고 싶다는 생각이 든다!