Next.js 14로 네이버 지도 API를 이용해 지도 기능 개발하기 (2)

osohyun0224·2024년 3월 31일
1

VACGOM 개발이야기

목록 보기
2/4
post-thumbnail

안녕하세요, 대학생 웹 프론트엔드 개발자 가든입니다 ;)

최근에 참가한 해커톤에서 네이버 지도 api를 사용하여 특정 백신 접종을 지원하는 병원 조회 기능을 개발하게 되었습니다.

저번 포스팅에서는 네이버 지도 Api를 직접 사용해 Next.js에 지도를 불러오고 기본으로 제공하는 마커 찍기 기능까지 구현해보았는데요!
바로 이 글에서 1편을 만나볼 수 있습니다 - Next.js 14로 네이버 지도 API를 이용해 지도 기능 개발하기

현재 사용 언어는 타입스크립트, Nextjs 버전은 14를 사용하면서 구현중입니다! Next.js에 구현하는 레퍼런스가 많이 없길래 자세히 구현하는 과정을 기록해보았습니다

오늘은 최종 디자인 대로 마커를 커스텀하고, 해당 병원에 대한 세부 정보를 조회할 수 있는 기능을 함께 구현해보겠습니다
병원 데이터를 크롤링해오고 지오코딩에 데이터 정제 구현 이야기까지 담겨있습니다 ㅎㅎ,,,

1. 마음대로 마커 커스텀 하기

최종 디자인이 나옴에 따라 네이버에서 기본적으로 지원하는 파란색 마커를 사용하지 않고 디자이너의 마커를 커스텀해야했습니다.

저희 디자이너분의 예쁜 마크,,, 마크보고 정말 감탄했습니다 ㅠㅠ 다시봐도 넘 예쁘네요,,,

마커 커스텀 기능에 대한 요구사항을 정리해보면 다음과 같습니다.

[마커 개발 요구 사항]

  1. 사용자의 현 위치를 나타내는 마커는 검정색 백곰 마크여야한다.
  2. 사용자가 특정 병원을 선택했을 때의 마커 상태는 파란색 백신 마크여야한다.
  3. 사용자가 지도에서 병원들의 위치를 파악할때(선택하지 않은 병원)의 상태는 회색 마크여야한다.

생각보다 간단한 마커 요구사항이었고, 위의 마커들을 svg이미지로 저장해서 바로 코드에 적용시켜보았습니다.

// 특정 병원 목록들에 대한 마커 코드입니다. 
hospitals.forEach((hospital) => {
  const marker = new naver.maps.Marker({
    position: new naver.maps.LatLng(hospital.lat, hospital.lng),
    map: map,
    title: hospital.name,
    icon: {
      url:
        selectedHospitalId === hospital.id
          ? '/assets/ico/ico-map-selec.svg'
          : '/assets/ico/ico-map-unselec.svg',
      size: new naver.maps.Size(50, 63),
      scaledSize: new naver.maps.Size(50, 63),
      origin: new naver.maps.Point(0, 0),
      anchor: new naver.maps.Point(12, 37),
    },
  });

  naver.maps.Event.addListener(marker, 'click', () => {
    setSelectedHospitalId(
      selectedHospitalId === hospital.id ? null : hospital.id,
    );
    setModalContent({
      name: hospital.name,
      major: hospital.major,
      address: hospital.address,
    });
    setIsModalOpen(true);
  });
});

[1] 특정 위치들 불러오고 마커 생성

위의 코드를 살펴보면 hospitals이라는 병원 목록 데이터가 담긴 배열을 순회하면서 배열에 담긴 병원 목록들의 위도 경로에 맞는 위치에 마커를 위치시키도록 하고 있습니다.

[1.5] 웬만하면 백엔드에게 부탁하자... 크롤링과 지오코딩

