카카오맵에서 좌표로 주소 얻어오기

김현준·2025년 1월 18일
0

리액트 이모저모

목록 보기
26/27

리액트 카카오맵 라이브러리를 사용하면서 좌표로 주소를 얻어오려 한다.
카카오맵 자바스크립트 aip의 Geocoder 객체를 활용할 생각이다.

구현 단계

1. 리액트 카카오맵 설치

npm install react-kakao-maps-sdk

2. Kakao Maps API 키 등록

<script src="https://dapi.kakao.com/v2/maps/sdk.js?appkey=YOUR_APP_KEY&libraries=services"></script>

3. Geocoder 객체를 사용하여 좌표를 주소로 변환

예제 코드

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

const KakaoMapExample = () => {
  const [address, setAddress] = useState<string>(""); // 변환된 주소 저장
  const [position, setPosition] = useState({ lat: 37.5665, lng: 126.9780 }); // 기본 좌표 (서울시청)

  useEffect(() => {
    // Geocoder 객체 생성
    const geocoder = new window.kakao.maps.services.Geocoder();

    // 좌표로 주소를 얻어오는 함수
    geocoder.coord2Address(
      position.lng, // 경도
      position.lat, // 위도
      (result, status) => {
        if (status === window.kakao.maps.services.Status.OK) {
          const addressName = result[0]?.address?.address_name || "주소를 찾을 수 없음";
          setAddress(addressName); // 변환된 주소 저장
        }
      }
    );
  }, [position]);

  return (
    <div>
      <h1>React Kakao Maps SDK 예제</h1>
      <Map
        center={position}
        style={{ width: "100%", height: "400px" }}
        onClick={(target, mouseEvent) => {
          // 지도 클릭 시 좌표 업데이트
          const lat = mouseEvent.latLng.getLat();
          const lng = mouseEvent.latLng.getLng();
          setPosition({ lat, lng });
        }}
      >
        <MapMarker position={position}>
          <div style={{ color: "#000" }}>{address}</div>
        </MapMarker>
      </Map>
      <p>클릭한 위치의 주소: {address}</p> //서울 구로구 구로동 3-25
    </div>
  );
};

export default KakaoMapExample;

주요 포인트

Geocoder 객체

  • window.kakao.maps.services.Geocoder는 Kakao Maps API에서 제공하는 객체로, 좌표를 주소로 변환하는 데 사용한다.
  • coord2Address(longitude, latitude, callback) 메서드를 통해 좌표를 주소로 변환한다.

Callback 함수

  • coord2Address의 세 번째 인자로 콜백 함수가 들어가며, 변환 결과와 상태값을 반환한다.
  • statuswindow.kakao.maps.services.Status.OK인 경우에만 결과를 처리한다.

지도 클릭 이벤트

  • onClick 이벤트를 사용해 클릭한 위치의 좌표를 얻고, 해당 좌표로 주소를 변환한다.

좀 더 세부적인 주소 정보 가져오기(coord2RegionCode 이용)

coord2RegionCode란?

카카오맵 SDK의 coord2RegionCode API를 사용하면 위도/경도에 해당하는 행정구역 정보를 정확히 가져올 수 있다.

result 객체에서 가져올 수 있는 정보

카카오 지도 API의 coord2Address 메서드를 사용하면 위도, 경도로부터 주소 정보를 가져올 수 있다.
이때 반환되는 result[0] 객체에는 지번 주소와 도로명 주소 정보가 포함되어 있다.

현재 geocoder.coord2Address를 사용해서 위도, 경도로부터 주소를 가져오고 있는데, 카카오 API를 활용하면 좀 더 세부적인 주소 정보를 가져올 수 있다.

1. 지번 주소 (address)

const newAddress = result[0]?.address || {};
console.log('newAddress', newAddress);

result 객체를 살펴보면 다음과 같은 정보를 얻을 수 있다.

  • address_name: 전체 주소 (예: "서울특별시 구로구 새말로 97")
  • region_1depth_name: 시/도 (예: "서울")
  • region_2depth_name: 구/군 (예: "구로구")
  • region_3depth_name: 동/읍/면 (예: "구로동" 또는 값이 비어 있을 수도 있음)
  • main_address_no: 주번지 번호 (예: "3")
  • sub_address_no: 부번지 번호 (예: "25" 또는 값이 없을 수도 있음)
  • mountain_yn: 산 여부 ("Y": 산, "N": 일반 번지)
  • zip_code: 우편번호 (예: "08288" 또는 비어 있을 수도 있음)

