리액트에서 카카오 지도 API를 써보았습니다. 근데 이제 타입스크립트를 곁들인..

성택·2022년 5월 24일
6

프로젝트에서 지도 기능이 필요해서 카카오 지도 API를 써보았다.

네이버 지도 API도 있지만 카카오를 사용한 이유는 키워드로 검색하여 장소를 찾는 기능이 필요했는데 네이버 지도 REST API에는 해당 기능이 없었기 때문이다.

React + TypeScript 환경에서 카카오맵 API를 썼기 때문에 공식 홈페이지에 있는 자바스크립트 예제 코드에서 쬬끔 변형해서 썼는데,

이에 대한 기록이 필요한 것 같아 글을 쓰며 정리해보고자 한다.

1. kakao developers APP KEY 발급

카카오 지도 API 를 이용하기 위해서는 우선 카카오 디벨로퍼에 내 어플리케이션을 등록하고 KEY를 발급받아야 한다.

우선 카카오 지도 API 사이트로 가보자 https://apis.map.kakao.com/

사이트에 가보면 위 사진에 표시해 놓은 것 처럼 APP KEY 발급이라는 버튼이 있다.

클릭해서 들어가보면 kakao developers 의 내 애플리케이션 페이지로 이동한다.

이제 이 페이지에서 애플리케이션 추가하기 버튼을 클릭하고 내가 만든(혹은 만들 예정인) 애플리케이션 이름을 등록하면 된다.

그러면 위 사진처럼 내가 등록한 애플리케이션이 보이게 된다. 클릭해서 들어가보면 APP KEY를 확인할 수 있다.

블러로 가려놓긴 했는데 저 위치에서 확인할 수 있는 JavaScript 키를 사용해 줄 것이다.

이제 마지막으로 내 애플리케이션의 URL을 등록해주기만 하면 준비 끝이다. 앱 설정 메뉴의 플랫폼 페이지로 가보자.

이번에 만들 프로젝트는 웹사이트이므로 Web 사이트 URL을 입력해주면 된다.

아직 배포가 안된 상태면 http://localhost:3000 으로 등록해주면 로컬에서도 잘 동작한다. 주의할 점은 끝에 / 는 빼야한다.

2. APP KEY를 내 프로젝트에 넣기

카카오 지도 API 공식 홈페이지에 보면 실제 지도를 그릴 API를 불러오려면

<script type="text/javascript" src="//dapi.kakao.com/v2/maps/sdk.js?appkey=발급받은 APP KEY를 넣으시면 됩니다."></script>

위와 같은 스크립트 태그를 이용해서 불러와야 한다고 설명하고 있다.

검색 기능 등 여러 가지 기능을 지원하는 지도 라이브러리를 함께 사용하기 위해서는 라이브러리도 함께 불러와야 한다.

나는 장소 검색 기능이 필요해서 해당 기능을 제공해주는 services 라이브러리도 함께 불러 왔다.

<!-- services 라이브러리 불러오기 -->
<script type="text/javascript" src="//dapi.kakao.com/v2/maps/sdk.js?appkey=발급받은 APP KEY&libraries=services"></script>

React 에서는 index.html 에 넣으면 된다.

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8" />
  <link rel="icon" type="image/svg+xml" href="/src/favicon.ico" />
  
  <script type="text/javascript"
   				src="//dapi.kakao.com/v2/maps/sdk.js?appkey=발급받은 APP KEY&libraries=services"></script>
  <title>웹 페이지 타이틀</title>
</head>

<body>
  <div id="root"></div>
  <script type="module" src="/src/main.tsx"></script>
</body>

</html>

근데 이렇게 index.html 에 스크립트 태그를 이용해서 불러오는 코드를 넣어주게 되면 내 APP KEY가 그대로 노출된다는 단점이 있다.

물론 개발자가 지정한 URL에서만 카카오 지도 API를 불러올 수 있지만, 대다수의 개발자들이 로컬 URL을 이용해서 개발을 하기 때문에 키가 노출되서 좋을 건 하나도 없다.

그래서 .env 파일을 이용해서 APP KEY를 숨기려고 했는데, Vite로 빌드한 프로젝트에서 script 태그 내의 APP 키를 환경 변수로 넣는 방법을 아직 못찾았다.