저는 예방접종도우미 - 백신 지원사업 병원 리스트 조회 에서 사용자의 현 위치의 지역의 구 단위 (??시 ??구)의 병원 리스트들만 마커에 띄워지도록 구현을 할 예정이었습니다. 따라서 실시간으로 위의 사이트에서 특정 백신의 특정 시, 특정 구의 병원 목록에서의 이름과, 주소를 크롤링 작업을 1차로 진행했습니다.

이걸 왜 프론트에서 하게 되었냐면 마커 띄우고 이래야되는데 생각보다 백엔드에서 인증 쪽 api를 먼저 작업 시작해서 계속 api를 기다릴 수가 없었기 때문입니다... 걍 내가 해보자 하고 각잡고 데이터 넣기 시작했습니다 (이렇게 해서 시작된 JSON 상하차 작업)
그런데 저희처럼 스케일이 큰 (전국 대상으로 다 끌어와 보여야한다)라고 하시면 웬만하면 백엔드분들에게 맡기시는게 정신 건강에 좋습ㅁ니다...

크롤링해온 목록들을 가지고 1차적으로 데이터를 정제했습니다.

  {
     "id":1,
     "name":"고려메디웰의원",
     "address":"경기 용인시 수지구 성복2로 230 301호",
  },
    ....

위와 같이 1차적으로 정제하고 나서, 지오코딩을 사용해 각 위치들의 위도 경도 값을 가져와야합니다. 여기서 지오코딩은 주소를 지리적 좌표(위도와 경도)로 변환하는 과정을 뜻하고, 네이버 지도를 사용하냐, 구글 지도를 사용하냐에 따라서 각 플랫폼에서 지원하는 api가 따로 있습니다.
저는 네이버 지도를 사용했기 때문에 네이버의 지오코드를 사용했습니다. 구글 맵을 사용하신다면 구글 지오코드를 보시면 됩니다!

api 비용 절감...ㅜㅜ

여기서 또 작은 이슈가 있었는데 모든 사용자들이 저희 서비스를 사용해 접속할때마다

예방 접종 도우미에서 1차적으로 특정한 조건들에 맞는 장소 데이터들을 크롤링해서 정제 -> 정제된 데이터들을 가지고 지오코딩으로 위도 경도 추출

위의 과정을 지속하면 다 네이버 지도 api를 사용하다보니 비용이 미친듯이 발생할 것 같았습니다. 비용을 진짜 어떻게 안들고 변환하게 하지 라는 생각을 하다가 제공하는 위치를 제한하고 (수도권), 제한한 위치에 있는 장소들을 먼저 지오코딩을 구현해서 다 위도 경도 값을 추출하고 이를 다 넣어두자라는 미친 생각을 하게 됩니다...
사실 위의 방법말고는 유료 api 서비스를 안 쓸 수 있는 방법이 없어서 10일이라는 급한 시간내에 저렇게 구현하게 되었는데 비용이 상관없다면 돈을 쓰자!!!가 제일 좋습니다 ㅎㅎ

그래서 1차적으로 가져온 모든 병원 데이터들에 대해 네이버 지오코드 api를 사용해서 위도 경도 값을 다음과 같이 추출했습니다.

const addresses = [
//위에서 1차적으로 작성한 병원들의 주소들을 배열에 담았습니다. 
];


function geocodeAddress(address) {
  return fetch(`https://naveropenapi.apigw.ntruss.com/map-geocode/v2/geocode?query=${encodeURIComponent(address)}`, {
    method: 'GET',
    headers: {
      'X-NCP-APIGW-API-KEY-ID': '개인의 클라이언트 id',
      'X-NCP-APIGW-API-KEY': '개인의 클라이언트 secret'
    }
  })
  .then(response => response.json())
  .then(data => {
    if (data.addresses && data.addresses.length > 0) {
      const location = data.addresses[0];
      return {
        lat: location.y,
        lng: location.x
      };
    } else {
      throw new Error('no address in location ' + address);
    }
  });
}

// 주소 배열에 대한 지오코딩 요청을 처리하는 기능
const geocodePromises = addresses.map(address => geocodeAddress(address));

