우리 어디서 볼까? [카카오 맵 api로 경로 시각화하기]

승환입니다·2024년 3월 4일
post-thumbnail

현재 진행하고 있는 경매 거래 프로젝트에서 구매자와 판매자사이의 경로를 시각화해주는 기능을 구현하게되었다.

이 기능을 구현하기전에는 세가지 준비물이 있다.

  • 구매자의 상세 주소
  • 판매자의 상세 주소
  • 구매자와 판매자의 위치를 보여줄 지도
  • (kakao map api, geolocation api, kakao mobility api)

구매자의 상세주소는 geoLocation api를 사용해서 실제로 구매자의 위치를 받아왔다.
판매자의 상세주소는 글을 쓸 때 상세주소를 같이 기입하게 만들었다.

마지막으로 구매자와 판매자의 위치를 보여줄 지도는 카카오맵 api를 활용했고 카카오 맵에서 구매자와 판매자 사이의 거리를 시각화해줄 api로는 카카오 모빌리티 api를 썻다.
또한 구매자와 판매자의 위치에 마커를 찍어주었고 각각 다른 마커로 커스텀해주었다.

이번 포스팅은 어떤 과정을 거쳐서 기능을 구현했는지를 기록하고 싶어서 하게되었다.


지도 구현과 마커설정

'use client';

import Script from 'next/script';
import classNamees from 'classnames/bind';
import { useRecoilValue, useSetRecoilState } from 'recoil';
import { locationState, mapState } from '@/src/atom';
import styles from './index.module.scss';

declare global {
  interface Window {
    kakao: any;
  }
}

type MapProps = {
  lat?: string | null;
  lng?: string | null;
  zoom?: number;
};

export default function Map({ lat, lng, zoom }: MapProps) {
  const cx = classNamees.bind(styles);

  const setMap = useSetRecoilState(mapState);
  const location = useRecoilValue(locationState);

  const loadKakaoMap = () => {
    window.kakao.maps.load(() => {
      const mapContainer = document.getElementById('map');
      const mapOption = {
        center: new window.kakao.maps.LatLng(lat, lng),
        level: zoom ?? location.zoom,
      };

      const map = new window.kakao.maps.Map(mapContainer, mapOption);

      setMap(map);
    });
  };

  return (
    <>
      <div id="map" className={cx('map')} />
      <Script
        strategy="afterInteractive"
        type="text/javascript"
        src={`//dapi.kakao.com/v2/maps/sdk.js?appkey=${process.env.NEXT_PUBLIC_KAKAO_MAP_CLIENT}&autoload=false`}
        onReady={loadKakaoMap}
      />
    </>
  );
}

Map이라는 컴포넌트를 만들었다.
재사용이 가능하다고 생각했기때문에 위도와 경도 그리고 확대의 수준을 받도록 구현했다.

window.kakaomaps.Map(내가 지도를 띄우고 싶은 dom 요소, 지도의 옵션) 이며 여기서 얻은 값 (map)을 setMap에 넣어서 전역으로 관리시켰다.
map을 변수로 저장하지않는 이유는 map은 객체이기때문에 렌더링되었을 때 참조값이 바뀌기때문이다.

또한 Script안에 onReady를 넣어주며 Script가 실행된 후에 지도를 그려주도록 해주었다.

두번째는 maker이다.
우리는 지도에 마커를 찍어줘야한다. 마커를 찍는 것도 재사용이 가능하다고 생각해서 컴포넌트로 만들어줬다.

/* eslint-disable react/jsx-no-useless-fragment */
import { mapState } from '@/src/atom';
import { useRecoilValue } from 'recoil';
import React, { useEffect } from 'react';
import { LocationType } from '@/src/types/map';
import arrive from '../../assets/auction/arrive.png';

