리액트로 네이버지도 api 활용하기(2)

차민재·2024년 2월 24일
0

naver-api

목록 보기
2/2

1편에서는 api 키 발급, 지도 제작하기까지 완료했다.
2편에서는 현재 사용자 위치 받기, 마커와 정보창 만들기, 클릭이벤트 설정, 지도 영역 내 마커만 보이기 등의 기능을 정리할 예정이다.

1. 현재 내 위치 좌표 받기

이제 내 위치를 받아와보자. 사용자 위치는 Geolocation API를 이용해서 받아올 수 있다.
Geolocation API 사용하기
자세한 내용은 공식 문서에서 확인하고 바로 코드를 작성해보자.

  1. 우선, src 디렉토리 내에 hooks 디렉토리를 생성한다.(일반적으로 사용자 커스텀 함수를 제작할 때 hooks라고 많이 명명한다)
  2. hooks 디렉토리 내에 useGeolocation.js라는 이름의 파일을 생성한다.

정리하면 경로가 다음과 같다. src/hooks/useGeolocation.js
여기에 다음과 같이 코드를 작성한다.

import { useState, useEffect } from 'react';

const useGeoloaction = () => {
  const [currentMyLocation, setCurrentMyLocation] = useState({
    lat: 0,
    lng: 0,
  });
  const [locationLoading, setLocationLoading] = useState(false);

  const getCurPosition = () => {
    setLocationLoading(true);
    const success = (location) => {
      setCurrentMyLocation({
        lat: location.coords.latitude,
        lng: location.coords.longitude,
      });
      setLocationLoading(false);
    };

    const error = () => {
      setCurrentMyLocation({ lat: 37.5666103, lng: 126.9783882 });
      setLocationLoading(false);
    };

    if (navigator.geolocation) {
      navigator.geolocation.getCurrentPosition(success, error);
    }
  };

  useEffect(() => {
    getCurPosition();
  }, []);

  return { currentMyLocation, locationLoading, getCurPosition };
};

export default useGeoloaction;

이제 앞서 만들었던 지도에 사용자 위치를 넣어보자.

import { useEffect, useRef } from 'react';
import useGeolocation from '../../hooks/useGeolocation';  // 본인 폴더 위치에 주의할 것!

function Map() {
  const mapRef = useRef(null);
  const { naver } = window;
  const { currentMyLocation } = useGeolocation();
  
  useEffect(() => {
    if (currentMyLocation.lat !== 0 && currentMyLocation.lng !== 0) {
      // 네이버 지도 옵션 선택
      const mapOptions = {
        // 지도의 초기 중심 좌표
        center: new naver.maps.LatLng(currentMyLocation.lat, currentMyLocation.lng),
        logoControl: false, // 네이버 로고 표시 X
        mapDataControl: false, // 지도 데이터 저작권 컨트롤 표시 X
        scaleControl: true, // 지도 축척 컨트롤의 표시 여부
        tileDuration: 200, // 지도 타일을 전환할 때 페이드 인 효과의 지속 시간(밀리초)
        zoom: 14, // 지도의 초기 줌 레벨
        zoomControl: true, // 줌 컨트롤 표시
        zoomControlOptions: { position: 9 }, // 줌 컨트롤 우하단에 배치
      };
      mapRef.current = new naver.maps.Map(
        'map',
        mapOptions
      );
    }
  }, [currentMyLocation]);

  return <div id="map" />
}

export default Map;

pc를 이용한 웹 이기에 다소 오차가 많이 있을수도 있지만 기존에 설정한 서울 시청이 아닌 현재 위치와 근접하게 지도가 나타날 것이다.

2. 마커 생성하기

이제 마커를 만들어보자.

마커는 new naver.maps.Marker를 이용해 만들 수 있다.

import { useEffect, useRef } from 'react';
import useGeolocation from '../../hooks/useGeolocation';  // 본인 폴더 위치에 주의할 것!