// 모든 지오코딩 요청이 완료되고 각각의 주소들에 대한 위도 경도 값이 results 배열안에 담김!
Promise.all(geocodePromises)
  .then(results => {
    // 여기에서 results 배열은 각 주소의 지오코딩 결과를 담고 있습니다!
    console.log(results);
  })
  .catch(error => {
    console.error(error);
  });

위와 같은 과정을 거쳐 아래의 최종적인 병원 목록 데이터를 확정할 수 있었습니다. 여기서 위도 경도값이 확실한지 보고 싶다 하시는 분들은 직접 주소를 입력해 위도 경도 값을 얻을 수 있는 사이트 geoservice-web 를 추천해드립니다!

최종 데이터 정제

  {
     "id":1,
     "name":"고려메디웰의원",
     "lat":37.319962,
     "lng":127.062170,
     "address":"경기 용인시 수지구 성복2로 230 301호",
     "major":"병원,의원"
  },
    ....

눈물나네요 진쨔ㅠㅠ 이 과정이 지도 개발하면서 하루만에 끝날껄 3일을 쓰게했던 것 같습니다...
다시한번 더 웬만하면 돈을 쓰자 혹은 백엔드에게 부탁하자! ㅜㅜ

[2] 디자인 마커 커스텀

또한 사용자가 해당 마커를 선택했을 때와 선택하지 않았을때의 마커 이미지들이 올바르게 바뀌도록 구현했습니다.

위와 같이 구현하다 버그를 만나게 되었는데요 ㅎㅎ 바로 마커의 위치가 너무 크게 보이거나, 작게 보이거나, 제멋대로 위치하는 버그가 발생하게 되었습니다.
또한 사용자가 지도를 확대했을때와, 확대하지 않은 넓은 범위의 지도 반경에서도 확대했을 때와 동일하게 보여야하기 때문입니다.