const Makers = ({
  location,
  info,
  title,
}: {
  location: LocationType;
  info?: boolean;
  title?: string;
}) => {
  const map = useRecoilValue(mapState);

  const loadKakaoMarker = () => {
    if (map) {
      const imageSrc = arrive.src;

      const imageSize = new window.kakao.maps.Size(40, 40);
      const imageOption = { offset: new window.kakao.maps.Point(27, 69) };

      const markerImage = new window.kakao.maps.MarkerImage(
        imageSrc,
        imageSize,
        imageOption,
      );

      const markerPosition = new window.kakao.maps.LatLng(
        location.lat,
        location.lng,
      );

      const marker = new window.kakao.maps.Marker({
        position: markerPosition,
        image: markerImage,
      });
      marker.setMap(map);

      if (info) {
        const iwContent = `<div style="padding:5px;">${title}<br><a href="https://map.kakao.com/link/map/나의 위치,${location.lat},${location.lng}" style="color:blue" target="_blank">길찾기</a></div>`;

        const iwPosition = new window.kakao.maps.LatLng(
          location.lat,
          location.lng,
        );

        const infowindow = new window.kakao.maps.InfoWindow({
          position: iwPosition,
          content: iwContent,
        });

        infowindow.open(map, marker);
      }
    }
  };
  useEffect(() => {
    loadKakaoMarker();
  }, [map]);

  return <></>;
};

export default Makers;

이 부분은 공식문서에서 긁어서 컴포넌트로 커스텀해준게 다이기때문에 생략하겠다.

geoLocation api

구매자의 위치를 받아오기위해 geoLocation api를 썻다. geoLocation api는 현재 나의 위치를 가져올 수 있다.

코드는 아래와 같다.

  useEffect(() => {
    if (!isPending) {
      const query = encodeURIComponent(
        `${data?.board.creator.province.name}${data?.board.creator.city.name}`,
      );
      (async () => {
        const data = await GetCurrentLocation(query);
        setArriveLocation({
          lat: data?.documents[0]?.y,
          lng: data?.documents[0]?.x,
          zoom: 8,
        });
      })();
    }
  }, [data]);
export const GetCurrentLocation = async (query: string) => {
  const response = await axios.get(
    `https://dapi.kakao.com/v2/local/search/address.json?query=${query}`,
    {
      headers: {
        Authorization: `KakaoAK ${process.env.NEXT_PUBLIC_KAKAO_MAP_REST_API}`,
      },
    },
  );

  return response.data;
};

if(!isPending)을 해준 이유는 판매자의 위치를 받아오기전에 게시글 상세데이터를 먼저 가져와야했었기때문에 상세데이터를 가져오는동안 pending 상태로 만들어주었고 pending상태가 끝났을 때 요청을 받도록 했다.

자세한 geoLocation api는 공식문서를 참고해보자
https://apis.map.kakao.com/web/sample/basicMap/

카카오 모빌리티 api

구매자와 판매자의 위치를 알았는데 그 사이의 경로를 찾아줘야한다. 우리는 비행기를 타지않을꺼기 때문에 지도상의 도보와 차도만을 경우의 수를 두고 가장 최적의 경로를 찾아서 시각화 해줘야한다. 카카오 모빌리티 api를 잘 활용하면 구현이 가능하다.

 useEffect(() => {
    if (currentLocation && arriveLocation) {
      (async () => {
        const origin = `${currentLocation.lng},${currentLocation.lat}`;
        const destination = `${arriveLocation.lng},${arriveLocation.lat}`;

        if (origin === ',' || destination === ',') return;
        const queryParams = new URLSearchParams({
          origin,
          destination,
        });

        const response = await GetFindLocation(queryParams);

        const linePath = [];
        response.routes[0].sections[0].roads.forEach(
          (router: { vertexes}) => {
            router.vertexes.forEach((vertex, index) => {
              if (index % 2 === 0) {
                linePath.push(
                  new window.kakao.maps.LatLng(
                    router.vertexes[index + 1],
                    router.vertexes[index],
                  ),
                );
              }
            });
          },
        );
        const polyline = new window.kakao.maps.Polyline({
          path: linePath,
          strokeWeight: 5,
          strokeColor: '#f57a00',
          strokeOpacity: 0.7,
          strokeStyle: 'solid',
        });
        polyline.setMap(map);
      })();
    }
  }, [currentLocation, arriveLocation]);

결과

profile
자바스크립트를 좋아합니다.

0개의 댓글