프로젝트/Next+TypeScript

[맛길] Naver Map API를 이용한 맛집 지도 추가

dev-hpk 2025. 2. 13. 18:02

오늘은 맛집 상세 페이지에 지도를 추가해 보겠습니다. 지도는 Naver Map API를 이용해 볼 계획입니다.

 

0️⃣ Naver Map API 선택 이유

Naver Map API v3 특징

Naver Map API v3 - 특징

 

  1. 프레임워크에 의존하지 않고 독립적으로 동작하기 때문에 불필요한 의존성을 최소화할 수 있고,  React & Next.js로 제작된 맛길 프로젝트에 적합할 것 같아 선택했습니다. 
  2. DOM 처리 및 웹 브라우저 호환 코드를 내장하고 있어 크로스 브라우징 이슈를 최소화하면서 손쉽게 지도 기능을 구현할 수 있을 거라 생각해 선택했습니다.
  3. 별도의 CSS를 필요로 하지 않도록 설계된 내용을 보고, 개발 부담을 줄일 수 있을 것 같아서 선택했습니다.
  4. 모바일 환경에서도 최적화된 성능을 제공하기 때문에 별도의 최적화 작업이 필요하지 않아 개발 부담을 줄일 수 있을 것 같아 선택했습니다.

1️⃣ NAVER CLOUD PLATFORM 인증키 발급

NCloud - Application 등록

Application 등록

 

Application 이름은 서비스와 동일하게 matgil로 설정하겠습니다.

맛길 서비스의 지도에서는 길 찾기 기능은 필요하지 않기 때문에 Directions 옵션은 선택하지 않았습니다.

Directions 제공 기능

 

Application 등록을 완료했고, 인증 정보 버튼을 클릭해 보니 Client ID가 생겼습니다.

Client ID는 프로젝트에서 Naver Map API를 이용할 때 사용되는 API Key이기 때문에 외부 노출을 방지하기 위해 .env 파일에 저장했습니다.

Application 인증 정보

2️⃣ Naver Map  API 타입 패키지 설치

npm i -D @types/navermaps

3️⃣ Naver Map API 이용해 지도 컴포넌트 작업

Map.tsx - 지도 컴포넌트

'use client';

import { useEffect, useRef } from 'react';
import Script from 'next/script';

function Map() {
  const mapRef = useRef<naver.maps.Map | null>(null);

  const initMap = (x: number, y: number) => {
    const map = new naver.maps.Map('map', {
      center: new naver.maps.LatLng(x, y),
      zoom: 20,
    });

    new naver.maps.Marker({
      position: new naver.maps.LatLng(x, y),
      map: map,
    });

    mapRef.current = map;
  };

  useEffect(() => {
    naver.maps.Service.geocode(
      {
        query: '통영시 무전1길 64-11 되뫼골부대찌개',
      },
      function (status, response) {
        if (status === naver.maps.Service.Status.ERROR) {
          console.error('지도 정보를 불러오는 중 에러가 발생했습니다.');
        }

        const result = response.v2.addresses[0];
        const x = Number(result.x);
        const y = Number(result.y);

        initMap(x, y);
      },
    );

    return () => {
      if (mapRef.current) {
        mapRef.current.destroy();
      }
    };
  }, []);

  return (
    <>
      <Script
        type="text/javascript"
        src={`https://oapi.map.naver.com/openapi/v3/maps.js?ncpClientId=${process.env.NEXT_PUBLIC_NAVER_CLIENT_ID}&submodules=geocoder`}
      />
      <div id="map" style={{ width: '100%', height: '400px' }} />
    </>
  );
}

export default Map;

 

query의 주소는 지도가 잘 동작하는지 확인하기 위해 임시로 하드 코딩했습니다. 지도 동작 확인 후 prop으로 받도록 수정할 예정입니다.

 

DetailPage.tsx - 맛집 상세 페이지

import Divider from '@/app/components/common/Divider';
import VideoPlayer from '@/app/components/detail/VideoPlayer';
import IconMarker from '@/app/components/icons/IconMarker';
import Map from '@/app/components/Map/Map';
import axios from '@/app/lib/instance';