function Map() {
  const mapRef = useRef(null);
  const { naver } = window;
  const { currentMyLocation } = useGeolocation();
  
  useEffect(() => {
    if (currentMyLocation.lat !== 0 && currentMyLocation.lng !== 0) {
      // 네이버 지도 옵션 선택
      const mapOptions = {
        // 지도의 초기 중심 좌표
        center: new naver.maps.LatLng(currentMyLocation.lat, currentMyLocation.lng),
        logoControl: false, // 네이버 로고 표시 X
        mapDataControl: false, // 지도 데이터 저작권 컨트롤 표시 X
        scaleControl: true, // 지도 축척 컨트롤의 표시 여부
        tileDuration: 200, // 지도 타일을 전환할 때 페이드 인 효과의 지속 시간(밀리초)
        zoom: 14, // 지도의 초기 줌 레벨
        zoomControl: true, // 줌 컨트롤 표시
        zoomControlOptions: { position: 9 }, // 줌 컨트롤 우하단에 배치
      };
      mapRef.current = new naver.maps.Map(
        'map',
        mapOptions
      );
      
      // 현재 내 위치 마커 표시
      new naver.maps.Marker({
        // 생성될 마커의 위치
        position: new naver.maps.LatLng(currentMyLocation.lat, currentMyLocation.lng),
        // 마커를 표시할 Map 객체
        map: mapRef.current,
      });
    }
  }, [currentMyLocation]);

  return <div id="map" />
}

export default Map;

현재는 기본 마커를 썼는데 다음과 같이 마커를 커스텀 해줄수도 있다.

const markerContent = `
  <div style="border: 1px solid black; border-radius: 50%; width: 20px; height: 20px"></div>
`;

// 현재 내 위치 마커 표시
new naver.maps.Marker({
  // 생성될 마커의 위치
  position: new naver.maps.LatLng(currentMyLocation.lat, currentMyLocation.lng),
  // 마커를 표시할 Map 객체
  map: mapRef.current,
  icon: {
    content: markerContent,
    anchor: new naver.maps.Point(0, 50),  // 마커의 위치 설정
  },
});

이외에도 더 다양한 옵션이 있고, 이미지로 마커를 대체할 수도 있으니 원하는 방식으로 본인의 마커를 커스텀하기 바란다.
일단, 설명은 기본 마커로 계속 진행하겠다.

3. 정보창 생성하기

정보창은 new naver.maps.InfoWindow를 이용해 만들 수 있다.

import { useEffect, useRef } from 'react';
import useGeolocation from '../../hooks/useGeolocation';  // 본인 폴더 위치에 주의할 것!

function Map() {
  const mapRef = useRef(null);
  const { naver } = window;
  const { currentMyLocation } = useGeolocation();
  
  useEffect(() => {
    if (currentMyLocation.lat !== 0 && currentMyLocation.lng !== 0) {
      // 네이버 지도 옵션 선택
      const mapOptions = {
        // 지도의 초기 중심 좌표
        center: new naver.maps.LatLng(currentMyLocation.lat, currentMyLocation.lng),
        logoControl: false, // 네이버 로고 표시 X
        mapDataControl: false, // 지도 데이터 저작권 컨트롤 표시 X
        scaleControl: true, // 지도 축척 컨트롤의 표시 여부
        tileDuration: 200, // 지도 타일을 전환할 때 페이드 인 효과의 지속 시간(밀리초)
        zoom: 14, // 지도의 초기 줌 레벨
        zoomControl: true, // 줌 컨트롤 표시
        zoomControlOptions: { position: 9 }, // 줌 컨트롤 우하단에 배치
      };
      mapRef.current = new naver.maps.Map(
        'map',
        mapOptions
      );
      
      // 현재 내 위치 마커 표시
      new naver.maps.Marker({
        // 생성될 마커의 위치
        position: new naver.maps.LatLng(currentMyLocation.lat, currentMyLocation.lng),
        // 마커를 표시할 Map 객체
        map: mapRef.current,
      });
      
      // 정보창 객체
      const infoWindow = new naver.maps.InfoWindow({
        content: [
          '<div style="padding: 10px; box-shadow: rgba(0, 0, 0, 0.1) 0px 4px 16px 0px;">',
          `   <div style="font-weight: bold; margin-bottom: 5px;">여기는 제목</div>`,
          `   <div style="font-size: 13px;">여기는 내용<div>`,
          "</div>",
        ].join(""),
        maxWidth: 300,
        anchorSize: {
          width: 12,
          height: 14,
        },
        borderColor: "#cecdc7",
      });
    }
  }, [currentMyLocation]);

  return <div id="map" />
}