[2.5] 세부적으로 위치와 크기를 커스터마이징하기

 const marker = new naver.maps.Marker({
    position: new naver.maps.LatLng(hospital.lat, hospital.lng),
    map: map,
    title: hospital.name,
    icon: {
      url:
        selectedHospitalId === hospital.id
          ? '/assets/ico/ico-map-selec.svg'
          : '/assets/ico/ico-map-unselec.svg',
      size: new naver.maps.Size(50, 63),
      scaledSize: new naver.maps.Size(50, 63),
      origin: new naver.maps.Point(0, 0),
      anchor: new naver.maps.Point(12, 37),
    },
  • size: 이 속성은 아이콘의 원래 크기를 지정하는 속성인데요, 여기서 이 크기는 아이콘 이미지의 실제 픽셀 크기와 일치해야 하며, 아이콘 이미지가 이 크기보다 클 경우 이미지가 잘릴 수 있기 때문에 여러분들이 지정한 아이콘 이미지의 크기와 동일하게 맞춰주시는 게 중요합니다!

  • scaledSize: 이 속성은 아이콘 이미지를 어떤 크기로 화면에 표시할지 결정하는 속성입니다. 여기서 이 속성은아이콘 이미지를 원래 크기보다 더 크거나 작게 표시하고 싶을 때 사용하는데, 제가 위에서 언급했던

    또한 사용자가 지도를 확대했을때와, 확대하지 않은 넓은 범위의 지도 반경에서도 확대했을 때와 동일하게 보여야하기

위의 사항대로 개발할때 중요하게 고려하는 속성입니다. 실제 지도의 배율을 확대, 축소해보면서 이 속성을 알맞게 조절하는게 중요합니다!

  • origin: 이 속성은 아이콘 이미지 내에서 실제 아이콘 그래픽이 시작되는 지점을 정의하는 속성입니다. new naver.maps.Point(0, 0)는 이미지의 가장 왼쪽 상단 모서리를 시작점으로 사용하겠다는 것을 의미합니다. 만약 아이콘 그래픽이 이미지 내에서 오프셋되어 있다면, 이 속성을 조절하여 정확한 시작점을 지정할 수 있기 때문에 저는 0,0 으로 정의해두었습니다!

  • anchor: 마커의 위치에 대한 아이콘의 기준점을 설정하는 속성입니다. 이는 아이콘 이미지 내에서 마커가 가리키는 실제 지점을 정밀하게 조정할 수 있게 해주는 역할입니다!

2. 병원에 대한 세부 정보를 조회 모달 기능

사용자가 특정 병원을 선택하면, 선택한 병원의 정보가 모달창으로 보여져야하는 기능을 구현했습니다.
우선적으로 전체적인 내용은 모달 창을 열어서 특정 병원의 정보가 보여지게 하는 것은 모달 컴포넌트로 따로 구현하고 다음과 같이 보여지도록 했습니다.

// 병원 마커에 이벤트 리스너를 추가하였습니다!
hospitals.forEach((hospital) => {
  const marker = new naver.maps.Marker({
    position: new naver.maps.LatLng(hospital.lat, hospital.lng),
    map: map,
    title: hospital.name,
    icon: {
      url:
        selectedHospitalId === hospital.id
          ? '/assets/ico/ico-map-selec.svg'
          : '/assets/ico/ico-map-unselec.svg',
      size: new naver.maps.Size(50, 63),
      scaledSize: new naver.maps.Size(50, 63),
      origin: new naver.maps.Point(0, 0),
      anchor: new naver.maps.Point(12, 37),
    },
  });

  // 마커를 클릭했을 때 모달을 표시하도록 구현했습니다
  naver.maps.Event.addListener(marker, 'click', () => {
    setSelectedHospitalId(
      selectedHospitalId === hospital.id ? null : hospital.id,
    );
    setModalContent({
      name: hospital.name,
      major: hospital.major,
      address: hospital.address,
    });
    setIsModalOpen(true);
  });
});
.....
// 모달 컴포넌트를 렌더링하는 부분입니다!
<Modal
  isOpen={isModalOpen}
  onClose={() => setIsModalOpen(false)}
  content={modalContent}
/>

모달을 띄우게 하는 부분은 어렵지 않으나, 여기서 구현 이슈가 한 가지 발생합니다.

[1] 실시간 운영 정보 가져오기

디자이너의 화면을 보면 이 병원이 현재 진료 중인지 아닌지 실시간 정보를 가져와야합니다.......

이거 네이버에서 지원하는지 안지원하는지 리서치를 진행했는데 이 기능에 대한 api를 제공하지 않았습니다.
제가 못 찾은 거라면 댓글에 알려주시길 바랍니다...

그래서 각 병원의 진료 시간을 해당 병원 데이터에 각각 다 넣고, 사용자의 현재 시간을 기준으로 진료 중인지 아닌지 휴무인지 아닌지만 구현을 진행했습니다. 이런 데이터 노가다 엔딩을 맞았지만 해당 방법 말고는 아예 기능을 삭제해야했기에 어쩔 수 없었던 것 같습니다...

구현 화면

profile
학부생 Frontend Developer

2개의 댓글

comment-user-thumbnail
2024년 4월 1일

안녕하세요, 네이버 클라우드 플랫폼입니다.
네이버클라우드의 기술 콘텐츠 리워드 프로그램 ‘이달의 Nclouder(3월)’ 도전자로 초대합니다 🙂

네이버 클라우드 플랫폼 서비스와 관련된 모든 주제로 4/4(목) 23시까지 신청 가능합니다. (*3월 작성 콘텐츠 한정 신청 가능)

Ncloud 크레딧을 포함한 다양한 리워드가 준비되어 있으니 많은 관심 부탁드립니다!

자세한 내용은 아래 링크에서 확인부탁드립니다.
https://blog.naver.com/n_cloudplatform/223380729192

신청 링크
https://navercloud.typeform.com/to/lF8NUaCF

1개의 답글