[NextJs 지도 개발 #3] 지도 공유하기 및 마커 선택 기능

김유진·2023년 4월 24일
1

Nextjs

목록 보기
6/9

지도에 있는 마커를 클릭할 때 발생하는 이벤트를 설정하고 지도를 공유하였을 때 해당 좌표를 기억하여 지도를 로드하는 기능까지 완성해보자.

1. Marker에 onClick 기능 넣기

현재 선택된 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를 반환하는데 비해 clearCurrentStorenull을 반환하여 현재 설정된 값을 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와 같이 반환된다. initialZoominitialCenter 값을 통하여 만약 쿼리에 값이 존재하면 존재하는 값을 가져오도록 하고, 쿼리에 존재하지 않으면 초기 세팅되어 있던 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로 넘겨주는 코드를 작성한다.

그럼 이렇게 기본적으로 설정해 둔 값으로 돌아온다는 것을 알 수 있다.

0개의 댓글