async function DetailPage({
  params,
}: {
  params: { channel: string; id: string };
}) {
  const { channel, id } = await params;

  const {
    data: { list },
  } = await axios.get(`${channel}/${id}`);

  const { videoId, thumbnail, timeline, address } = list;

  return (
    <div className="max-w-[46.25rem] mx-auto px-3 py-5 text-white">
      <div className="relative w-full aspect-[1.75/1]">
        <VideoPlayer videoId={videoId} lazy={thumbnail} timeline={timeline} />
      </div>
      <div className="mt-3 font-semibold line-clamp-2">{list.title}</div>
      <Divider />
      <div className="flex items-center gap-1">
        <IconMarker className="w-3.5 h-3.5" />
        {address}
      </div>
      <Map />
    </div>
  );
}

export default DetailPage;

error - naver is not defined

 

결과를 확인해보니 ReferenceError: naver is not defined 에러가 발생해 네이버 지도를 호출하지 못하고 있습니다.

Script 컴포넌트 strategy 옵션

 

Next 공식 문서를 확인해 보니 Script 컴포넌트의 strategy 옵션은 스크립트 로딩 시점을 제어할 수 있는 4개의 옵션을 제공합니다. afterInteractive가 default로 설정되어 있고, 이 옵션은 hydration이 발생한 후 Script를 로드한다고 하네요.

 

strategy="afterInteractive"hydration이 완료된 후에 스크립트를 로드하기 때문에,  naver 객체가 아직 정의되지 않아 naver is not defined 오류가 발생하는 것이었습니다.

 

Script 컴포넌트 - strategy props 수정

<Script
  strategy="beforeInteractive"
  type="text/javascript"
  src={`https://oapi.map.naver.com/openapi/v3/maps.js?ncpClientId=${process.env.NEXT_PUBLIC_NAVER_CLIENT_ID}&submodules=geocoder`}
/>;

Naver Map API - 지도 생성 결과

 

지도를 로드하는것은 성공했지만, 정상적인 위치가 아니라 이상한 위치가 보이고 있습니다.

Naver Map API - geocode response

 

응답을 확인해봐도 API 요청이 성공해 주소를 response로 받고 있습니다.

주소를 위도/경도 좌표(geocode)로 변환하는 로직의 문제는 아니기 때문에 지도를 생성하는 initMap 함수를 확인해 보겠습니다.

const initMap = (x: number, y: number) => {
  const map = new naver.maps.Map('map', {
    center: new naver.maps.LatLng(x, y),
    zoom: 20,
  });

  new naver.maps.Marker({
    position: new naver.maps.LatLng(x, y),
    map: map,
  });

  mapRef.current = map;
};

 

 

여러 방법을 시도해보다가 도저히 해결될 기미가 보이지 않아 공식 문서를 확인해 보겠습니다.

naver.maps.LatLng 사용법 - 공식 문서
geocode - x,y 응답 값

 

geocode의 response로 받은 x, y는 (경도, 위도)인데 naver.maps.LatLng의 매개변수는 (위도, 경도)로 전달해야 하네요🤣

API 사용 전 공식 문서를 꼼꼼히 읽는 습관이 아직도 부족하다는 것을 한 번 더 느끼게 됩니다.

 

매개변수 이름을 헷갈리지 않게 lat, lng으로 수정 후 동작을 확인해 보겠습니다.

const initMap = (lat: number, lng: number) => {
  const map = new naver.maps.Map('map', {
    center: new naver.maps.LatLng(lat, lng),
    zoom: 20,
  });

  new naver.maps.Marker({
    position: new naver.maps.LatLng(lat, lng),
    map: map,
  });

  mapRef.current = map;
};
  
useEffect(() => {
  naver.maps.Service.geocode(
    {
      query: '통영시 무전1길 64-11 되뫼골부대찌개',
    },
    function (status, response) {
      if (status === naver.maps.Service.Status.ERROR) {
        console.error('지도 정보를 불러오는 중 에러가 발생했습니다.');
      }

      const result = response.v2.addresses[0];
      const lng = Number(result.x);
      const lat = Number(result.y);

      initMap(lat, lng);
    },
  );

  return () => {
    if (mapRef.current) {
      mapRef.current.destroy();
    }
  };
}, []);

Naver Map API - 지도 생성 결과

 

이제 Naver Map API를 이용해 지도에 원하는 위치를 불러올 수 있게 되었습니다.

아직 마커와 지도뿐이지만 추후 여러 기능들을 테스트해보며 필요한 기능들을 추가해 보겠습니다!