Next.js 14 로 프로젝트를 진행하면서 카카오 지도와 로컬을 사용해야 했다.
공식 문서를 보니 정리도 잘 되어 있고 비교적 쉬운 난이도라 생각했는데, 생각보다 우여곡절이 많았다.
이유 중 하나가 next.js 14 관련 글이 적어 공식문서와 카카오 문서를 여러번 읽어가면서 하나씩 해보는 방법 밖엔 없었다...
그렇기에 나와 같은 분들에게 도움이 되고자 정보를 공유하고 더 나은 방법이 있거나 틀린 부분이 있다면 피드백을 받고자 작성한다.
우선, kakao developer 사이트에서 key 값을 받으면 script 태그를 선언해야 한다.
카카오 공식 문서에 보면 스크립트 태그는 HTML 파일안의 head, body 등 어떠한 위치에 넣어도 상관없지만, 반드시 실행 코드보다 먼저 선언되어야 한다고 나와있다.
<script type="text/javascript" src="//dapi.kakao.com/v2/maps/sdk.js?appkey=발급받은 APP KEY를 넣으시면 됩니다."></script>
그래서 나는 <head>
태그 안에 script 태그를 선언해줬다. 그러면 스크립트가 실행 코드보다 먼저 로드되고 실행된다.
⭐️이때 모든 route 에서 third-party 스크립트를 로드하려면 Next.js 에서 제공하는 Script 태그를 사용해 root layout 에 추가해야 한다.
import Script from 'next/script';
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="ko">
<head>
<Script
type="text/javascript"
src={`//dapi.kakao.com/v2/maps/sdk.js?appkey=${process.env.NEXT_PUBLIC_KAKAO_API_KEY}&libraries=services,clusterer,drawing&autoload=false`}
/>
</head>
<body style={{ display: 'flex' }}>
<ReactQueryProvider>
<StyledComponent>
{children}
</StyledComponent>
</ReactQueryProvider>
</body>
</html>
);
}
이렇게 카카오 API SDK 를 로드 하고 나서 실제로 지도를 불러오기 위한 코드를 작성해줘야 한다.
const mapEl = useRef<HTMLDivElement>(null);
useEffect(() => {
if (!mapEl.current) return;
const options = {
center : new kakao.maps.LatLng(latitude, longitude)
level : 3
}
const map = new kakao.maps.Map(mapEl.current!, option)
},[])
return <div ref={mapEl} style={{ width: '100%', height: '100%' }}></div>;
이처럼 script 를 먼저 선언하고, 지도를 불러오는 로직을 공식문서에 나와 있는 대로 작성했는데, 아래와 같은 에러가 나온다.
Cannot read properties of undefined (reading 'maps')
해당 오류는 주로 script 가 로드되기 전에 객체에 접근해서 생기는 오류이다. 즉, script 가 로드되기 전에 useEffect 훅이 먼저 실행되어서 maps 객체를 찾을 수 없어 에러가 발생하는 것이다.
혹시나 스크립트를 잘 못 작성한걸까 싶어서 공식문서를 다시 살펴보니 Script 에는 전략 속성을 사용해 로드 동작을 조정할 수 있었다.
afterInteractive 는 기본값 (default) 이고, 읽어보니 beforeInteractive 가 Next.js 코드 및 hydration 이 되기 전에 스크립트가 로드되는 속성이므로 strategy 를 추가해줬다.
<head>
<Script
type="text/javascript"
src={`//dapi.kakao.com/v2/maps/sdk.js?appkey=${process.env.NEXT_PUBLIC_KAKAO_API_KEY}&libraries=services,clusterer,drawing&autoload=false`}
strategy="beforeInteractive"
/>
</head>
그러나 여전히 같은 에러가 발생했다. 혹시나 싶어 strategy 를 afterInteractive, beforeInteractive, lazyOnload 로 변경해봐도 똑같은 에러가 발생했다.
해결방법을 찾다가 kakao dev 에서 관리자(?) 분이 작성한 글을 발견했다.
⭐️스크립트 동적 로딩(autoload=false
)을 사용한 경우에는 내가 필요한 시점에 스크립트를 불러와서 실행을 하게 되므로 이 시점을 알려주는 kakao.maps.load 함수를 사용해야 한다는 것이다..!
문서를 찾아보니 스크립트의 로딩이 끝나기 전에 객체에 접근하려고 하면 에러가 발생하기 때문에 = 내가 겪은 에러
로딩이 끝나는 시점에 콜백함수를 통해 객체에 접근할 수 있도록 load 를 사용한다고 되어 있다.
kakao.maps.load 함수를 사용하기 위해서 kakao 객체에 접근해야 하는데 script 에 이미 kakao api 를 설치했기 때문에 window 객체 안에 kakao 가 들어가 있다.
따라서 window 에서 kakao 객체를 뽑아서 사용할 수 있다.
const mapEl = useRef<HTMLDivElement>(null);
useEffect(() => {
const {kakao} = window;
if(!kakao) return;
kakao.maps.load(() => {
if (!mapEl.current) return;
const center = new kakao.maps.LatLng(latitude, longitude);
const options = {
center,
level: 3,
};
const map = new kakao.maps.Map(mapEl.current!, options);
});
},[])
return <div ref={mapEl} style={{ width: '100%', height: '100%' }}></div>;
app/layout.tsx
import Script from 'next/script';
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="ko">
<head>
<Script
type="text/javascript"
src={`//dapi.kakao.com/v2/maps/sdk.js?appkey=${process.env.NEXT_PUBLIC_KAKAO_API_KEY}&libraries=services,clusterer,drawing&autoload=false`}
/>
</head>
<body style={{ display: 'flex' }}>
<ReactQueryProvider>
<StyledComponent>
{children}
</StyledComponent>
</ReactQueryProvider>
</body>
</html>
);
}
components/Map.tsx
const Map = () => {
const mapEl = useRef<HTMLDivElement>(null);
useEffect(() => {
const {kakao} = window;
if(!kakao) return;
kakao.maps.load(() => {
if (!mapEl.current) return;
const center = new kakao.maps.LatLng(latitude, longitude);
const options = {
center,
level: 3,
};
const map = new kakao.maps.Map(mapEl.current!, options);
});
},[])
return <div ref={mapEl} style={{ width: '100%', height: '100%' }}></div>;
}
혹시나 위 방법으로도 되지 않거나 필요할때 동적으로 스크립트를 생성해 지도를 불러오고 싶다면 아래와 같은 방법을 사용해도 된다.
⭐️우선 직접 script 를 생성할 것이므로 app/layout.tsx 에 Script 태그 부분은 삭제한다.
그 다음 useEffect 훅을 이용해 동적으로 script 를 생성하는 코드를 작성해준다.
components/Map.tsx
useEffect(() => {
const script = document.createElement('script');
script.async = false;
script.src = `//dapi.kakao.com/v2/maps/sdk.js?appkey=${process.env.NEXT_PUBLIC_KAKAO_API_KEY}&libraries=services,clusterer,drawing&autoload=false`;
document.head.appendChild(script);
script.onload = () => {
kakao.maps.load(() => {
if (!mapEl.current) return;
const center = new kakao.maps.LatLng(latitude, longitude);
const options = {
center,
level: 3,
};
const map = new kakao.maps.Map(mapEl.current!, options);
});
};
}, []);
document.createElement 이용해 script 태그를 직접 만들어 준다.
그걸 async 방식으로 만들어 준 다음 script.src 에 카카오 api 주소를 넣어준다.
그리고 head 부분에 해당 script 를 붙여주면 앞서 Script 태그로 작성한 것 처럼 생성 된다.
script.onload 함수를 통해 스트립트가 로드된 후에 특정 함수를 실행하도록 설정한다. 여기서는 지도를 불러오는 로직을 작성했다.
Next.js 에서 (14버전) 카카오 API 를 사용하는 방법을 정리하면 아래와 같다.
Next.js(14ver) app/layout.tsx 파일에서 head 부분에 Script 태그 선언한다.
<head>
<Script
type="text/javascript"
src={`//dapi.kakao.com/v2/maps/sdk.js?appkey=${process.env.NEXT_PUBLIC_KAKAO_API_KEY}&libraries=services,clusterer,drawing&autoload=false`}
/>
</head>
<body>
...생략
</body>
useEffect 훅을 사용해 Kakao Maps API가 로드된 후에 지도를 초기화하는 로직을 작성한다.
useEffect(() => {
const { kakao } = window;
if(!kakao) return;
kakao.maps.load(() => {
/// 지도 불러오는 로직
})
},[])
script 가 페이지 렌더링 전에 로드 되므로 모든 컴포넌트에서 바로 API 를 사용할 수 있다.
script 로직이 layout.tsx 파일에 작성되어 있어 컴포넌트에서 별도로 스크립트 로직을 작성하지 않아도 된다. 코드가 분리되어 깔끔하다.
모든 페이지에서 script 를 로드하기 때문에 필요 없는 페이지에서도 불필요한 로드가 발생할 수 있다.
특정 컴포넌트에서만 API 를 사용할때는 위 방법은 불필요한 로드가 발생하고 따라서 로드 시간이 증가되는 문제가 생길 수 있다.
app/layout.tsx 파일에서 head 부분에 Script 태그를 삭제한다.
동적으로 script 를 생성한 후, 로드 되었을때 지도를 초기화하는 로직을 작성한다.
useEffect(() => {
const script = document.createElement('script');
script.async = false;
script.src = `//dapi.kakao.com/v2/maps/sdk.js?appkey=${process.env.NEXT_PUBLIC_KAKAO_API_KEY}&libraries=services,clusterer,drawing&autoload=false`;
document.head.appendChild(script);
script.onload = () => {
kakao.maps.load(() => {
/// 지도 불러오는 로직
});
};
}, []);
특정 컴포넌트에서만 script 를 로드할 수 있어 필요한 경우에만 로드할 수 있다.
단, 필요한 컴포넌트마다 script 를 로드하는 코드를 작성해야 한다.
나는 지도가 서비스 전반에 사용되어야 하므로 첫번째 방법인 head 부분에 script 를 삽입해 사용했다.