(CRA에서 하는 방법은 찾았는데, Vite에 대한 정보는 생각보다 찾기 어려운 것 같다.)

Vite와 환경 변수에 대해 좀 더 공부하고 해당 주제에 대한 글을 따로 포스팅하도록 하겠다.

3. 원하는 페이지에서 카카오 지도 가져오기

이제 원하는 페이지에서 카카오 지도 api를 사용할 수 있게 되었다.

공식 홈페이지에 Docs와 더불어 코드 샘플을 통해 설명들이 잘 되어있으니 참고하면서 필요한 기능을 쓰면 된다.

일단 빈 지도를 하나 가져와 보겠다. 공홈에서는 이런 예제로 설명되어있다.

var mapContainer = document.getElementById('map'), // 지도를 표시할 div 
    mapOption = { 
        center: new kakao.maps.LatLng(33.450701, 126.570667), // 지도의 중심좌표
        level: 3 // 지도의 확대 레벨
    };

// 지도를 표시할 div와  지도 옵션으로  지도를 생성합니다
var map = new kakao.maps.Map(mapContainer, mapOption);

위 코드를 그대로 복붙하면 내가 원하는 페이지에서 지정한 좌표를 중심으로 한 지도가 생기는 것을 볼 수 있다.

지도를 표시할 용도로 지정한 div와 옵션을 인수로 전달하면서 지도 객체를 생성하고, 생성된 지도 객체를 map이라는 변수에 할당해주는 것이다.

그런데 위의 예시 코드는 리액트에서는 부적합한 코드라고 할 수 있는데, DOM 셀렉터 함수로 특정 DOM 요소를 조작하는 것을 리액트에서는 지양하기 때문이다.