export default Map;

아마 정보창이 안 나올 것으로 예상하지만 다양한 시도를 해보지 않아서 자세히는 모르겠다. 바로 나온다면 다행일수도 있고...
아무튼, 일반적으로 정보창은 마커와 결합해서 '클릭이벤트'를 연결한다.
코드를 아래와 같이 조금 더 추가해주자.

import { useEffect, useRef } from 'react';
import useGeolocation from '../../hooks/useGeolocation';  // 본인 폴더 위치에 주의할 것!

function Map() {
  const mapRef = useRef(null);
  const { naver } = window;
  const { currentMyLocation } = useGeolocation();
  
  useEffect(() => {
    if (currentMyLocation.lat !== 0 && currentMyLocation.lng !== 0) {
      // 네이버 지도 옵션 선택
      const mapOptions = {
        // 지도의 초기 중심 좌표
        center: new naver.maps.LatLng(currentMyLocation.lat, currentMyLocation.lng),
        logoControl: false, // 네이버 로고 표시 X
        mapDataControl: false, // 지도 데이터 저작권 컨트롤 표시 X
        scaleControl: true, // 지도 축척 컨트롤의 표시 여부
        tileDuration: 200, // 지도 타일을 전환할 때 페이드 인 효과의 지속 시간(밀리초)
        zoom: 14, // 지도의 초기 줌 레벨
        zoomControl: true, // 줌 컨트롤 표시
        zoomControlOptions: { position: 9 }, // 줌 컨트롤 우하단에 배치
      };
      mapRef.current = new naver.maps.Map(
        'map',
        mapOptions
      );
      
      // 현재 내 위치 마커 표시
      const marker = new naver.maps.Marker({
        // 생성될 마커의 위치
        position: new naver.maps.LatLng(currentMyLocation.lat, currentMyLocation.lng),
        // 마커를 표시할 Map 객체
        map: mapRef.current,
      });
      
      // 정보창 객체
      const infoWindow = new naver.maps.InfoWindow({
        content: [
          '<div style="padding: 10px; box-shadow: rgba(0, 0, 0, 0.1) 0px 4px 16px 0px;">',
          `   <div style="font-weight: bold; margin-bottom: 5px;">여기는 제목</div>`,
          `   <div style="font-size: 13px;">여기는 내용<div>`,
          "</div>",
        ].join(""),
        maxWidth: 300,
        anchorSize: {
          width: 12,
          height: 14,
        },
        borderColor: "#cecdc7",
      });
      
      // 현재 나와 가장 가까이 있는 화장실의 정보창 이벤트 핸들러
      naver.maps.Event.addListener(marker, "click", () => {
        if (infoWindow.getMap()) {
          // 정보창이 닫힐 때 이벤트 발생
          infoWindow.close();
        } else if (mapRef.current !== null) {
          // 정보창이 열릴 때 이벤트 발생
          infoWindow.open(mapRef.current, marker);
        }
      });
    }
  }, [currentMyLocation]);

  return <div id="map" />
}

export default Map;

보면 new naver.maps.Marker를 marker라는 변수에 할당해주고 addListener에 해당 marker와 infoWindow를 연결해 클릭이벤트를 설정하였다.
마커를 클릭하면 우리가 설정한 정보창이 나타나고 마커를 다시 클릭하면 정보창이 닫힐 것이다.

4. 다중 마커와 다중 정보창 설정하기

