오늘은 맛집 상세 페이지에 지도를 추가해 보겠습니다. 지도는 Naver Map API를 이용해 볼 계획입니다.
0️⃣ Naver Map API 선택 이유
- 프레임워크에 의존하지 않고 독립적으로 동작하기 때문에 불필요한 의존성을 최소화할 수 있고, React & Next.js로 제작된 맛길 프로젝트에 적합할 것 같아 선택했습니다.
- DOM 처리 및 웹 브라우저 호환 코드를 내장하고 있어 크로스 브라우징 이슈를 최소화하면서 손쉽게 지도 기능을 구현할 수 있을 거라 생각해 선택했습니다.
- 별도의 CSS를 필요로 하지 않도록 설계된 내용을 보고, 개발 부담을 줄일 수 있을 것 같아서 선택했습니다.
- 모바일 환경에서도 최적화된 성능을 제공하기 때문에 별도의 최적화 작업이 필요하지 않아 개발 부담을 줄일 수 있을 것 같아 선택했습니다.
1️⃣ NAVER CLOUD PLATFORM 인증키 발급
Application 이름은 서비스와 동일하게 matgil로 설정하겠습니다.
맛길 서비스의 지도에서는 길 찾기 기능은 필요하지 않기 때문에 Directions 옵션은 선택하지 않았습니다.
Application 등록을 완료했고, 인증 정보 버튼을 클릭해 보니 Client ID가 생겼습니다.
Client ID는 프로젝트에서 Naver Map API를 이용할 때 사용되는 API Key이기 때문에 외부 노출을 방지하기 위해 .env 파일에 저장했습니다.
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;
결과를 확인해보니 ReferenceError: naver is not defined 에러가 발생해 네이버 지도를 호출하지 못하고 있습니다.
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`}
/>;
지도를 로드하는것은 성공했지만, 정상적인 위치가 아니라 이상한 위치가 보이고 있습니다.
응답을 확인해봐도 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;
};
여러 방법을 시도해보다가 도저히 해결될 기미가 보이지 않아 공식 문서를 확인해 보겠습니다.
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를 이용해 지도에 원하는 위치를 불러올 수 있게 되었습니다.
아직 마커와 지도뿐이지만 추후 여러 기능들을 테스트해보며 필요한 기능들을 추가해 보겠습니다!
'프로젝트 > Next+TypeScript' 카테고리의 다른 글
[Coworkers] IOS 이미지 업로드 이슈 (0) | 2025.02.12 |
---|---|
[Coworkers] 리팩토링 (0) | 2025.02.07 |
[맛길] 상세 페이지 - Youtube 영상 추가 (1) | 2025.02.06 |
[맛길] 동적 라우팅(Dynamic Routing) 적용 (1) | 2025.02.05 |
[Coworkers] 할 일 리스트 페이지 작업 (협업) (1) | 2025.02.04 |