지도에 있는 마커를 클릭할 때 발생하는 이벤트를 설정하고 지도를 공유하였을 때 해당 좌표를 기억하여 지도를 로드하는 기능까지 완성해보자.
현재 선택된 store가 무엇인지 판단하는 함수가 존재해야 한다.
그렇기 때문에 custom hook 작성을 통하여 현재 선택된 store를 지정하도록 한다.
import { useCallback } from 'react';
import { mutate } from 'swr';
import type { Store } from '../types/store';
export const CURRENT_STORE_KEY = '/current-store';
const useCurrentStore = () => {
const setCurrentStore = useCallback((store: Store) => {
mutate(CURRENT_STORE_KEY, store);
}, []);
const clearCurrentStore = useCallback(() => {
mutate(CURRENT_STORE_KEY, null);
}, []);
return {
setCurrentStore,
clearCurrentStore,
};
};
export default useCurrentStore;
current-store
라는 id를 사용하고, setCurrentStore
함수는 store를 반환하는데 비해 clearCurrentStore
는 null
을 반환하여 현재 설정된 값을 Null로 바꾸어준다.
이제 해당 custom Hook의 값을 불러와서 onClick 함수에 적용해보자.
const Markers = () => {
const { data : currentStore } = useSWR<Store>(CURRENT_STORE_KEY);
const { setCurrentStore, clearCurrentStore } = useCurrentStore();
return (
<>
{stores.map((store) => {
return (
<Marker
map={map}
coordinates={store.coordinates}
icon ={generateStoreMarkerIcon(store.season, false)}
key={store.nid}
onClick = {() => {
setCurrentStore(store)}}
/>
);
})}
)
}
이렇게 작성하게 되면 Marker 함수에 onClick 이벤트가 수행되었을 때 setCurrentStore
함수가 전달되어 전역으로 store의 상태가 저장된다.
그리고 Marker 컴포넌트에 OnClick함수가 전달되었다는 사실을 수행하기 위하여 세팅을 추가로 해주어야 한다.
const Marker = ({ map, coordinates, icon, onClick }: Marker): null => {
useEffect(() => {
let marker: naver.maps.Marker | null = null;
if (map) {
marker = new naver.maps.Marker({
map: map,
position: new naver.maps.LatLng(...coordinates),
icon,
});
}
if (onClick) {
naver.maps.Event.addListener(marker, 'click', onClick);
}
return () => {
marker?.setMap(null);
};
}, [map]); // eslint-disable-line react-hooks/exhaustive-deps
return null;
};
onClick
props가 들어오게 되면, 네이버 맵 객체에 이벤트리스너를 이용하여 click 이벤트를 전달해주고, onClick 함수 자체를 전달해주면 마커에 클릭 이벤트가 발생하도록 설정해둔 것이다.
클릭이 발생하였을 때 마커를 빨간색으로 바꾸기 위하여 아래 스프라이트를 지정할 수 있도록 해야 한다.
색을 반전시킬 수 있는 요소가 따로 없으므로 z-index
를 설정하여 클릭된 스프라이트가 가장 먼저 올라올 수 있도록 세팅을 추가로 진행해준다.
그런데 네이버 맵의 마커는 밑에 작성한 컴포넌트일수록 z-index를 기본적으로 큰 값으로 만들어 주기 때문에 화면 위로 올리고 싶은 컴포넌트는 기본 컴포넌트 아래에 작성하기만 하면 된다.
return (
<>
{stores.map((store) => {
return (
<Marker
map={map}
coordinates={store.coordinates}
icon ={generateStoreMarkerIcon(store.season, false)}
key={store.nid}
onClick = {() => {
setCurrentStore(store)}}
/>
);
})}
{currentStore && (
<Marker
map={map}
coordinates={currentStore.coordinates}
icon={generateStoreMarkerIcon(currentStore.season, true)}
onClick={clearCurrentStore}
key={currentStore.nid}
/>
)}
</>
);
currentStore
가 존재하는 경우에는 generateStoreMarkerIcon
에 boolean값을 전달하여 선택된 스프라이트가 랜더링 될 수 있도록 하고,
선택된 스프라이트를 다시 선택하면 clearCurrentStore
훅을 실행하여 현재 선택된 currentStore을 Null로 설정해 줄 수 있도록 한다.
export function generateStoreMarkerIcon(
markerIndex: number,
isSelected: boolean,
): ImageIcon {
return {
url: isSelected? '/markers-selected.png' : '/markers.png',
size: new naver.maps.Size(SCALED_MARKER_WIDTH, SCALED_MARKER_HEIGHT),
origin: new naver.maps.Point(SCALED_MARKER_WIDTH * markerIndex, 0),
scaledSize: new naver.maps.Size(
SCALED_MARKER_WIDTH * NUMBER_OF_MARKER,
SCALED_MARKER_HEIGHT
),
};
generateStoreMarkerIcon
함수는 새로운 매개변수를 받아올 수 있고, isSelected 상태에 따라서 다른 sprite를 랜더링 할 수 있도록 세팅되어있다.
일반적으로 이러한 지도 서비스는 지도의 다른 부분을 선택하면 마커가 선택 해제된다. 그렇기 때문에 MapSection 차원에서 onClick 이벤트를 인식할 수 있도록 만들어보자.
const MapSection = () => {
const { initializeMap } = useMap();
const { clearCurrentStore } = useCurrentStore();
const onLoadMap = (map: NaverMap) => {
initializeMap(map);
naver.maps.Event.addListener(map, 'click', clearCurrentStore);
};
맵이 로드될 때, onclick이벤트를 인식할 수 있도록 만드는 것이다. 간단하다!
공유하기 버튼을 클릭하면 현재 위치에 대한 정보와 zoom 정보를 넘겨 다음에 해당 링크로 접속하였을 때 같은 화면을 보고 있을 수 있도록 만들어보고자 한다.
Header 컴포넌트에서 onClick함수를 설정해보자.
먼저 주어진 위도, 경도, 줌 값으로 map을 초기화할 수 있게 해주는 함수를 구현해두어야 한다.
현재 map의 옵션 정보를 가져오는 훅을 useMap
파일에 추가해보자.
const getMapOptions = useCallback(() => {
const mapCenter = map.getCenter();
const center: Coordinates = [mapCenter.lat(), mapCenter.lng()];
const zoom = map.getZoom();
return { center, zoom };
}, [map]);
현재 Map의 옵션을 가져오는 함수이다. 맵의 중심, 줌값을 리턴한다.
이번에는 useRouter 훅을 이용하여 클릭 이벤트를 구현해보자.
import copy from 'copy-to-clipboard';
const Header = () => {
const { resetMapOptions, getMapOptions } = useMap();
const router = useRouter();
const replaceAndCopyUrl = useCallback(() => {
const mapOptions = getMapOptions();
const query = `/?zoom=${mapOptions.zoom}&lat=${mapOptions.center[0]}&lng=${mapOptions.center[1]}`;
router.replace(query);
copy(location.origin + query);
}, [router, getMapOptions]);
}
}
맵의 옵션을 가져와서 zoom과 lat, lng값을 url로 대체해준다. 그리고 클립보드에 복사할 수 있는 라이브러리인 copy-to-clipboard
를 이용하여 현재 url 주소를 복사해둔다.
이제 복사한 링크를 다시 붙여넣었을 때 화면에는 같은 줌을 가진 것으로 이동할 수 있게끔 맵을 로드해야 한다.
const MapSection = () => {
const router = useRouter();
const query = useMemo(() => new URLSearchParams(router.asPath.slice(1)), []); // eslint-disable-line react-hooks/exhaustive-deps
const initialZoom = useMemo(
() => (query.get('zoom') ? Number(query.get('zoom')) : INITIAL_ZOOM),
[query]
);
const initialCenter = useMemo<Coordinates>(
() =>
query.get('lat') && query.get('lng')
? [Number(query.get('lat')), Number(query.get('lng'))]
: INITIAL_CENTER,
[query]
);
router.asPath.slice(1)
를 수행하면 그 결과값으로 /?zoom=10&lat=37.2313257&lng=128.2690251
와 같이 반환된다. initialZoom
과 initialCenter
값을 통하여 만약 쿼리에 값이 존재하면 존재하는 값을 가져오도록 하고, 쿼리에 존재하지 않으면 초기 세팅되어 있던 INITIAL_ZOOM
값과 INITIAL_CENTER
값을 이용할 수 있도록 한다.
만약 쿼리에 값이 존재할 경우 Map
에 해당 값을 넘겨 주어야 하므로
return (
<>
<Map onLoad = {onLoadMap}
initialZoom={initialZoom}
initialCenter={initialCenter}/>
<Markers/>
</>
);
Map 객체에 받아온 값을 넘겨주도록 한다.
그럼 이제 결과값으로 공유하기 버튼을 눌렀을 때 복사받은 링크를 다시 url에 넣으면 같은 화면을 볼 수 있다.
화면을 초기화하는 함수는 useMap
에서 구현해두자.
const resetMapOptions = useCallback(() => {
map.morph(new naver.maps.LatLng(...INITIAL_CENTER), INITIAL_ZOOM);
}, [map]);
morph
는 부드러운 UX를 구현해줄 수 있는 함수이고, 해당 함수가 실행되면 초기값의 센터와 줌으로 map 객체를 새롭게 만들어 로드해주는 것이다.
const Header = () => {
const { resetMapOptions, getMapOptions } = useMap();
const router = useRouter();
const replaceAndCopyUrl = useCallback(() => {
const mapOptions = getMapOptions();
const query = `/?zoom=${mapOptions.zoom}&lat=${mapOptions.center[0]}&lng=${mapOptions.center[1]}`;
router.replace(query);
copy(location.origin + query);
}, [router, getMapOptions]);
return (
<>
<HeaderComponent
onClickLogo={resetMapOptions}
rightElements={[
...
/>
</>
)}
헤더 컴포넌트에서 resetMapOptions
를 사용할 수 있도록 불러오고, 로고를 클릭하였을 때 일이 발생할 수 있도록 그 값을 props로 넘겨주는 코드를 작성한다.
그럼 이렇게 기본적으로 설정해 둔 값으로 돌아온다는 것을 알 수 있다.