티스토리 뷰

☁️환경☁️

  • Next.js
  • TypeScript
  • react-kakao-map-sdk

이번 쳅터에서는 코드 작성에대한 글을 작성해 보고자 한다.

 

멍청한 나는 kakao-map 공식문서만보고 javaScript로 필요한 기능을 모두 완성을 했는데 버그가 너무 많았다..

이벤트가 제대로 작동하지 않았던..

 

그러다 ` react-kakao-map-sdk ` 가 있다는 것을 알게 되었고 다시 리펙토링 하느라 고생했다^^.. 🤬🤬🤬

 

컴포넌트 별로 분리해서 TIL을 작성할 예정입니다. 너무 길어지기도 하고 나중에 제가 다시 보려면 

그 편이 더 좋을것 같아서.. ㅎ

 

이쁜 코드도 아니고 실수도 많은 코드일 것 같으니 참고만 해주시면 감사하겠습니다.

 

 

파일 구조

📦components
 ┗ 📂map
 ┃ ┣ 📂funtion
 ┃ ┃ ┣ 📜MapContainer.tsx
 ┃ ┃ ┣ 📜MapControls.tsx
 ┃ ┃ ┣ 📜Markers.tsx
 ┃ ┃ ┣ 📜PollutantSelector.tsx
 ┃ ┃ ┗ 📜PolygonLine.tsx
 ┃ ┣ 📂ui
 ┃ ┃ ┣ 📜Dot.tsx
 ┃ ┃ ┣ 📜MapIconButton.tsx
 ┃ ┃ ┣ 📜MapTextButton.tsx
 ┃ ┃ ┣ 📜Panel.tsx
 ┃ ┃ ┣ 📜PanelQualityLayout.tsx
 ┃ ┃ ┗ 📜Tooltip.tsx
 ┃ ┗ 📜AirMap.tsx

 

 

<MapContainer/> component

기능

-지도를 화면에 렌더링

 

 

1. react-kakao-maps-sdk에서 제공하는 <Map/> 컴포넌트로 지도를 그려줄 부분 만들기

import Script from "next/script";
import { Map } from "react-kakao-maps-sdk";

interface MapContainerProps {
  children: React.ReactNode;
}

export default function MapContainer({ children }: MapContainerProps) {
  return (
    <>
      <Script
        strategy="afterInteractive"
        src={`//dapi.kakao.com/v2/maps/sdk.js?appkey=${process.env.NEXT_PUBLIC_KAKAO_MAP_CLIENT}&autoload=false`}
        onLoad={handleScriptLoad}
      />
        <Map>
          {children}
        </Map>
    </>
  );
}

 

1편에서 말했던 `Script` 태그를 사용해 비동기로 로드를 해준다. 

 

✅Script는 무엇인가?

Next.js에서 제공하는 컴포넌트로 외부 스크립트를 로드할 때 사용되는데, 

일반 HTML에서 <script> 태그를 사용하는 것과 비슷하지만 Next.js에서 <Script>는 비동기적이고 최적화된 방식으로 로드할수 있게 도와준다.

 

왜 Script를 사용해야하는가?

Kakao지도 Api는 외부에서 제공하는 JavaScript 라이브러리이다. 

이 라이브러리는 로드된 후 Kakao 지도관련 기능을 사용할 수 있다. 

때문에 <Script> 태그를 통해 ` Kakao지도 Api ` 라이브러리를 비동기로 로드하고, 

준비가 되었을 때 지도를 화면에 렌더링할 수 있게 해준다. 

 

strategy="afterInteractive"

사용자가 페이지와 상호작용한 후에 스크립트를 로드하게 해준다. 

스크립트 로드 타이밍을 조절해 페이지 성능을 최적화할 수 있는 Next.js 기능인데, 

즉 페이지를 먼저 렌더링 후 필요할 때 지도를 로드할 수 있다.

 

 onLoad={handleScriptLoad}