이제 조금만 더 난이도를 높여서 마커와 정보창을 여러 개로 설정해보자.
코드가 길어져서 슬슬 알아보기 어려울 것 같아 추가되는 코드를 먼저 설명하겠다.

// 마커 리스트와 정보창 리스트 선언
const markers = [];
const infoWindows = [];

const samples = [
  { lat: currentMyLocation.lat, lng: currentMyLocation.lng },
  { lat: 37.5666103, lng: 126.9783882 },
  { lat: 37.5796103, lng: 126.9772882 },
];  // 좌표 샘플

for (let i = 0; i < samples.length; i++) {
  // 현재 내 위치 마커 표시
  const marker = new naver.maps.Marker({
    // 생성될 마커의 위치
    position: new naver.maps.LatLng(samples[i].lat, samples[i].lng),
    // 마커를 표시할 Map 객체
    map: mapRef.current,
  });

  // 정보창 객체
  const infoWindow = new naver.maps.InfoWindow({
    content: [
      '<div style="padding: 10px; box-shadow: rgba(0, 0, 0, 0.1) 0px 4px 16px 0px;">',
      `   <div style="font-weight: bold; margin-bottom: 5px;">여기는 제목${i+1}</div>`,
      `   <div style="font-size: 13px;">여기는 내용${i+1}<div>`,
      "</div>",
    ].join(""),
    maxWidth: 300,
    anchorSize: {
      width: 12,
      height: 14,
    },
    borderColor: "#cecdc7",
  });
  
  markers.push(marker);
  infoWindows.push(infoWindow);
}

// 각 마커에 이벤트가 발생했을 때 기능 설정
const getClickHandler = (index) => {
  if (infoWindows[index].getMap()) {
    infoWindows[index].close();
  } else if (mapRef.current !== null) {
    infoWindows[index].open(mapRef.current, markers[index]);
  }
};

// 각 마커에 이벤트 핸들러 설정
for (let i = 0; i < markers.length; i++) {
  naver.maps.Event.addListener(markers[i], "click", getClickHandler(i));
}

샘플은 내 현재위치, 서울 시청, 경복궁이다. 완성된 코드는 아래와 같다.
이제 완성 코드를 확인하자!

import { useEffect, useRef } from 'react';
import useGeolocation from '../../hooks/useGeolocation';  // 본인 폴더 위치에 주의할 것!

function Map() {
  const mapRef = useRef(null);
  const { naver } = window;
  const { currentMyLocation } = useGeolocation();
  
  useEffect(() => {
    if (currentMyLocation.lat !== 0 && currentMyLocation.lng !== 0) {
      // 네이버 지도 옵션 선택
      const mapOptions = {
        // 지도의 초기 중심 좌표
        center: new naver.maps.LatLng(currentMyLocation.lat, currentMyLocation.lng),
        logoControl: false, // 네이버 로고 표시 X
        mapDataControl: false, // 지도 데이터 저작권 컨트롤 표시 X
        scaleControl: true, // 지도 축척 컨트롤의 표시 여부
        tileDuration: 200, // 지도 타일을 전환할 때 페이드 인 효과의 지속 시간(밀리초)
        zoom: 14, // 지도의 초기 줌 레벨
        zoomControl: true, // 줌 컨트롤 표시
        zoomControlOptions: { position: 9 }, // 줌 컨트롤 우하단에 배치
      };
      mapRef.current = new naver.maps.Map(
        'map',
        mapOptions
      );
      
      // 마커 리스트와 정보창 리스트 선언
      const markers = [];
      const infoWindows = [];

      const samples = [
        { lat: currentMyLocation.lat, lng: currentMyLocation.lng },
        { lat: 37.5666103, lng: 126.9783882 },
        { lat: 37.5796103, lng: 126.9772882 },
      ];  // 좌표 샘플
      
      for (let i = 0; i < samples.length; i++) {
        // 현재 내 위치 마커 표시
        const marker = new naver.maps.Marker({
          // 생성될 마커의 위치
          position: new naver.maps.LatLng(samples[i].lat, samples[i].lng),
          // 마커를 표시할 Map 객체
          map: mapRef.current,
        });

        // 정보창 객체
        const infoWindow = new naver.maps.InfoWindow({
          content: [
            '<div style="padding: 10px; box-shadow: rgba(0, 0, 0, 0.1) 0px 4px 16px 0px;">',
            `   <div style="font-weight: bold; margin-bottom: 5px;">여기는 제목${i+1}</div>`,
            `   <div style="font-size: 13px;">여기는 내용${i+1}<div>`,
            "</div>",
          ].join(""),
          maxWidth: 300,
          anchorSize: {
            width: 12,
            height: 14,
          },
          borderColor: "#cecdc7",
        });

        markers.push(marker);
        infoWindows.push(infoWindow);
      }
      
      // 각 마커에 이벤트가 발생했을 때 기능 설정
      const getClickHandler = (index) => {
        if (infoWindows[index].getMap()) {
          infoWindows[index].close();
        } else if (mapRef.current !== null) {
          infoWindows[index].open(mapRef.current, markers[index]);
        }
      };

      // 각 마커에 이벤트 핸들러 설정
      for (let i = 0; i < markers.length; i++) {
        naver.maps.Event.addListener(markers[i], "click", getClickHandler(i));
      }
    }
  }, [currentMyLocation]);

  return <div id="map" />
}