2. 도로명 주소 (road_address, 있을 경우)

const newLoaadAddress = result[0].road_address;
console.log('newLoaadAddress', newLoaadAddress);
  • address_name: 도로명 주소 전체 (예: "서울특별시 구로구 새말로 97")
  • road_name: 도로명 (예: "새말로")
  • main_building_no: 주 건물번호 (예: "97")
  • sub_building_no: 부 건물번호 (예: "" → 없을 수도 있음)
  • building_name: 건물명 (예: "신도림테크노마트")
  • underground_yn: 지하 여부 ("Y": 지하, "N": 지상)
  • zone_no: 도로명 주소 기준 우편번호 (예: "08288")

코드 예시

import { useEffect, useState, useMemo } from "react";
import { Map, MapMarker } from "react-kakao-maps-sdk";
import useFindTheaterQuery from "../../hooks/useFindTheaterQuery";

function TheaterLocation() {
  const { isFindLoading, findTheater } = useFindTheaterQuery();

  // ✅ useMemo를 사용하여 불필요한 재계산 방지
  const center = useMemo(() => {
    return { lat: Number(findTheater?.y) || 0, lng: Number(findTheater?.x) || 0 };
  }, [findTheater]);

  // ✅ 상태 관리
  const [centerAddress, setCenterAddress] = useState<string>("");
  const [addressInfo, setAddressInfo] = useState<any>(null);
  // 지도 클릭 시 주소 저장
  const [clickedAddress, setClickedAddress] = useState<string>(""); 

  useEffect(() => {
    if (!window.kakao || !center.lat || !center.lng) return;

    const geocoder = new window.kakao.maps.services.Geocoder();

    // ✅ 지도 중심 주소 가져오기
    const fetchAddress = () => {
      geocoder.coord2Address(center.lng, center.lat, (result, status) => {
        if (status === window.kakao.maps.services.Status.OK) {
          const newAddress = result[0]?.address || {};
          setAddressInfo((prev) => (JSON.stringify(prev) !== JSON.stringify(newAddress) ? newAddress : prev));
        }
      });

      geocoder.coord2RegionCode(center.lng, center.lat, (result, status) => {
        if (status === window.kakao.maps.services.Status.OK) {
          const newCenterAddress = result.find((r) => r.region_type === "H")?.address_name || "";
          setCenterAddress((prev) => (prev !== newCenterAddress ? newCenterAddress : prev));
        }
      });
    };

    fetchAddress(); // 초기 실행
  }, [center]);

  // ✅ 지도를 클릭했을 때 해당 좌표의 주소 가져오기
  const handleMapClick = (_: any, mouseEvent: kakao.maps.event.MouseEvent) => {
    const geocoder = new window.kakao.maps.services.Geocoder();
    const lat = mouseEvent.latLng.getLat();
    const lng = mouseEvent.latLng.getLng();

    geocoder.coord2Address(lng, lat, (result, status) => {
      if (status === window.kakao.maps.services.Status.OK) {
        const clickedAddr = result[0]?.address?.address_name || "주소를 찾을 수 없음";
        setClickedAddress(clickedAddr);
        console.log("클릭한 위치의 주소:", clickedAddr);
      }
    });
  };

  return (
    <main className="flex flex-col items-center justify-center w-full p-4">
      {!isFindLoading && (
        <>
          {/* 지도 정보 표시 */}
          <section className="w-3/4">
            <h2 className="text-xl font-semibold text-white bg-gray-700 p-2 rounded-md">
              지도 중심 주소: {centerAddress || "주소를 가져오는 중..."}
            </h2>
            <h2 className="text-xl font-semibold text-white bg-gray-700 p-2 rounded-md mt-2">
              지번 주소: {addressInfo?.address_name || "없음"}
            </h2>
            <h2 className="text-xl font-semibold text-white bg-gray-700 p-2 rounded-md mt-2">
              클릭한 위치 주소: {clickedAddress || "지도에서 위치를 클릭하세요."}
            </h2>
          </section>

          {/* 지도 */}
          <section className="w-3/4 mt-4">
            <Map
              id="map"
              center={center}
              style={{ width: "100%", height: "500px" }}
              level={3}
              onClick={handleMapClick}
            >
              <MapMarker position={center} />
            </Map>
          </section>

          {/* 외부 지도 버튼 */}
          <section className="flex gap-4 mt-4">
            <button
              className="bg-blue-500 hover:bg-blue-600 text-white font-semibold py-2 px-4 rounded-lg"
              onClick={() =>
                window.open(
                  `https://map.kakao.com/link/map/${findTheater?.name},${center.lat},${center.lng}`,
                  "_blank"
                )
              }
            >
              카카오맵에서 보기
            </button>
            <button
              className="bg-gray-600 hover:bg-gray-700 text-white font-semibold py-2 px-4 rounded-lg"
              onClick={() =>
                window.open(`https://www.google.com/maps/search/?api=1&query=${center.lat},${center.lng}`, "_blank")
              }
            >
              구글맵에서 보기
            </button>
            <button
              className="bg-green-500 hover:bg-green-600 text-white font-semibold py-2 px-4 rounded-lg"
              onClick={() =>
                window.open(`https://map.naver.com/v5/search/${center.lat},${center.lng}`, "_blank")
              }
            >
              네이버맵에서 보기
            </button>
          </section>
        </>
      )}
    </main>
  );
}

