React 애플리케이션에 지도 및 마커 추가

개요

이 튜토리얼에서는 @googlemaps/react-wrapper를 사용하여 React 애플리케이션에 지도와 마커를 추가하고 지도와 마커를 애플리케이션 상태에 통합하는 방법을 설명합니다.

@googlemaps/react-wrapper 설치

@googlemaps/react-wrapper 라이브러리를 설치하고 사용하여 구성요소가 렌더링될 때 Maps JavaScript API를 동적으로 로드합니다.

npm install @googlemaps/react-wrapper

이 라이브러리는 다음 코드를 사용하여 가져오고 사용할 수 있습니다.

import { Wrapper, Status } from "@googlemaps/react-wrapper";

이 구성요소의 기본 사용법은 Maps JavaScript API에 종속되는 하위 구성요소를 래핑하는 것입니다. Wrapper 구성요소는 구성요소를 로드하거나 Maps JavaScript API를 로드하는 중에 발생하는 오류를 처리하기 위한 render 속성을 허용합니다.

const render = (status: Status) => {
  return <h1>{status}</h1>;
};

<Wrapper apiKey={"YOUR_API_KEY"} render={render}>
  <YourComponent/>
</Wrapper>

지도 구성요소 추가

지도를 렌더링하는 기본 기능 구성요소는 useRef, useState, useEffect React 후크를 사용할 가능성이 큽니다.

초기 지도 구성요소에는 다음과 같은 서명이 있습니다.

const Map: React.FC<{}> = () => {};

google.maps.Map에는 Element가 생성자 매개변수로 필요하므로 구성요소의 전체 기간 동안 지속되는 변경 가능한 객체를 유지하려면 useRef가 필요합니다. 다음 스니펫은 Map 구성요소 본문의 useEffect 후크 내에서 지도를 인스턴스화합니다.

TypeScript

const ref = React.useRef<HTMLDivElement>(null);
const [map, setMap] = React.useState<google.maps.Map>();

React.useEffect(() => {
  if (ref.current && !map) {
    setMap(new window.google.maps.Map(ref.current, {}));
  }
}, [ref, map]);

자바스크립트

const ref = React.useRef(null);
const [map, setMap] = React.useState();

React.useEffect(() => {
  if (ref.current && !map) {
    setMap(new window.google.maps.Map(ref.current, {}));
  }
}, [ref, map]);

위의 useEffect 후크는 ref가 변경된 경우에만 실행됩니다. 이제 Map 구성요소가 다음을 반환합니다.

return <div ref={ref} />

추가 속성을 사용하여 지도 구성요소 확장

기본 지도 구성요소는 지도 옵션, 이벤트 리스너, 지도가 포함된 div에 적용되는 스타일을 위한 추가 속성을 사용하여 확장할 수 있습니다. 다음 코드는 이 기능 구성요소의 확장된 인터페이스를 보여줍니다.

interface MapProps extends google.maps.MapOptions {
  style: { [key: string]: string };
  onClick?: (e: google.maps.MapMouseEvent) => void;
  onIdle?: (map: google.maps.Map) => void;
}

const Map: React.FC<MapProps> = ({
  onClick,
  onIdle,
  children,
  style,
  ...options
}) => {}

style 객체는 직접 전달하고, 렌더링된 div에서 속성으로 설정할 수 있습니다.

return <div ref={ref} style={style} />;

onClick, onIdle, google.maps.MapOptions에는 google.maps.Map에 업데이트를 반드시 적용하기 위한 useEffect 후크가 필요합니다.

TypeScript

// because React does not do deep comparisons, a custom hook is used
// see discussion in https://github.com/googlemaps/js-samples/issues/946
useDeepCompareEffectForMaps(() => {
  if (map) {
    map.setOptions(options);
  }
}, [map, options]);

자바스크립트

// because React does not do deep comparisons, a custom hook is used
// see discussion in https://github.com/googlemaps/js-samples/issues/946
useDeepCompareEffectForMaps(() => {
  if (map) {
    map.setOptions(options);
  }
}, [map, options]);

속성으로 전달된 핸들러가 업데이트된 경우 기존 리스너를 지우려면 이벤트 리스너에 약간 더 복잡한 코드가 필요합니다.

TypeScript

React.useEffect(() => {
  if (map) {
    ["click", "idle"].forEach((eventName) =>
      google.maps.event.clearListeners(map, eventName)
    );

    if (onClick) {
      map.addListener("click", onClick);
    }

    if (onIdle) {
      map.addListener("idle", () => onIdle(map));
    }
  }
}, [map, onClick, onIdle]);

자바스크립트

React.useEffect(() => {
  if (map) {
    ["click", "idle"].forEach((eventName) =>
      google.maps.event.clearListeners(map, eventName)
    );
    if (onClick) {
      map.addListener("click", onClick);
    }

    if (onIdle) {
      map.addListener("idle", () => onIdle(map));
    }
  }
}, [map, onClick, onIdle]);

마커 구성요소 빌드

