프로젝트에서 공간 상세화면에 들어가면 해당 공간의 위치를 지도로 보여주는 기능이 있으면 좋겠다는 피드백이 들어왔다. 추가로 큐레이션에 담겨 있는 모든 공간의 위치도 띄워주어야했다.
지도를 보기 위해 서비스 밖으로 이동하는 것이 아닌 서비스 자체에서 지도를 띄워야 했고, 이를 위해 나는 네이버에서 제공해주는 map api를 사용해 보기로했다.
https://www.ncloud.com/product/applicationService/maps
위 링크에 접속한 뒤 신청하기를 누른다. 회원가입을 하면 네이버 클라우드 콘솔로 이동된다.
네이버가 제공하는 map 기능이다. 나는 Web Dynamic Map와 Geocoding을 활용하였다.
네이버 지도 api는 일정 사용 횟수 이전까지는 무료이지만 그 이후부터는 과금이 되므로 결제수단을 등록해야지만 서비스를 이용할 수 있다.
그 후 왼쪽 사이드바에서 Services > AI-NAVER API 탭에 접속해보자.
접속한 후 Application 등록 버튼을 누른 뒤
먼저 본인의 Application 이름을 적어준다.
다음 Service 선택 창에서 본인이 사용할 서비스를 선택해준다. 나는 Web Dynamic Map과 주소를 x,y 좌표로 변환하는 과정을 위해 Geocoding 선택해주었다.
마지막으로 api를 활용할 클라이언트 url을 추가해준다.
보통 로컬 작업을 위해 https://localhost:3000 과 실제 배포된 url 주소를 입력해준다.
위 과정을 마치고 등록을 눌러주면
위와 같이 등록한 Application이 뜨는 것을 확인할 수 있다.
인증 정보를 클릭해보면 api 연결에 사용될 Client Id 값과 Client Secret 값을 얻을 수 있다.
<script type="text/javascript" src="https://oapi.map.naver.com/openapi/v3/maps.js?ncpClientId=YOUR_CLIENT_ID&submodules=geocoder"></script>
index.html 파일의 위 코드를 넣어주어야 한다. 주소 검색 기능을 이용하기 위해 서브 모듈을 geocoder로 지정해주자.
※ Next.js 사용 중일 때에는 글로벌 layout에
<Script
type="text/javascript"
src="https://oapi.map.naver.com/openapi/v3/maps.js?ncpClientId=YOUR_CLIENT_ID&submodules=geocoder"
></Script>
다음과 같이 next/script를 활용하여 넣어주면 된다.
특정 장소를 중심으로 하는 지도 띄우기
특정 장소를 중심으로 하는 지도를 띄우기 위해서는 해당 장소의 위도와 경도 값을 얻어내는 과정을 거쳐야 한다. 이를 위해 Geocoder를 활용하였다.
지도를 렌더링하기 위해 Map이라는 컴포넌트를 하나 생성해주었다.
useEffect(() => {
placeData.forEach((place, i) => {
naver.maps.Service.geocode(
{ query: place.address + " " + place.name },
function (status, res) {
if (status !== naver.maps.Service.Status.OK) {
alert(`${place} 주소에 맞는 결과가 없습니다`);
} else {
// 검색된 주소에 해당하는 위도, 경도를 숫자로 변환후 상태 저장
const resAddress = res.v2.addresses[0];
if (i === 0) {
setCenterX(parseFloat(resAddress.x));
setCenterY(parseFloat(resAddress.y));
}
setMapPlacesData((prev) => [
...prev,
{
x: parseFloat(resAddress.x),
y: parseFloat(resAddress.y),
type: place.type,
name: place.name,
address: place.address,
purpose: place.purpose,
imgUrl: place.imgUrl,
},
]);
}
}
);
});
}, [placeData]);
위는 Map 컴포넌트에서 주소(들)의 위도와 경도 값을 알아내는 코드 중 일부이다. placeData 변수는 각 장소의 주소, 이름, type, 방문목적, 이미지 담긴 객체의 배열이다(나는 장소가 하나여도 배열로 전달했다).
Geocoder 서브모듈에서 제공하는 naver.maps.Service.geocode 메소드를 활용한다. 메소드의 첫번째 파라미터로 optional인 ServiceOptions라는 객체 & required인 query를 전달해야한다. 나는 배열에 담긴 장소정보를 각각 불러오며 query 값에 각 주소와 이름 값을 전달해 주었다..
두번째 파라미터로 callback 함수를 전달해줘야한다. callback 함수의 첫번째 파라미터로 Status 값이 전달된다.
위는 Status 객체의 정보이다.
naver.maps.Service.Status.OK의 값으로 요청이 성공했는지를 판단하고, 성공했을 시의 동작과 실패했을 시의 동작으로 분기처리가 가능하다.
callback 함수의 두번째 파라미터로는 주소 검색 결과가 넘어오고, 이 결과로 경도(x값)와 위도(y값) 값을 알아낼 수 있다.
나는 주소가 여러개 올 때는 맨 처음 주소의 장소를 지도의 중심으로 설정해주기 위해 따로 useState 훅으로 관리해 주었고, 모든 주소의 경도와 위도 값도 저장해주었다.
이제 지도를 보여줄 차례이다.
if (!mapRef.current || !naver) return;
const center = new naver.maps.LatLng(centerY, centerX);
const mapOptions: naver.maps.MapOptions = {
//center 옵션에 생성한 지도 중심 인스턴스 넣기
center,
zoom,
minZoom: 11,
maxZoom: 20,
zoomControl: true,
zoomControlOptions: {
style: naver.maps.ZoomControlStyle.SMALL,
position: naver.maps.Position.TOP_RIGHT,
},
mapDataControl: false,
scaleControl: false,
};
let map = new naver.maps.Map(mapRef.current, mapOptions);
naver.maps.Map 클래스를 활용하여 인스턴스를 생성해야 한다.
생성한 인스턴스의 파라미터로 전달해야 할 정보는 아래와 같다.
const mapRef = useRef<HTMLDivElement>(null);
<div className={twMerge("w-full h-[100vh]", className)}>
<div
ref={(el) => assignMultipleRefs(el, [outsideClickRef, mapRef])}
className="w-full h-[78%]"
/>
</div>
위 코드와 같이 mapRef 로 지도를 삽입할 요소를 지정해주고 mapOptions를 설정한 뒤 파라미터로 전달해주었다.
이때, 처음 단계에서 첫번째 주의 경도와 위도 값을 따로 설정해 준 것을 활용하여 options의 center 속성을 지정해줄 수 있다.
위는 center 속성의 정의이다.
Coord | CoordLiteral 타입에 맞춰주기 위해 공식문서에 나와있는 naver.maps.LatLng 클래스를 활용하였다.
지도에서 특정 장소 마커로 표시하기
이제 장소들의 위치를 마커로 표시해주어야 한다.
mapPlacesData.forEach((place) => {
let marker = new naver.maps.Marker({
position: new naver.maps.LatLng(place.y, place.x),
//생성한 지도 세팅
map,
icon: {
content: MapMarker(place.type),
},
animation: naver.maps.Animation.BOUNCE,
});
});
마커를 사용하기 위해서는 naver.maps.Marker 클래스의 인스턴스를 생성해야한다.
마커를 표시할 위치를 position 속성으로, 위에서 생성해준 지도 인스턴스를 map 속성으로 설정해줘야 한다.
icon 속성을 사용하면 마커를 직접 원하는 디자인으로 설정할 수 있다.
예를 들어 HTML 마크업을 활용할 수 있다.
export default function MapMarker(type: string) {
const markerArray = [
`<div style="width:5rem; height:5rem; background-image:url(${
type == "CAFE" ? "/coffee.png" : "/restaurant.png"
}); background-size: cover; background-position: center; background-repeat: no-repeat;">`,
];
return markerArray.join("");
}
위는 마커 디자인을 생성하는 함수이다.
장소 type에 따라 각각 마커의 이미지가 다르게 보여질 수 있도록 설정해주었다.
animation 속성으로 마커의 애니메이션도 설정해줄 수 있다.
위는 Animation 객체가 가지는 속성을 나타낸다. 나는 naver.maps.Animation.BOUNCE 애니메이션을 마커에 사용하였다.
정보창 표시하기
드디어 마지막 단계이다. 마커를 클릭하면 해당 장소와 관련된 정보창이 뜰 수 있도록 구현해보았다.
let infoWindow = new naver.maps.InfoWindow({
content: MapInfoWindow({
name: place.name,
type: place.type,
address: sliceText(place.address, 15),
purpose: place.purpose,
imgUrl: place.imgUrl,
}),
borderWidth: 0,
pixelOffset: new naver.maps.Point(0, 150),
disableAnchor: true,
backgroundColor: "transparent",
});
naver.maps.Event.addListener(marker, "click", function (e) {
if (infoWindow.getMap()) {
infoWindow.close();
} else {
infoWindow.open(map, marker);
}
});
정보창을 띄우기 위해서는 naver.maps.InfoWindow 클래스를 사용해야 한다. InfoWindowOptions 객체를 통해 해당 클래스를 정의할 수 있다.
content 속성에는 위에 마커와 마찬가지로 HTML 마크업을 생성하는 함수를 만들어 활용하였다.
또한 여러 속성을 통해 정보창의 기본 디자인을 없애주었다.
이제 직접 디자인한 정보창을 열어볼 차례이다.
open(map, anchor) 메소드를 활용하여 정보창을 열 수 있다.
open 메소드의 파라미터로 전달해야 할 값들이다.
위에서 만들어준 Map 객체를 전달하였, 나의 경우 정보창은 마커를 참조하므로 마찬가지로 위에서 만든 naver.maps.Marker 인스턴스를 참조하도록 설정해주었다.
마지막으로 각 마커를 클릭하면 각 장소의 정보창이 뜰 수 있도록 naver.maps.Event 클래스를 활용하여 이벤트 시스템을 구현하면 완성이다.
해당 클래스가 제공하는 addListener(target, eventName, listener) 메소드를 활용하자.
addListener 메소드의 파라미터로 전달해야 할 값들이다.
나는 target 에 만들어준 마커, 클릭 시 이벤트를 발생시키기 위해 eventName에 'click' 을 전달해주었다.
그 후 listener에 마커를 클릭했을 때 실행할 함수를 전달해주었다. 해당 함수에는 정보창을 띄우고 닫는 open 메소드와 close 메소드를 활용한 로직이 포함되어있다.
드디어 모든 과정이 끝나고 결과를 봐보자.
원하는 장소가 마커로 잘 표시되고, 정보창까지 잘 띄워지는 것을 확인할 수 있다!!
보완의 필요성
네이버 지도 api는 일정 요청 초과 시 과금이 되는 시스템이라 지도를 최대한 효율적으로 띄워야 했다.
내가 생각한 방법은 크게 3가지이다.
1. geoCode로 위도, 경도 가져오는 로직을 해당 페이지 접근 시 한번만 불러오고 나머지 로직은 Map 컴포넌트에서 실행하는 방법
-> 해당 로직을 시도해 보았고 얼핏 봤을 때 큰 문제는 없어보였지만, 지도를 드래그할 시 마치 2개의 지도가 겹쳐보이듯 부자연스러워 보이는 현상이 있었다. 또한 사용자가 지도를 열지 않았을 때도 요청을 실행한다는 것이 비효율적으로 보였다.
2. 모든 로직을 지도를 열었을 때 실행하는 방법
-> 현재 내가 사용하는 로직이다. 지도가 띄워졌을 때만 api 요청을 하는 자연스러운 동작 방식이지만, 사용자가 많아진다면 얼마나 과금이 될지 무서운 상황이다.
3. 위도와 경도를 DB에 저장하여 불러오는 방법
-> 사용자가 많아진다면 해당 방법이 가장 이상적이라고 생각한다. 해당 방법을 활용하면 geoCode 로 과금될 일은 없으니 과금에 대한 부담감이 덜어질 것이라고 생각한다.
더 좋은 방법은 없을지 계속 고민해봐야겠다~!!
'FrontEnd > React' 카테고리의 다른 글
프론트엔드(React) 클린 코드로 나아가기 (0) | 2024.03.29 |
---|---|
Suspense 효율적으로 사용하기 (4) | 2024.03.21 |
React-Transition-Group 으로 애니메이션 주입하기 (0) | 2024.02.18 |
ref 속성 파헤치기 (4) | 2024.02.10 |
스토리북(Storybook) 도입하기 (2) | 2024.02.05 |