export default TheaterLocation;

무한루프 발생 코드

문제되는 부분

useEffect(() => {
  if (!window.kakao) return;

  const geocoder = new window.kakao.maps.services.Geocoder();

  geocoder.coord2Address(center.lng, center.lat, (result, status) => {
    if (status === window.kakao.maps.services.Status.OK) {
      const address = result[0]?.address || {};
      setAddressInfo(address);  // 상태 업데이트 발생
    }
  });

  geocoder.coord2RegionCode(center.lng, center.lat, (result, status) => {
    if (status === window.kakao.maps.services.Status.OK) {
      for (let i = 0; i < result.length; i++) {
        if (result[i].region_type === 'H') {
          setCenterAddress(result[i].address_name);  // 상태 업데이트 발생
          break;
        }
      }
    }
  });
}, [center]); // `center` 값이 변경될 때 실행됨

무한 루프 발생 이유
1. useEffect가 실행되면서 setAddressInfo 또는 setCenterAddress가 호출됨.
setState가 호출되면 컴포넌트가 리렌더링됨.
2. 리렌더링 후 center 값이 다시 계산됨 (이 과정에서 useFindTheaterQuery()의 findTheater가 변경될 수도 있음).
3. center 값이 변경되었다고 판단하여 useEffect가 다시 실행됨.
위 과정이 무한 반복되면서 콘솔 로그가 끝없이 출력됨.

해결: useState와 useMemo를 활용한 최적화
useState를 업데이트할 때, 이전 상태와 비교하고, 값이 다를 때만 setState를 실행

1. useMemo를 활용한 center 값 최적화

  • findTheater 값이 바뀔 때만 center가 변경되도록 하여 불필요한 리렌더링 방지
const center = useMemo(() => {
  return { lat: Number(findTheater?.y) || 0, lng: Number(findTheater?.x) || 0 };
}, [findTheater]);

2. 이전 값과 비교하여 setState를 실행하도록 최적화

  • 기존 상태(prev)와 새로운 값(newAddress, newCenterAddress)을 비교하여 다를 때만 setState 실행.
  • 불필요한 렌더링 방지 → 무한 루프 해결
setAddressInfo((prev) => (JSON.stringify(prev) !== JSON.stringify(newAddress) ? newAddress : prev));
setCenterAddress((prev) => (prev !== newCenterAddress ? newCenterAddress : prev));

3. fetchAddress 함수로 중복 코드 제거

  • geocoder.coord2Address와 geocoder.coord2RegionCode를 하나의 함수로 묶어 가독성 향상
const fetchAddress = () => {
  geocoder.coord2Address(center.lng, center.lat, (result, status) => {
    if (status === window.kakao.maps.services.Status.OK) {
      const newAddress = result[0]?.address || {};
      setAddressInfo((prev) => (JSON.stringify(prev) !== JSON.stringify(newAddress) ? newAddress : prev));
    }
  });

  geocoder.coord2RegionCode(center.lng, center.lat, (result, status) => {
    if (status === window.kakao.maps.services.Status.OK) {
      const newCenterAddress = result.find((r) => r.region_type === "H")?.address_name || "";
      setCenterAddress((prev) => (prev !== newCenterAddress ? newCenterAddress : prev));
    }
  });
};
profile
기록하자

0개의 댓글

관련 채용 정보