`onLoad`는 스크립트가 성공적으로 로드된 후 호출되는 콜백 함수이다. 

 

✅ <Map/>

react-kakao-maps-sdk 라이브러리에서 제공하는 컴포넌트로, 

Kakao 지도 Api를 기반으로 React 스타일로 지도를 다룰 수 있게 도와준다. 

 

이제 지도위에서 그려질 컴포넌트들을 만들어서 {children}에 전달하면 <Map/> 내부에서 렌더링 된다.

      <MapContainer>
        <Markers />
        <PollutantSelector />
        <PolygonLine />
      </MapContainer>

 

 

2. kakao-maps-sdk가 완전히 로드되었을 때를 체크해주는 상태관리 함수 만들기

  const [isMapLoaded, setIsMapLoaded] = useState(false);

  const handleScriptLoad = () => {
    if (window.kakao && window.kakao.maps) {
      kakao.maps.load(() => {
        setIsMapLoaded(true);
      });
    }
  };

 

 

` kakao-maps-sdk `가 로드되면 `window` 객체에 `kakao`라는 글로벌 객체가 등록된다. 

`kakao` 안에는 `maps`라는 하위 객체가 포함되어있고, kakao 지도 관련된 모든 기능이 안에 들어있다. 

 

`kakao.maps.load()`는 kakao 지도 Api가 완전히 로드되었을 때 실행할 콜백을 지정한다. 

즉, 콜백이 실행되는 시점이 kakao 지도 Api를 제대로 사용할 수 있는 모든 기능이 준비된 상태이다. 

 

모든 기능을 사용할 준비가 된 시점을 `boolean` 값으로 체크하고

ture가 되었을 때 지도페이지를 렌더링해주었다.

 

✨ 알고가기

로드가 되었는지 안되었는지 상태관리를 해주지 않으면 ` kakao-maps-sdk `가 로드되기 전에 지도가 렌더링 될 수 있다.

 

처음에는 상태관리를 해주지 않았는데,

지도가 제대로 렌더링 되지 않는다거나 커스텀 오버레이들이 제대로 나타지 않았었다.

 

그렇다면 ` kakao-maps-sdk `가 뭐길래 그런걸까? 

 

 kakao-maps-sdk 

` kakao-maps-sdk `는

kakao에서 제공하는 지도API로 kakao지도 서비스를 웹페이지에 통합하여 사용할 수 있게 해주는 라이브러리이다. 

`JavaScript` 기반으로 지도를 사용할 수 있게 해주고 지도, 마커, 커스텀 오버레이 등을 페이지에 그릴 수 있게 해준다.

 

때문에 React에서 사용하는 경우 ` kakao-maps-sdk `가 브라우저에 완전히 로드된 후에 사용하고자 하는 기능들을 제대로 사용할 수 있다. 

 

3. onLoad에 상태관리 함수 전달하기

<Script
  strategy="afterInteractive"
  src={`//dapi.kakao.com/v2/maps/sdk.js?appkey=${process.env.NEXT_PUBLIC_KAKAO_MAP_CLIENT}&autoload=false`}
  onLoad={handleScriptLoad}
/>

 

 

 

이해하기 쉽게 앞에서 말한 설명을 다시 해보자면 

 

 onLoad={handleScriptLoad}

`onLoad`는 스크립트가 성공적으로 로드된 후 호출되는 콜백 함수이다. 

 

때문에 스크립트가 성공적으로 로드된 후 `handleScriptLoad 함수` 를 콜백할 것이고, 

`handleScriptLoad 함수` 안에서 지도를 사용할 수 있는 상태가 되었는지를 체크해 줄 것이다. 

 

4. 상태관리 함수를 이용해 지도 그려내기

const [isMapLoaded, setIsMapLoaded] = useState(false);

  return (
    <>
      {isMapLoaded && (
        <Map>
          {children}
        </Map>
      )}
    </>
  );

상태를 관리 값인  `isMapLoaded`가 ture가 되었을 때, 