export default Map;

완성된 코드로 실행해보면 총 3개의 마커가 나타날 것이다.(운이 안 좋게 사용자 위치가 서울 시청이나 경복궁과 겹친다면 2개가 나타날 것이다)
각 마커를 클릭하면 각각의 정보창이 나타나고 리스트 번호를 볼 수 있을 것이다.

5. 현재 보이는 영역에만 마커 표시하기

마지막으로 마커가 내가 현재 보는 지도의 영역 밖으로 나가면 사라지는 기능을 추가하겠다.

이 기능은 아래 참고자료를 작성하신 donggu님께 감사함을 전하며 자세한 설명은 참고자료를 꼭 확인하길 바란다.
네이버 지도 api를 이용하여 지도 만들기

새로운 파일을 만들어야 한다.

  1. src 디렉토리 안에 util 디렉토리를 생성한다.
  2. util 디렉토리 안에 checkForMarkersRendering.js라는 파일을 생성한다.
  3. checkForMarkersRendering.js 파일에 다음 코드를 넣는다.
// 마커 표시 함수
const showMarker = (map, marker) => {
  marker.setMap(map);
};

// 마커 숨김 함수
const hideMarker = (marker) => {
  marker.setMap(null);
};

const checkForMarkersRendering = (map, markers) => {
  const mapBounds = map.getBounds();

  for (let i = 0; i < markers.length; i += 1) {
    const position = markers[i].getPosition();

    if (mapBounds.hasLatLng(position)) {
      showMarker(map, markers[i]);
    } else {
      hideMarker(markers[i]);
    }
  }
};

export default checkForMarkersRendering;

순서대로 잘 따라왔으면 마지막으로 Map.jsx를 수정해주자.

import { useEffect, useRef } from 'react';
import useGeolocation from '../../hooks/useGeolocation';  // 본인 폴더 위치에 주의할 것!
import checkForMarkersRendering from '../../util/checkForMarkersRendering';

