스파르타코딩클럽/과제

[React 아웃소싱 프로젝트] 지도에 커스텀 오버레이 생성하기

myinfo7091 2024. 12. 2. 23:43

지도에 커스텀 오버레이 생성하기

 

https://apis.map.kakao.com/web/sample/customOverlay2/

 

카카오맵 api 공식 문서를 참고해서 지도에 자유롭게 컨텐츠를 표시할 수 있는 커스텀 오버레이를 생성한다. 마커를 클릭할 시 식당의 상호명과 주소를 적당한 ui 안에 나타내기로 했다.

 

 

 

◆ Custom Hook으로 분리하기

 

 

그 전에 점점 길어지고 있는 코드를 정리할 필요가 있었다. supabase에서 식당의 위도와 경도 정보를 가져와서 해당 좌표에 마커를 생성하는 지도 컴포넌트인데, 기능을 추가하다 보니 뭐가 참 많다(...) supabase에서 restaurants 테이블의 정보를 가져오는 부분은 다른 팀원들도 활용할 가능성이 있어서 따로 커스텀 훅을 만들면 좋을 것 같다.

 

import { useEffect, useState } from 'react';
import { supabase } from '../supabase/supabaseClient';

const useFetchRestaurants = () => {
  const [restaurants, setRestaurants] = useState([]);

  useEffect(() => {
    // fetchRestaurants: supabase에서 데이터를 가져오는 함수
    const fetchRestaurants = async () => {
      const { data } = await supabase.from('restaurants').select('*');
      setRestaurants(data);
    };
    fetchRestaurants();
  }, []);

  return restaurants;
};

export default useFetchRestaurants;

 

 

 

◆ 커스텀 오버레이 내용 작성하기

 

 

처음에는 react 컴포넌트 만들던 습관 그대로 OverlayContent.jsx 파일을 만들어 신나게 ui를 만들고 import해서 가져오겠다는 생각이었다. 그런데 정작 렌더링이 정상적으로 이루어지지 않았는데, 콘솔 오류 메세지를 확인해보니 parameter 1 is not of type Node 란다. 

 

// 좌표에 커스텀 오버레이를 생성한다.
    const overlay = new kakao.maps.CustomOverlay({
      // 문제.
      // OverlayContent는 리액트 컴포넌트이고, content 속성은 HTML 문자열 또는 DOM 노드여야 한다.
      // 이로 인해 콘솔에 'parameter 1 is not of type Node'오류가 생긴다.

      // 해결법.
      // 1. 컴포넌트를 html로 변환해서 전달한다.
      // 2. 직접 dom 노드를 생성해서 오버레이를 만드는 코드를 작성한다.
      // 3. 커스텀 오버레이 내용을 그냥 ``로 만들어서 넣는다.

      content: OverlayContent
      map: null,
      position: markerCoordinates,
      removeable: true // 오버레이 제거 기능
    });

 

 

OverLayContent는 리액트 컴포넌트이고, content 속성은 HTML 문자열 또는 DOM 노드여야 하기 때문에 문제가 생겼다. 해결법은 단순했다.

  • 컴포넌트를 HTML로 변환해서 전달한다.
  • 직접 DOM 노드를 생성해서 커스텀 오버레이를 만드는 코드를 작성한다.
  • 커스텀 오버레이 내용을 ``로 만들어 넣는다.

방법은 다양했는데, 우선 ui 모양을 보고 싶어서 커스텀 오버레이 내용을 `` 안에 그대로 만들어 보았다. 들어갈 내용이 팀원들과의 논의 하에 변동될 수 있기 때문이다. useCallback은 강의에서 배운 내용을 잊어버리기 전에 적용해본 건데, 사실 여기서는 메모리를 낭비하는 꼴이라 굳이 필요하지 않을 것 같다.

 

디자인 별로인거 나도 안다 바꿀거다

 

 

// 전체 코드
import { useCallback, useEffect, useRef } from 'react';
import useFetchRestaurants from './useFetchRestaurants';
// import Overlaycontent from './Overlaycontent';

const MapComponent = () => {
  const mapContainer = useRef(null);
  const restaurants = useFetchRestaurants();

  // createMarker: 마커, 인포윈도우를 생성하는 함수
  const createMarker = useCallback((map, restaurant) => {
    const { kakao } = window;

    // 좌표에 마커를 생성한다.
    const markerCoordinates = new kakao.maps.LatLng(restaurant.latitude, restaurant.longitude);
    const marker = new kakao.maps.Marker({
      position: markerCoordinates
    });

    // 커스텀 오버레이 내용을 작성한다.
    const overlayContent = `
       <div style="border-radius:5px; overflow:hidden; width:250px; text-align:center; font-size:12px; border:1px solid #333; background:#fff;">
        <div style="padding:10px; font-weight:bold;">${restaurant.name}</div>
        <div style="padding:5px;">
          ${
            restaurant.img_url
              ? `<img src="${restaurant.img_url}" alt="${restaurant.name}" style="width:100%; height:auto;" />`
              : ''
          }
        </div>
        <div style="padding:5px;">주소: ${restaurant.address}</div>
        ${restaurant.tel ? `<div style="padding:5px;">전화번호: ${restaurant.tel}</div>` : ''}
      </div>
    `; // 으악 더러워

    // 좌표에 커스텀 오버레이를 생성한다.
    const overlay = new kakao.maps.CustomOverlay({
      content: overlayContent,
      map: null,
      position: markerCoordinates,
      removeable: true // 오버레이 제거 기능
    });

    // 마커를 지도에 추가한다.
    marker.setMap(map);

    // 마커에 클릭 이벤트를 등록한다.
    kakao.maps.event.addListener(marker, 'click', function () {
      overlay.setMap(map);
    });
  }, []);

  // initializeMap: 지도와 마커를 설정하는 함수(지도 초기화)
  const initializeMap = useCallback(
    (restaurants) => {
      const { kakao } = window;
      const mapOption = {
        center: new kakao.maps.LatLng(37.5665, 126.978), //지도의 중심좌표
        level: 5 // 지도의 확대 레벨 (3 ~ 5 사이 추천)
      };

      const map = new kakao.maps.Map(mapContainer.current, mapOption); // 지도
      const infowindow = new kakao.maps.InfoWindow({ zIndex: 1 }); // 인포윈도우

      // 마커를 생성하고 지도에 표시한다.
      restaurants.forEach((restaurant) => {
        createMarker(map, restaurant, infowindow);
      });
    },
    [createMarker]
  );

  // 카카오맵 api에서 마커를 표시한다.
  useEffect(() => {
    if (restaurants.length > 0) {
      initializeMap(restaurants);
    }
  }, [restaurants, initializeMap]);

  return (
    <>
      <div
        ref={mapContainer}
        style={{
          width: '1000px',
          height: '1000px'
        }}
      ></div>
    </>
  );
};

export default MapComponent;