( 참고 : https://mingule.tistory.com/61 )

나는 map 객체를 표시할 div를 지정하기 위한 용도로 useRef 훅을 사용했다. useRef는 .current 프로퍼티로 전달된 인자로 초기화된 ref 객체를 반환한다.

export const Map = () => {
  const mapRef = useRef<HTMLDivElement>(null);
	useEffect(() => {
    const container = mapRef.current;
    console.log(container);
  }, []);
	
  return (
    <>
    	<div></div>
    </>
  )
}

mapRef 변수에 null 값을 가지고 있는 ref 객체를 할당하고, mapRef의 .current 를 다시 container 변수에 할당했다. (mapRef 의 참조를 container 변수에 할당)

타입스크립트에서는 타입을 미리 지정해주어야 하는데, 우리가 mapRef와 연결할 엘리먼트는 div 이기 때문에 <HTMLDivElement> 타입을 지정해준다.

지금은 container (= mapRef.current ) 가 null 값을 가지고 있는데, 변경하려면 특정 element와 mapRef 의 ref 를 연결해주어야 한다.

export const Map = () => {
  const mapRef = useRef<HTMLDivElement>(null);
	useEffect(() => {
    const container = mapRef.current;
    console.log(container);
  }, []);
	
  return (
    <>
    	<div ref={mapRef}></div>
    </>
  )
}

그 후 container 를 출력하면 div 태그가 출력되는 것을 알 수 있다.

그 다음에는 options 변수를 생성해서 지도의 옵션을 지정해주고, 지도를 표시할 div로 container 변수를 사용해서 지도 객체를 생성하면 된다.

주의할 점은 지도 객체를 생성하는 것만으로는 지도가 보이지 않고 지도를 표시할 div에 width와 height를 지정해주어야 지도가 보이니까 반드시 지정해주어야 한다.

예제 코드처럼 kakao 라는 생성자를 통해 객체를 생성하려고 하니 어? kakao가 뭔디요? 하는 오류를 띄운다.

const { kakao } = window;
이렇게 window 객체에 kakao라는 값을 직접 추가해줘야 한다.

export const Map = () => {
  const { kakao } = window;
  const mapRef = useRef<HTMLDivElement>(null);
	useEffect(() => {
    const container = mapRef.current;
    const options = {
      center: new kakao.maps.LatLng(33.450701, 126.570667),
      level: 5,
    };
    // 지도 객체 생성
    const map = new kakao.maps.Map(container, options);

  }, []);
	
  return (
    <>
    	<Div ref={mapRef}></Div>
    </>
  )
}

const Div = styled.div`
  width: 100vw;
  height: 100vh;
`;

이제 빈 지도 하나를 출력하는데 성공했다. 카카오 지도라 그런지 예제 코드에 있던 좌표는 카카오 본사 좌표였다.

4. 키워드를 검색해서 지도에 마커 표시하기

그 다음에는 키워드를 검색해서 가져온 빈 지도에 마커를 표시해보자. 공홈에는 이런 예제 코드로 설명하고 있다.

// 마커를 클릭하면 장소명을 표출할 인포윈도우 입니다
var infowindow = new kakao.maps.InfoWindow({zIndex:1});

var mapContainer = document.getElementById('map'), // 지도를 표시할 div 
    mapOption = {
        center: new kakao.maps.LatLng(37.566826, 126.9786567), // 지도의 중심좌표
        level: 3 // 지도의 확대 레벨
    };  

// 지도를 생성합니다    
var map = new kakao.maps.Map(mapContainer, mapOption); 

// 장소 검색 객체를 생성합니다
var ps = new kakao.maps.services.Places(); 

// 키워드로 장소를 검색합니다
ps.keywordSearch('이태원 맛집', placesSearchCB); 

// 키워드 검색 완료 시 호출되는 콜백함수 입니다
function placesSearchCB (data, status, pagination) {
    if (status === kakao.maps.services.Status.OK) {

        // 검색된 장소 위치를 기준으로 지도 범위를 재설정하기위해
        // LatLngBounds 객체에 좌표를 추가합니다
        var bounds = new kakao.maps.LatLngBounds();

        for (var i=0; i<data.length; i++) {
            displayMarker(data[i]);    
            bounds.extend(new kakao.maps.LatLng(data[i].y, data[i].x));
        }       

        // 검색된 장소 위치를 기준으로 지도 범위를 재설정합니다
        map.setBounds(bounds);
    } 
}

// 지도에 마커를 표시하는 함수입니다
function displayMarker(place) {
    
    // 마커를 생성하고 지도에 표시합니다
    var marker = new kakao.maps.Marker({
        map: map,
        position: new kakao.maps.LatLng(place.y, place.x) 
    });

    // 마커에 클릭이벤트를 등록합니다
    kakao.maps.event.addListener(marker, 'click', function() {
        // 마커를 클릭하면 장소명이 인포윈도우에 표출됩니다
        infowindow.setContent('<div style="padding:5px;font-size:12px;">' + place.place_name + '</div>');
        infowindow.open(map, marker);
    });
}

예제 코드를 보면 장소 검색 객체를 생성하고, 키워드로 장소를 검색한 뒤에, 지도에 해당되는 장소에 마커를 표시해주는 로직임을 알 수 있다.

아까 작성한 코드에서 필요한 부분만 가져와서 검색 결과를 마커로 표시되게 해보자. 우선 최종 코드의 형태는 아래와 같다.

export const Map = () => {
  const { kakao } = window;
  const mapRef = useRef<HTMLDivElement>(null);

  useEffect(() => {
    const container = mapRef.current;
    const options = {
      center: new kakao.maps.LatLng(33.450701, 126.570667),
      level: 5,
    };
    // 지도 객체 생성
    const map = new kakao.maps.Map(container, options);
    
    // 장소 검색 객체 생성
    const places = new kakao.maps.services.Places(map);
    
    // 키워드 검색이 끝나고 호출될 콜백 함수
    const placesSearchCB = (data, status, pagination) => {
      if (status === kakao.maps.services.Status.OK) {
        for (let i = 0; i < data.length; i++) {
          displayMarker(data[i]);
        }
      }
    };

    // 키워드로 장소 검색
    places.keywordSearch("치킨", placesSearchCB, {
      useMapBounds: true,
    });

    // 지도에 마커를 표시해주는 함수
    const displayMarker = (place) => {
      const marker = new kakao.maps.Marker({
        map: map,
        position: new kakao.maps.LatLng(place.y, place.x),
      });
    };
  }, []);
  
  return (
    <>
    	<Div ref={mapRef}></Div>
    </>
  )
}

const Div = styled.div`
  width: 100vw;
  height: 100vh;
`;

KeywordSearsh 메서드는 3개의 인수를 가지며, 각각 검색할 키워드, 검색 결과를 받는 콜백 함수, 다양한 옵션들을 파라미터를 통해 적용할 수 있는 options(생략 가능) 이다.

나는 지도의 범위에서만 검색하기 위해 useMapBounds 값을 true로 변경하였다. 기본값은 false이며 변경해주지 않으면 전국에서 해당 키워드의 검색 결과를 확인할 수 있다.

다양한 옵션이 있으니 이 부분은 Docs 를 참조해서 추가하면 된다. ( 참고 : https://apis.map.kakao.com/web/documentation/#services_Places_keywordSearch)

keywordSearch 메서드의 콜백 함수도 3개의 인수를 가지는데, 각각 result : 결과 목록, status : 응답 코드, pagination : 검색 결과의 페이징을 담당하는 클래스객체 이다.

나는 status 의 값이 성공이면 지도에 마커를 표시해주는 함수는 displayMarker 함수를 호출하고 이를 검색 결과 데이터의 요소를 순회해서 반복문으로 처리하였다.

    // 키워드 검색이 끝나고 호출될 콜백 함수
    const placesSearchCB = (data, status, pagination) => {
      if (status === kakao.maps.services.Status.OK) {
        for (let i = 0; i < data.length; i++) {
          displayMarker(data[i]);
        }
      }
    };

이렇게 작성하여 현재 내가 지정한 지도의 범위에서 해당되는 키워드 ( 현재는 "치킨" 으로 하드 코딩 )의 결과 장소를 마커로 표시해준다.

카카오 본사 근처에는 치킨집이 하나밖에 없나보다. 하긴 제주도니까 뭐.

"치킨"이라는 키워드로 하드코딩 되어있는 부분은 state 를 이용해서 변경되게 만들어주면 검색되는 키워드도 변경할 수 있다.

5. 마커를 클릭하면 해당 장소의 정보 띄워주기

현재는 지도에 마커를 표시해줄뿐 마커에는 어떠한 기능이 없다. 마커에 이벤트를 걸어서 클릭되면 해당 장소의 정보를 띄워주도록 해보자.

우선 그 장소의 데이터가 필요한데 keywordSearch 메서드의 콜백 함수 중 첫 번째 인수가 바로 이 데이터 값을 우리에게 제공해준다.

    const placesSearchCB = (data, status, pagination) => {
      if (status === kakao.maps.services.Status.OK) {
        console.log(data);
        
        for (let i = 0; i < data.length; i++) {
          // displayMarker 함수의 매개변수로 해당되는 데이터 값을 전달
          displayMarker(data[i]);
        }
      }
    };

이런 식으로 첫 번째 인수를 출력해보면 저 치킨집의 정보를 확인할 수 있다. (물론 결과값이 많으면 많은 장소의 데이터를 제공해 줄 것이다.)
저 치킨집은 멕시카나 치킨이었네용? 친절하게 카카오 지도 링크 URL도 준다.

displayMarker 함수에 매개 변수로 마커가 표시하는 장소의 정보를 전달해주면 이 정보들을 마커의 클릭 이벤트에 활용할 수 있다.

그럼 다음으로 displayMarker 함수에서 각각의 마커에 정보를 담을 state를 추가하고 setState를 통해 정보를 전달해보자.

먼저 우리가 담아야 하는 데이터는 전부 스트링으로 되어있는 프로퍼티를 가지고 있으니까 타입 선언부터 하자.

type Place = {
  place_name: string;
  distance: string;
  place_url: string;
  category_name: string;
  address_name: string;
  road_address_name: string;
  id: string;
  phone: string;
  category_group_code: string;
  category_group_name: string;
  x: string;
  y: string;
};

네. 노가다 였습니다. 객체는 전부 같은 타입일 때 한번에 타입 지정해주는 기능 없나? 한 번 찾아봐야겠다.

어쨋든 이렇게 타입 지정을 해주고 state를 추가해주자.

const [selectedPlace, setSelectedPlace] = useState<Place>();

마지막으로 displayMarker 함수에서 우리가 만든 state에 데이터를 전달하는 클릭 이벤트를 만들어주자.

    const displayMarker = (place) => {
      const marker = new kakao.maps.Marker({
        map: map,
        position: new kakao.maps.LatLng(place.y, place.x),
      });
      kakao.maps.event.addListener(marker, "click", function () {
        setSelectedPlace(place);
      });
    };

이제 마커를 클릭할 때마다 selectedPlace 에는 해당 마커의 정보들이 담기게 된다. 이 정보 중에 사용할 데이터들만 뽑아서

삼항 연산자를 통해 모달창처럼 만들어주면 이 기능도 완성이다. 최종 완성된 코드는 다음과 같다.

export const Map = () => {
  const { kakao } = window;
  const mapRef = useRef<HTMLDivElement>(null);
  const [selectedPlace, setSelectedPlace] = useState<Place>();
  const [isLoading, setIsLoading] = useState(false);
  const { query } = useParams();

  useEffect(() => {
    const container = mapRef.current;
    const options = {
      center: new kakao.maps.LatLng(33.450701, 126.570667),
      level: 5,
    };

    const map = new kakao.maps.Map(container, options);
    const places = new kakao.maps.services.Places(map);

    const placesSearchCB = (data, status, pagination) => {
      if (status === kakao.maps.services.Status.OK) {
        console.log(data);
        for (let i = 0; i < data.length; i++) {
          displayMarker(data[i]);
        }
      }
    };

    places.keywordSearch("치킨", placesSearchCB, {
      useMapBounds: true,
    });

    const displayMarker = (place) => {
      const marker = new kakao.maps.Marker({
        map: map,
        position: new kakao.maps.LatLng(place.y, place.x),
      });
      kakao.maps.event.addListener(marker, "click", function () {
        setSelectedPlace(place);
      });
    };
  }, []);

  return (
    <>
      <Div ref={mapRef}></Div>
      {selectedPlace && (
        <PlaceInfo>
          <PlaceName>{selectedPlace.place_name}</PlaceName>
          <hr></hr>
          <PlaceDetail>{selectedPlace.road_address_name}</PlaceDetail>
          <PlaceDetail>{selectedPlace.phone}</PlaceDetail>
          <a href={selectedPlace.place_url}>
            <StyleBtn>카카오 지도로 보기</StyleBtn>
          </a>
        </PlaceInfo>
      )}
    </>
  );
};
const Div = styled.div`
  width: 100vw;
  height: 100vh;
  z-index: 0;
`;
const Input = styled.input`
  position: absolute;
  top: 10%;
  left: 10%;
  z-index: 10;
`;
const Btn = styled.button`
  position: absolute;
  top: 10%;
  left: 50%;
  z-index: 10;
`;
const PlaceInfo = styled.div`
  background-color: white;
  position: absolute;
  bottom: 3vh;
  z-index: 1;
  width: 100%;
  padding: 24px;
  border-radius: 16px;
`;
const PlaceName = styled.div`
  font-size: 1.3rem;
`;
const PlaceDetail = styled.div`
  font-size: 1.1rem;
  margin-bottom: 1vh;
`;
const StyleBtn = styled.button`
  width: 100%;
  padding: 16px 0;
  border: 3px solid grey;
  background-color: yellow;
  border-radius: 10px;
  font-size: 20px;
  margin-bottom: 10px;
  transition: all 300ms ease-in-out;
`;

이런 식으로 마커에 해당하는 장소의 정보를 간략하게 띄워주고 카카오지도 링크로 연결해주는 버튼까지 추가하는 것으로 카카오 지도 API에 대한 설명을 마친다.

사실 실제 프로젝트에서 사용한 코드에서는 WEB API 인 Geolocation API 를 사용해서 실제 사용자가 있는 장소의 좌표를 가져와서 지도를 보여주도록 했는데,

생각보다 글이 조금 길어진 것 같아 Geolocation API 에 대해서는 다른 글에서 다뤄보도록 하겠다.

위에서 설명하면서 쓴 예제 코드들과 거의 흡사하긴 한데, 실제 프로젝트에서 카카오 지도 API를 어떻게 활용했는지 보고 싶으시면 아래 링크로 이동해서 깃허브에서 보시길 바람요
https://github.com/team1codadoc/Vegopa/blob/dev/src/components/map.tsx

profile
frontend developer

0개의 댓글