function Map() {
  const mapRef = useRef(null);
  const { naver } = window;
  const { currentMyLocation } = useGeolocation();
  
  useEffect(() => {
    if (currentMyLocation.lat !== 0 && currentMyLocation.lng !== 0) {
      // 네이버 지도 옵션 선택
      const mapOptions = {
        // 지도의 초기 중심 좌표
        center: new naver.maps.LatLng(currentMyLocation.lat, currentMyLocation.lng),
        logoControl: false, // 네이버 로고 표시 X
        mapDataControl: false, // 지도 데이터 저작권 컨트롤 표시 X
        scaleControl: true, // 지도 축척 컨트롤의 표시 여부
        tileDuration: 200, // 지도 타일을 전환할 때 페이드 인 효과의 지속 시간(밀리초)
        zoom: 14, // 지도의 초기 줌 레벨
        zoomControl: true, // 줌 컨트롤 표시
        zoomControlOptions: { position: 9 }, // 줌 컨트롤 우하단에 배치
      };
      mapRef.current = new naver.maps.Map(
        'map',
        mapOptions
      );
      
      // 마커 리스트와 정보창 리스트 선언
      const markers = [];
      const infoWindows = [];

      const samples = [
        { lat: currentMyLocation.lat, lng: currentMyLocation.lng },
        { lat: 37.5666103, lng: 126.9783882 },
        { lat: 37.5796103, lng: 126.9772882 },
      ];  // 좌표 샘플
      
      for (let i = 0; i < samples.length; i++) {
        // 현재 내 위치 마커 표시
        const marker = new naver.maps.Marker({
          // 생성될 마커의 위치
          position: new naver.maps.LatLng(samples[i].lat, samples[i].lng),
          // 마커를 표시할 Map 객체
          map: mapRef.current,
        });

        // 정보창 객체
        const infoWindow = new naver.maps.InfoWindow({
          content: [
            '<div style="padding: 10px; box-shadow: rgba(0, 0, 0, 0.1) 0px 4px 16px 0px;">',
            `   <div style="font-weight: bold; margin-bottom: 5px;">여기는 제목${i+1}</div>`,
            `   <div style="font-size: 13px;">여기는 내용${i+1}<div>`,
            "</div>",
          ].join(""),
          maxWidth: 300,
          anchorSize: {
            width: 12,
            height: 14,
          },
          borderColor: "#cecdc7",
        });

        markers.push(marker);
        infoWindows.push(infoWindow);
      }
      
      // 각 마커에 이벤트가 발생했을 때 기능 설정
      const getClickHandler = (index) => {
        if (infoWindows[index].getMap()) {
          infoWindows[index].close();
        } else if (mapRef.current !== null) {
          infoWindows[index].open(mapRef.current, markers[index]);
        }
      };

      // 각 마커에 이벤트 핸들러 설정
      for (let i = 0; i < markers.length; i++) {
        naver.maps.Event.addListener(markers[i], "click", getClickHandler(i));
      }
      
      // 지도 줌 인/아웃 시 마커 업데이트 이벤트 핸들러
      naver.maps.Event.addListener(mapRef.current, "zoom_changed", () => {
        if (mapRef.current !== null) {
          checkForMarkersRendering(mapRef.current, markers);
        }
      });
      
      // 지도 드래그 시 마커 업데이트 이벤트 핸들러
      naver.maps.Event.addListener(mapRef.current, "dragend", () => {
        if (mapRef.current !== null) {
          checkForMarkersRendering(mapRef.current, markers);
        }
      });
    }
  }, [currentMyLocation]);

  return <div id="map" />
}

export default Map;

이로써 이번 프로젝트에서 배운 네이버지도 api 다루는 법을 모두 정리했다.
시간 문제와 두 번째 프로젝트로 인해 자세하게 정리하지는 못 했지만, 본질적인 목표인 불친절한 네이버 공식문서에 애먹는 다른 예비 개발자 분들이 조금이라도 더 쉽게 api를 다뤘으면 해서 되도록 완성본 코드로 작성하였다.

물론, 공식문서를 보지말라는 얘기는 아니다. 바닐라 JS 시절을 기준으로 문서가 작성되어 있어 react에 변경하여 적용하는 방법이 어려울 뿐이지 공식문서에는 지금 내 설명보다 더 많은 기능들이 기록되어 있다.
당장은 해석하기 어렵겠지만 내 코드를 바탕으로 공식문서를 열심히 해석해서 더 멋진 지도를 만들길 바란다.

참고자료

네이버 지도 api를 이용하여 지도 만들기

profile
병아리 개발자

0개의 댓글