`<Map/>` 컴포넌트를 그려주면 된다. 

 

5. 지도 초기화 및 상태관리

center={{
    lat: 37.5665,
    lng: 126.978,
  }}

center

지도의 초기 중심 좌표 설정(서울을 중심으로 지도가 처음 보여질 것이다.

 

  style={{
    width: "100%",
    height: "calc(100vh - 80px)",
  }}

style

지도가 그려질 영역만 설정해주었다. 

 

  level={13}

level

기본 줌레벨 설정 (초기 지도가 그려질 때 어떤 level로 보여질지)

 

  const { map, setMap } = useMapStore();
  
  onCreate={(mapInstance) => {
    if (!map) {
      setMap(mapInstance);
      mapInstance.setMinLevel(6);
      mapInstance.setMaxLevel(13);
    }
  }}

onCreate

지도 초기화 이벤트 

`onCreate` 이벤트는 kakao 지도가 처음 생성될 때 실행되는 이벤트이다. 

`onCreate`이벤트는 `mapInstance` 를 받아서 초기 설정을 진행하고 지도의 상태를 저장하는 역할을 해준다. 

 

➡️  (!map)

지도가 생생되지 않은 상태일 때만 초기화를 수행해준다. 

만약 지도가 생성된 상태라면 조건문을 통과하지 않기 때문에 불필요한 재설정이 방지된다.

 

➡️ setMap(mapInstance)

생성된 ` mapInstance `를 전역 상태로 관리하고 있는데 위에서 사용한 ` map  `으로 상태를 관리하고 있다.

 

➡️ mapInstance.setMinLevel(6) / mapInstance.setMaxLevel(13)

지도에서 사용자가 최대로 확대했을 때의 레벨, 최소로 축소했을 때의 레벨을 정할 수 있다.

 

 const debounceTimeout = useRef<NodeJS.Timeout | null>(null);
 
 onBoundsChanged={(mapInstance) => {
    if (mapInstance.getLevel() <= 10 && mapInstance.getLevel() >= 6) {
      if (debounceTimeout.current) {
        clearTimeout(debounceTimeout.current);
      }
      debounceTimeout.current = setTimeout(() => {
        });
      }, 1000);
    }
  }}

지도 경계가 변경될 때 실행되는 이벤트이다.

사용자가 지도를 이동하거나, 확대//축소 시 지도의 경계가 바뀌는데, 이를 감지해서 `setMapBounds`로 경계 정보를 업데이트한다. 

 

차후 대기질 정보를 요청할 때 보내야하는 값을 구하기 위함이다. 

 

if(mapInstance.getLevel() <= 10 && mapInstance.getLevel() >= 6)

지도의 zoom level 이 6 이상 10이하일 때만 경계 변경 처리를 실행한다. 

차후 대기질 정보를 보여줄 때 6 이상 10이하일 때만 보여주기 때문에 다른 레벨에서의 요청을 방지하기 위해서 설정했다. 

 

✅ const debounceTimeout = useRef<NodeJS.Timeout | null>(null);

디바운스 처리를 해주기 위한 함수이다. 

사용자가 정보가 필요한 부분으로 지도를 계속 이동하더라도 불필요한 연산을 방지하기 위해 useRef 훅을 사용했다.

useRef 훅을 사용했으므로  타이머 Id를 저장하고 이를 통해 이전에 설정된 타이머를 취소할 수 있게된다.

 

➡️ debounceTimeout.current

컴포넌트가 리렌더링될 때 초기화되지 않고 값을 저장한다. 

즉, 지도 이동시에도 값을 받아오지 않는다.

 

➡️ setTimeout(() => { ... }, 1000)

사용자가 지도를 움직인 후 1초동안 아무런 움직임이 없을 때 경계 값을 업데이트하도록 해두었다.

 

1초가 성능에 어떤 영향을 주겠냐라고 할 수도 있지만 사용자가 찰나 멈춘 순간에 데이터를 요청하는게 아닌

