React에서 더 쉽게 kakao maps-sdk 사용하기 - 키워드 검색

gigi·2023년 4월 26일
1

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

  • Kakao Maps API를 React에 맞게 변경한 라이브러리

index.html에 sdk script태그 추가

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

install

npm install react-kakao-maps-sdk
or
yarn add react-kakao-maps-sdk

지도 생성하기

import import { Map } from "react-kakao-maps-sdk";

export default function KakaoMap(){
  return (
    <Map // 지도를 표시할 Container
      center={{
        // 지도의 중심좌표
        lat: 33.450701,
        lng: 126.570667,
      }}
      style={{
        // 지도의 크기
        width: "100%",
        height: "450px",
      }}
      level={3} // 지도의 확대 레벨
    />
  );
}

나의 경우 음식점들만 지도에 나타내야했는데 react-kakao-maps-sdk에서 제공하는 api와 kakao developers에서 제공하는 api와는 차이가 있어서 카테고리별 검색 기능을 찾지 못해 처음에는 키워드 검색을 기준으로 map api를 구현하였다.

import React, { useState, useEffect } from "react";
import { Map, MapMarker, CustomOverlayMap } from "react-kakao-maps-sdk";


function(){
  const { kakao } = window;
  const [info, setInfo] = useState()
  const [markers, setMarkers] = useState([])
  const [map, setMap] = useState()

  useEffect(() => {
    if (!map) return
    const ps = new kakao.maps.services.Places()

    ps.keywordSearch("이태원 맛집", (data, status, _pagination) => {
      if (status === kakao.maps.services.Status.OK) {
        // 검색된 장소 위치를 기준으로 지도 범위를 재설정하기위해
        // LatLngBounds 객체에 좌표를 추가합니다
        const bounds = new kakao.maps.LatLngBounds()
        let markers = []

        for (var i = 0; i < data.length; i++) {
          // @ts-ignore
          markers.push({
            position: {
              lat: data[i].y,
              lng: data[i].x,
            },
            content: data[i].place_name,
          })
          // @ts-ignore
          bounds.extend(new kakao.maps.LatLng(data[i].y, data[i].x))
        }
        setMarkers(markers)

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

  return (
    <Map // 로드뷰를 표시할 Container
      center={{
        lat: 37.566826,
        lng: 126.9786567,
      }}
      style={{
        width: "100%",
        height: "350px",
      }}
      level={3}
      onCreate={setMap}
    >
      {markers.map((marker) => (
        <MapMarker
          key={`marker-${marker.content}-${marker.position.lat},${marker.position.lng}`}
          position={marker.position}
          onClick={() => setInfo(marker)}
        >
          {info &&info.content === marker.content && (
            <div style={{color:"#000"}}>{marker.content}</div>
          )}
        </MapMarker>
      ))}
    </Map>
  )
}

위 검색어 입력 지도 생성하기 sample에 input을 추가해 이태원 맛집 항목에 input의 value로 변경해 주었다.

const { kakao } = window;
const [info, setInfo] = useState()
const [markers, setMarkers] = useState([])
const [map, setMap] = useState()
const [searchInputValue, setSearchInputValue] = useState("");
const [keyword, setKeyword] = useState("");

useEffect(() => {
    if (!map) return
    const ps = new kakao.maps.services.Places()

    ps.keywordSearch(`${keyword} 음식점`, (data, status, _pagination) => {
      if (status === kakao.maps.services.Status.OK) {
        // 검색된 장소 위치를 기준으로 지도 범위를 재설정하기위해
        // LatLngBounds 객체에 좌표를 추가합니다
        const bounds = new kakao.maps.LatLngBounds()
        let markers = []

        for (var i = 0; i < data.length; i++) {
          markers.push({
            position: {
              lat: data[i].y,
              lng: data[i].x,
            },
            content: data[i].place_name,
          })
          bounds.extend(new kakao.maps.LatLng(data[i].y, data[i].x))
        }
        setMarkers(markers)

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

const handleKeyPress = (e) => {
  if (e.key === "Enter") setKeyword(searchInputValue);
};


...

return (
  <...>
	<SearchInput
    	onChange={(e) => setSearchInputValue(e.target.value)}
        onKeyPress={(e) => handleKeyPress(e)}
        value={searchInputValue}
        placeholder={"주소를 입력해주세요 ex)강남역 or 서울특별시 역삼동"}
    />
    <SearchButton onClick={() => setKeyword(searchInputValue)}>
      검색
    </SearchButton>
<...>
)
  

이런 형식으로 keyword 상태가 업데이트 되면 useEffect 안의 내용이 다시 실행 되게끔하고 검색어도 ${keyword} 음식점 이라는 형식으로 음식점만을 검색하게 만들었다.

검색결과는 ps.keywordSearch의 data 파라미터로 받아 볼 수 있다.


이렇게 사용해도 되는걸까

하지만 뭔가 만족스럽지 못하다. https://react-kakao-maps-sdk.jaeseokim.dev/ 해당 사이트에 카테고리별 검색에 대한 안내가 없지만 기본적으로 kakao developers에서 서비스중인 api를 베이스로 만든 것일 텐데 기능이 생각보다 너무 적다.
그래서 모듈안에 있는 Map 컴포넌트와 keywordSearch 함수에 대한 코드를 살펴봤다. 잘 읽어보면 사이트 내 api나 sample 항목에서 미처 설명못한 기능들을 구현할 방법들이 생각날거다.

Map 컴포넌트

/// <reference types="kakao.maps.d.ts" />
/// <reference types="kakao.maps.d.ts" />
/// <reference types="kakao.maps.d.ts" />
import React from "react";
import { PolymorphicComponentPropsWithOutRef } from "../types";
export declare const KakaoMapContext: React.Context<kakao.maps.Map>;
export declare type MapProps = {
    /**
     * 중심으로 설정할 위치 입니다.
     */
    center: {
        lat: number;
        lng: number;
    } | {
        x: number;
        y: number;
    };
    /**
     * 중심을 이동시킬때 Panto를 사용할지 정합니다.
     * @default false
     */
    isPanto?: boolean;
    /**
     * 중심 좌표를 지정한 좌표 또는 영역으로 부드럽게 이동한다. 필요하면 확대 또는 축소도 수행한다.
     * 만약 이동할 거리가 지도 화면의 크기보다 클 경우 애니메이션 없이 이동한다.
     * padding 만큼 제외하고 영역을 계산하며, padding 을 지정하지 않으면 기본값으로 32가 사용된다.
     */
    padding?: number;
    /**
     * 확대 수준 (기본값: 3)
     */
    level?: number;
    /**
     * 최대 확대 수준
     */
    maxLevel?: number;
    /**
     * 최소 확대 수준
     */
    minLevel?: number;
    /**
     * 지도 종류 (기본값: 일반 지도)
     */
    mapTypeId?: kakao.maps.MapTypeId;
    /**
     * 마우스 드래그, 휠, 모바일 터치를 이용한 시점 변경(이동, 확대, 축소) 가능 여부
     */
    draggable?: boolean;
    /**
     * 마우스 휠이나 멀티터치로 지도 확대, 축소 기능을 막습니다. 상황에 따라 지도 확대, 축소 기능을 제어할 수 있습니다.
     */
    zoomable?: boolean;
    /**
     * 마우스 휠, 모바일 터치를 이용한 확대 및 축소 가능 여부
     */
    scrollwheel?: boolean;
    /**
     * 더블클릭 이벤트 및 더블클릭 확대 가능 여부
     */
    disableDoubleClick?: boolean;
    /**
     * 더블클릭 확대 가능 여부
     */
    disableDoubleClickZoom?: boolean;
    /**
     * 투영법 지정 (기본값: kakao.maps.ProjectionId.WCONG)
     */
    projectionId?: string;
    /**
     * 지도 타일 애니메이션 설정 여부 (기본값: true)
     */
    tileAnimation?: boolean;
    /**
     * 키보드의 방향키와 +, – 키로 지도 이동,확대,축소 가능 여부 (기본값: false)
     */
    keyboardShortcuts?: boolean | {
        /**
         * 지도 이동 속도
         */
        speed: number;
    };
    /**
     * map 생성 후 해당 객체를 전달하는 함수
     */
    onCreate?: (map: kakao.maps.Map) => void;
    /**
     * 중심 좌표가 변경되면 발생한다.
     */
    onCenterChanged?: (target: kakao.maps.Map) => void;
    /**
     * 확대 수준이 변경되기 직전 발생한다.
     */
    onZoomStart?: (target: kakao.maps.Map) => void;
    /**
     * 확대 수준이 변경되면 발생한다.
     */
    onZoomChanged?: (target: kakao.maps.Map) => void;
    /**
     * 지도 영역이 변경되면 발생한다.
     */
    onBoundsChanged?: (target: kakao.maps.Map) => void;
    /**
     * 지도를 클릭하면 발생한다.
     */
    onClick?: (target: kakao.maps.Map, mouseEvent: kakao.maps.event.MouseEvent) => void;
    /**
     * 지도를 더블클릭하면 발생한다.
     */
    onDoubleClick?: (target: kakao.maps.Map, mouseEvent: kakao.maps.event.MouseEvent) => void;
    /**
     * 지도를 마우스 오른쪽 버튼으로 클릭하면 발생한다.
     */
    onRightClick?: (target: kakao.maps.Map, mouseEvent: kakao.maps.event.MouseEvent) => void;
    /**
     * 지도에서 마우스 커서를 이동하면 발생한다.
     */
    onMouseMove?: (target: kakao.maps.Map, mouseEvent: kakao.maps.event.MouseEvent) => void;
    /**
     * 드래그를 시작할 때 발생한다.
     */
    onDragStart?: (target: kakao.maps.Map, mouseEvent: kakao.maps.event.MouseEvent) => void;
    /**
     * 드래그를 하는 동안 발생한다.
     */
    onDrag?: (target: kakao.maps.Map, mouseEvent: kakao.maps.event.MouseEvent) => void;
    /**
     * 드래그가 끝날 때 발생한다.
     */
    onDragEnd?: (target: kakao.maps.Map, mouseEvent: kakao.maps.event.MouseEvent) => void;
    /**
     * 중심 좌표나 확대 수준이 변경되면 발생한다.
     * 단, 애니메이션 도중에는 발생하지 않는다.
     */
    onIdle?: (target: kakao.maps.Map) => void;
    /**
     * 확대수준이 변경되거나 지도가 이동했을때 타일 이미지 로드가 모두 완료되면 발생한다.
     * 지도이동이 미세하기 일어나 타일 이미지 로드가 일어나지 않은경우 발생하지 않는다.
     */
    onTileLoaded?: (target: kakao.maps.Map) => void;
    /**
     * 지도 기본 타일(일반지도, 스카이뷰, 하이브리드)이 변경되면 발생한다.
     */
    onMaptypeidChanged?: (target: kakao.maps.Map) => void;
    children?: React.ReactNode | undefined;
};
declare type MapComponent = <T extends React.ElementType = "div">(props: PolymorphicComponentPropsWithOutRef<T, MapProps> & React.RefAttributes<kakao.maps.Map>) => React.ReactElement | null;
/**
 * 기본적인 Map 객체를 생성하는 Comeponent 입니다.
 * props로 받는 `on*` 이벤트는 해당 `kakao.maps.Map` 객체를 함께 인자로 전달 합니다.
 *
 * `ref`를 통해 `map` 객체에 직접 접근하여 사용 또는 onCreate 이벤트를 이용하여 접근이 가능합니다.
 *
 * > *주의 사항* `Map`, `RoadView` 컴포넌트에 한하여, ref 객체가 컴포넌트 마운트 시점에 바로 초기화가 안될 수 있습니다.
 * >
 * > 컴포넌트 마운트 시점에 `useEffect` 를 활용하여, 특정 로직을 수행하고 싶은 경우 `ref` 객체를 사용하는 것보다
 * > `onCreate` 이벤트와 `useState`를 함께 활용하여 제어하는 것을 추천 드립니다.
 */
declare const Map: MapComponent;
export default Map;

keywordSearch

/// <reference path="index.d.ts" />

declare namespace kakao.maps.services {
  /**
   * 장소 검색 서비스.
   *
   * @see [Places](http://apis.map.kakao.com/web/documentation/#services_Places)
   */
  export class Places {
    /**
     * 장소 검색 서비스 객체를 생성한다.
     *
     * @param map 중심 좌표를 Places 객체의 location으로 설정할 지도 객체
     */
    constructor(map?: Map);

    /**
     * 지도 객체를 설정한다. 이미 설정되어 있는 지도는 `setMap(null)` 로 해제 가능하다.
     *
     * @param map 지도 객체
     */
    public setMap(map: Map | null): void;

    /**
     * 입력한 키워드로 검색한다.
     *
     * @param keyword 검색할 키워드
     * @param callback 검색 결과를 받을 콜백함수
     * @param options
     */
    public keywordSearch(
      keyword: string,
      callback: (
        result: PlacesSearchResult,
        status: Status,
        pagination: Pagination
      ) => void,
      options?: PlacesSearchOptions
    ): void;

    /**
     * 주어진 카테고리 코드로 검색한다.
     * 카테고리 검색은 영역 검색이 기본이므로
     * 옵션에 명세된 `x`, `y` 또는 `rect` 를 직접 지정하거나,
     * `location` 또는 `bounds` 값을 넣어 주어야 한다.
     * 아니면 지정한 `Map` 객체를 이용하는 옵션인 `useMapCenter` 또는
     * `useMapBounds` 을 참으로 설정하여 지도의 영역이 자동으로
     * 관련 값에 할당되도록 해도 된다.
     *
     * @param code 검색할 카테고리 코드
     * @param callback 검색 결과를 받을 콜백함수
     * @param options
     */
    public categorySearch(
      code: CategoryCode | `${CategoryCode}`,
      callback: (
        result: PlacesSearchResult,
        status: Status,
        pagination: Pagination
      ) => void,
      options?: PlacesSearchOptions
    ): void;
  }

  export type PlacesSearchResult = PlacesSearchResultItem[];

  export interface PlacesSearchResultItem {
    /**
     * 장소 ID
     */
    id: string;

    /**
     * 장소명, 업체명
     */
    place_name: string;

    /**
     * 카테고리 이름
     * 예) 음식점 > 치킨
     */
    category_name: string;

    /**
     * 중요 카테고리만 그룹핑한 카테고리 그룹 코드
     * 예) FD6
     */
    category_group_code?: `${CategoryCode}` | `${Exclude<CategoryCode, "">}`[];

    /**
     * 중요 카테고리만 그룹핑한 카테고리 그룹명
     * 예) 음식점
     */
    category_group_name: string;

    /**
     * 전화번호
     */
    phone: string;

    /**
     * 전체 지번 주소
     */
    address_name: string;

    /**
     * 전체 도로명 주소
     */
    road_address_name: string;

    /**
     * X 좌표값 혹은 longitude
     */
    x: string;

    /**
     * Y 좌표값 혹은 latitude
     */
    y: string;

    /**
     * 장소 상세페이지 URL
     */
    place_url: string;

    /**
     * 중심좌표까지의 거리(x,y 파라미터를 준 경우에만 존재). 단위 meter
     */
    distance: string;
  }

  export interface PlacesSearchOptions {
    /**
     * 키워드 필터링을 위한 카테고리 코드
     */
    category_group_code?: `${CategoryCode}` | `${Exclude<CategoryCode, "">}`[];

    /**
     * 중심 좌표. 특정 지역을 기준으로 검색한다.
     */
    location?: LatLng;

    /**
     * x 좌표, longitude, `location` 값이 있으면 무시된다.
     */
    x?: number;

    /**
     * y 좌표, latitude, `location` 값이 있으면 무시된다.
     */
    y?: number;

    /**
     * 중심 좌표로부터의 거리(반경) 필터링 값. `location` / `x`, `y` / `useMapCenter` 중 하나와 같이 써야 의미가 있음. 미터(m) 단위. 기본값은 5000, 0~20000까지 가능
     */
    radius?: number;

    /**
     * 검색할 사각형 영역
     */
    bounds?: LatLngBounds;

    /**
     * 사각 영역. 좌x,좌y,우x,우y 형태를 가짐. `bounds` 값이 있으면 무시된다.
     */
    rect?: string;

    /**
     * 한 페이지에 보여질 목록 개수. 기본값은 15, 1~15까지 가능
     */
    size?: number;

    /**
     * 검색할 페이지. 기본값은 1, `size` 값에 따라 1~45까지 가능
     */
    page?: number;

    /**
     * 정렬 옵션. `DISTANCE` 일 경우 지정한 좌표값에 기반하여 동작함. 기본값은 `ACCURACY` (정확도 순)
     */
    sort?: SortBy;

    /**
     * 지정한 Map 객체의 중심 좌표를 사용할지의 여부. 참일 경우, `location` 속성은 무시된다. 기본값은 false
     */
    useMapCenter?: boolean;

    /**
     * 지정한 Map 객체의 영역을 사용할지의 여부. 참일 경우, `bounds` 속성은 무시된다. 기본값은 false
     */
    useMapBounds?: boolean;
  }
}

역시나 생각했던 기능들이 다 들어있었다.
굳이 ${keyword} 음식점 으로 검색할 필요없이 장소만 검색어로 전달하고 keywordSearch 함수의 options 항목으로 음식점 코드(FD6)을 전달해주면 음식점만을 검색하게 된다.


 	// ...생략
    
  const options = {
    category_group_code: "FD6", // 음식점만 검색한다
    page : 2,	// 2페이지의 검색 결과를 받는다
  };
    
  ps.keywordSearch(
    searchInputValue,
    (data, status, _pagination) => {},
    options
  );

	// ...생략
    

map drag 재검색도 kakao developes 참고해서

useEffect(() => {
    var geocoder = new kakao.maps.services.Geocoder();
    geocoder.coord2Address(position.lng, position.lat, displayCenterInfo);
  }, [position]);

  function displayCenterInfo(result, status) {
    if (status === kakao.maps.services.Status.OK) {
      let detailAddr = !!result[0].road_address
        ? result[0].road_address.address_name
        : "";
      detailAddr += result[0].address.address_name;
      setKeyword(detailAddr);
    }
  }

위와같이 중심좌표를 먼저 구한다음 행정주소로 변환해서 keyword를 업데이트하는 방식으로 복잡하게 구현했었는데 그럴필요없이 Map 컴포넌트의

<Map
onDragEnd={(map) => {
  setPosition({
    lat: map.getCenter().getLat(),
    lng: map.getCenter().getLng(),
  });
>
  ...
</Map>

onDragEnd 이벤트로 중심좌표를 업데이트하고 keywordSearch options의 location 속성을 이용해보면 쉽게 수정이 가능 할 것 같다.

결론 : 좀 더 빨리 생각했으면 두번 고생 안할텐데..

0개의 댓글