마커 구성요소는 useEffectuseState 후크가 있는 지도 구성요소와 유사한 패턴을 사용합니다.

TypeScript

const Marker: React.FC<google.maps.MarkerOptions> = (options) => {
  const [marker, setMarker] = React.useState<google.maps.Marker>();

  React.useEffect(() => {
    if (!marker) {
      setMarker(new google.maps.Marker());
    }

    // remove marker from map on unmount
    return () => {
      if (marker) {
        marker.setMap(null);
      }
    };
  }, [marker]);

  React.useEffect(() => {
    if (marker) {
      marker.setOptions(options);
    }
  }, [marker, options]);

  return null;
};

자바스크립트

const Marker = (options) => {
  const [marker, setMarker] = React.useState();

  React.useEffect(() => {
    if (!marker) {
      setMarker(new google.maps.Marker());
    }

    // remove marker from map on unmount
    return () => {
      if (marker) {
        marker.setMap(null);
      }
    };
  }, [marker]);
  React.useEffect(() => {
    if (marker) {
      marker.setOptions(options);
    }
  }, [marker, options]);
  return null;
};

google.maps.Map이 DOM 조작을 관리하므로 구성요소에서 null을 반환합니다.

지도의 하위 구성요소로 마커 추가

지도에 마커를 추가하기 위해 Marker 구성요소가 다음과 같이 특수 children 속성을 사용하여 Map 구성요소에 전달됩니다.

<Wrapper apiKey={"YOUR_API_KEY"}>
  <Map center={center} zoom={zoom}>
    <Marker position={position} />
  </Map>
</Wrapper>

google.maps.Map 객체를 추가 하위 속성으로 모든 하위 요소에 전달하려면 Map 구성요소의 출력을 약간 변경해야 합니다.

TypeScript

return (
  <>
    <div ref={ref} style={style} />
    {React.Children.map(children, (child) => {
      if (React.isValidElement(child)) {
        // set the map prop on the child component
        // @ts-ignore
        return React.cloneElement(child, { map });
      }
    })}
  </>
);

자바스크립트

return (
  <>
    <div ref={ref} style={style} />
    {React.Children.map(children, (child) => {
      if (React.isValidElement(child)) {
        // set the map prop on the child component
        // @ts-ignore
        return React.cloneElement(child, { map });
      }
    })}
  </>
);

지도 및 애플리케이션 상태 연결

onClickonIdle 콜백에 위의 패턴을 사용하면 애플리케이션을 확장하여 지도 클릭 또는 화면 이동과 같은 사용자 작업을 완전히 통합할 수 있습니다.

TypeScript

const [clicks, setClicks] = React.useState<google.maps.LatLng[]>([]);
const [zoom, setZoom] = React.useState(3); // initial zoom
const [center, setCenter] = React.useState<google.maps.LatLngLiteral>({
  lat: 0,
  lng: 0,
});

const onClick = (e: google.maps.MapMouseEvent) => {
  // avoid directly mutating state
  setClicks([...clicks, e.latLng!]);
};

const onIdle = (m: google.maps.Map) => {
  console.log("onIdle");
  setZoom(m.getZoom()!);
  setCenter(m.getCenter()!.toJSON());
};

자바스크립트

const [clicks, setClicks] = React.useState([]);
const [zoom, setZoom] = React.useState(3); // initial zoom
const [center, setCenter] = React.useState({
  lat: 0,
  lng: 0,
});

const onClick = (e) => {
  // avoid directly mutating state
  setClicks([...clicks, e.latLng]);
};

const onIdle = (m) => {
  console.log("onIdle");
  setZoom(m.getZoom());
  setCenter(m.getCenter().toJSON());
};

이러한 후크는 위도 입력과 같이 다음 패턴을 사용하여 양식 요소에 통합할 수 있습니다.

<label htmlFor="lat">Latitude</label>
<input
  type="number"
  id="lat"
  name="lat"
  value={center.lat}
  onChange={(event) =>
    setCenter({ ...center, lat: Number(event.target.value) })
  }
/>

마지막으로 애플리케이션은 클릭을 추적하고 각 클릭 위치에서 마커를 렌더링할 수 있습니다.

{clicks.map((latLng, i) => (<Marker key={i} position={latLng} />))}

코드 살펴보기

아래의 온라인 코드 플레이그라운드를 통하거나 git 저장소를 클론하여 전체 샘플 코드를 살펴볼 수 있습니다.

샘플 사용해 보기

샘플 클론

이 샘플을 로컬로 실행하려면 Git 및 Node.js가 필요합니다. 다음 안내에 따라 Node.js와 NPM을 설치합니다. 다음 명령어는 샘플 애플리케이션을 클론하고 종속 항목을 설치하고 시작합니다.

  git clone -b sample-react-map https://github.com/googlemaps/js-samples.git
  cd js-samples
  npm i
  npm start

sample-SAMPLE_NAME으로 시작하는 분기로 전환하면 다른 샘플을 사용해 볼 수 있습니다.

  git checkout sample-SAMPLE_NAME
  npm i
  npm start