확실한 상태에서 값을 요청하길 바랬고, 그렇다고 너무 오랜 시간이 소요되면 사용자 입장에서 사용성이 안좋게 느낄 수 있을 것 이라고 생각해 지극히 개인적으로 시간을 설정해 두었다😅

 

       const { setMapBounds } = useMapStore();
       
       const bounds = mapInstance.getBounds();
        const swLatlng = bounds.getSouthWest();
        const neLatlng = bounds.getNorthEast();
        setMapBounds({
          minLat: swLatlng.getLat(),
          minLng: swLatlng.getLng(),
          maxLat: neLatlng.getLat(),
          maxLng: neLatlng.getLng(),
        });

 

bounds = mapInstance.getBounds();

현재 지도의 경계 가져오기

 

➡️ bounds.getSouthWest()

지도의 남서쪽 경계 좌표 반환

 

➡️ bounds.getNorthEast()

지도의 북동쪽 경계 좌표 반환

 

setMapBounds

사용자의 값은 다른 컴포넌트에서 대기질 값 요청 시 사용되어야 하므로, 

전역상태관리로 상태를 업데이트해주었다. 

 

 

<전체코드>

import Loader from "@/components/common/Loader";
import { useMapStore } from "@/stores/map/useMapStore";
import Script from "next/script";
import React, { useEffect, useRef, useState } from "react";
import { Map } from "react-kakao-maps-sdk";

interface MapContainerProps {
  children: React.ReactNode;
}

export default function MapContainer({ children }: MapContainerProps) {
  const debounceTimeout = useRef<NodeJS.Timeout | null>(null);
  const { userLocation, setMapBounds, map, setMap } = useMapStore();
  const [isMapLoaded, setIsMapLoaded] = useState(false);

  const handleScriptLoad = () => {
    if (window.kakao && window.kakao.maps) {
      kakao.maps.load(() => {
        setIsMapLoaded(true);
      });
    }
  };

  return (
    <>
      <Script
        strategy="afterInteractive"
        src={`//dapi.kakao.com/v2/maps/sdk.js?appkey=${process.env.NEXT_PUBLIC_KAKAO_MAP_CLIENT}&autoload=false`}
        onLoad={handleScriptLoad}
      />
      {isMapLoaded && (
        <Map
          center={{
            lat: userLocation ? userLocation.lat : 37.5665,
            lng: userLocation ? userLocation.lng : 126.978,
          }}
          style={{
            width: "100%",
            height: "calc(100vh - 80px)",
          }}
          level={13}
          onCreate={(mapInstance) => {
            if (!map) {
              setMap(mapInstance);
              mapInstance.setMinLevel(6);
              mapInstance.setMaxLevel(13);
            }
          }}
          onBoundsChanged={(mapInstance) => {
            if (mapInstance.getLevel() <= 10 && mapInstance.getLevel() >= 6) {
              if (debounceTimeout.current) {
                clearTimeout(debounceTimeout.current);
              }
              debounceTimeout.current = setTimeout(() => {
                const bounds = mapInstance.getBounds();
                const swLatlng = bounds.getSouthWest();
                const neLatlng = bounds.getNorthEast();
                setMapBounds({
                  minLat: swLatlng.getLat(),
                  minLng: swLatlng.getLng(),
                  maxLat: neLatlng.getLat(),
                  maxLng: neLatlng.getLng(),
                });
              }, 1000);
            }
          }}
        >
          {children}
        </Map>
      )}
    </>
  );
}

 

 

⛑️.eslint 에러가 발생하게 되면 아래 코드를 작성해주면 된다.

declare global {
  interface Window {
    kakao: any;
  }
}

 

kakao 공식문서 참고 자료

https://react-kakao-maps-sdk.jaeseokim.dev/

 

 

 

'Boundary-Intern( Next.js & yarn )' 카테고리의 다른 글

kakao map 사용하기 1 (셋팅)  (0) 2024.09.07