티스토리 뷰

좌표를 주소로 변환

geolocation은 위도와 경도를 가져올 수 있지만 우리가 이해할 수 있는 주소로 표기할 수 없다

그래서 좌표를 주소로 변환하는 함수를 만들었다

 

getCurrentAddress 함수에 geolocation으로 현재 위도와 경도를 가져오고 getAddressHandle(주소 변환 함수)에 인자로 넘겨주었다

 

좌표를 주소로 변환하기 위해 Kakao Maps API의 Geocoder 객체를 생성해 준다

Geocoder의 coord2Address 메서드를 호출하고 인자로 위도, 경도, 콜백함수를 넘겨준다

콜백함수를 통해 요청이 성공하면 데이터를 변수에 할당해 준다 (result를 꼭 콘솔로 찍어서 구조를 확인해 볼 것)

그리고 지도에 저장해 준다

 

* 주소 변환 변수를 지도에만 저장하는 이유

지도는 현재 화면의 중심좌표와 해당 좌표에 대한 정보를 담는 역할을 한다

마커는 사용자가 원하는 위치를 표시하는 역할을 한다

그래서 좌표 정보를 담고 있는 지도에만 저장해 준다

  // 현재 위치 가져오기
  const getCurrentAddress = () => {
    if (navigator.geolocation) {
      navigator.geolocation.getCurrentPosition(
        (position) => {
          const newCenter = {
            lat: position.coords.latitude,
            lng: position.coords.longitude,
          };

          setMapState((prev) => ({
            ...prev,
            center: newCenter,
          }));

          setMarkerState(newCenter);
          getAddressHandle(newCenter.lat, newCenter.lng);
        },
        (err) => {
          setMapState((prev) => ({ ...prev, errMsg: err.message }));
        }
      );
    } else {
      setMapState((prev) => ({
        ...prev,
        errMsg: "현재 위치를 찾을 수 없습니다.",
      }));
    }
  };
// 좌표를 주소로 변환
 const getAddressHandle = (lat: number, lng: number) => {
    const geocoder = new window.kakao.maps.services.Geocoder();
    geocoder.coord2Address(lng, lat, (result, status) => {
      if (status === kakao.maps.services.Status.OK) {
        const address = result[0].address.address_name;
        setInitialState((prev) => ({
          ...prev,
          address,
        }));
      } else {
        setInitialState((prev) => ({
          ...prev,
          address: "주소를 찾을 수 없습니다.",
        }));
      }
    });
  };

 

주소로 장소 표시하기

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

 


마커 위치 저장

마커를 이동시킨 후 위치 저장 버튼을 누르면 해당 위치가 저장되고, 새로고침 시 저장된 위치에 마커가 있어야 한다

서버가 있다면 서버에 상태를 저장하겠지만 없으면?

브라우저 로컬스토리지에 저장하면 된다

나중에 다른 컴포넌트에서도 사용할 경우를 고려해 쥬스탄드를 이용해 저장하기로 했다

 

좌표와 저장 상태를 확인하는 논리형 값을 저장하기로 했다 (오류나 주소도 저장하면 좋은데 이건 나중에 하자)

 

저장할 데이터의 타입을 지정하고

로컬스토리지에 저장하기 위해 쥬스탄드 미들웨어인 persist를 사용한다

각 초기값과 상태변경 함수를 넣어주고 로컬스토리지에 저장될 이름을 지정해 주면 쥬스탄드세팅 끝

import { create } from "zustand";
import { persist } from "zustand/middleware";

type MapType = {
  saveState: {
    center: {
      lat: number;
      lng: number;
    };
  };
  isSaved: boolean;
  setSaveState: (center: { lat: number; lng: number }) => void;
  setIsSaved: (isSaved: boolean) => void;
};

export const useMapStore = create(
  persist<MapType>(
    (set) => ({
      saveState: {
        center: {
          lat: 33.450701,
          lng: 126.570667,
        },
      },
      isSaved: false,

      setSaveState: (center: { lat: number; lng: number }) => {
        set({
          saveState: {
            center: { lat: center.lat, lng: center.lng },
          },
        });
      },
      setIsSaved: (isSaved: boolean) => {
        set({
          isSaved,
        });
      },
    }),
    {
      name: "saveState",
    }
  )
);

 

쥬스탄드에 위치 저장하기

위치 저장 버튼과 현 위치로 돌아가는 버튼을 만들어준다

위치 버튼을 클릭하면 (handleSaveLocation) 위치 저장 상태에 마커를 저장해 주고 저장상태를 true로 변경, 주소 변환 함수에 마커에 담겨있는 위도, 경도를 보내준다

 

* 주소 변경 함수에 마커 위, 경도를 보내야 새로 고침 시 저장된 위치의 주소가 변환된다

값을 보내지 않으면 현 위치 주소가 나온다

  const { saveState, isSaved, setSaveState, setIsSaved } = useMapStore();
  
  // 컴포넌트 마운트 시 초기화
  useEffect(() => {
    if (isSaved && saveState.center) {
      setMarkerState(saveState.center);
      setMapState((prev) => ({
        ...prev,
        center: saveState.center,
      }));
      getAddressHandle(saveState.center.lat, saveState.center.lng);
    } else {
      getCurrentAddress();
    }
  }, []);
  
    const handleSaveLocation = () => {
    setSaveState(markerState);
    setIsSaved(true);
    getAddressHandle(markerState.lat, markerState.lng);
  };

  const currentLocation = () => {
    setIsSaved(false);
    getCurrentAddress();
  };

  return (
    <div>
      <button onClick={handleSaveLocation}>위치 저장</button>
      <button onClick={currentLocation}>현재 위치</button>
      <Map
        center={isSaved ? saveState.center : mapState.center}
        style={{
          width: "100%",
          height: "100vh",
        }}
        level={3}
        onCenterChanged={(map) => centerChangeHandler(map)}
      >
        <MapMarker position={markerState}>
          <div
            style={{
              display: "inline",
              padding: "10px",
              color: "#000",
            }}
          >
            {mapState.errMsg ? mapState.errMsg : mapState.address}
          </div>
        </MapMarker>
      </Map>
    </div>
  );

디바운싱을 이용한 최적화

사용자가 지도를 이동할 때마다 계속 함수가 호출되면 성능저하의 원인이 될 수 있기 때문에

디바운싱을 통해 0.3초 동안 움직임이 없으면 주소 반환 함수를 호출도록했다

렌더링이 일어날 때마다 함수가 재생성되기 때문에 이걸 방지하기 위해 메모이제이션을 사용하기로 했다

 

* useCallback이 아닌 useMemo를 사용한 이유

함수 캐싱이라고 생각해 useCallback을 사용하려 했는데 계속 타입오류가 발생했다

이유는 getAddressHandle 함수가 debounce로 래핑 된 함수이기 때문 debounce는 실행 결과로 새로운 디바운싱된 함수 객체를 반환한다(주소 반환 함수)

useCallback은 함수자체를 메모이제션해야하는데 debounce함수의 반환값은 이미 실행된 결과(새로운 함수 객체)니까 타입이 안 맞았던 것

useMemo는 값을 메모이제이션하기때문에 디바운스 함수의 실행 결과를 값으로 보니까 타입 오류를 해결할 수 있었다

결론은 디바운스 함수의 실행 결과인 주소반환함수 자체를 메모이제이션하기위해 useMemo를 사용했다

함수 실행 타이밍의 차이
* useMemo
useMemo(() => debounce(fn, 300), []);
() => debounce(fn, 300) 함수실행 => 그 결과로 나온 디바운스 된 함수를 메모이제이션

* useCallback
useCallback(debounce(fn, 300), []);
debounce(fn, 300)가 즉시 실행되고 => 그 결과를 메모이제이션 하려고 시도
useCallback vs. useMemo 차이
useMemo useCallback
특정 값(계산된 결과)을 메모이제이션 콜백 함수 자체를 메모이제이션
반환값은 함수일 수도 있고, 다른 값일 수도 있음. 반환값은 함수
계산 비용이 높은 값을 의존성 기반으로 캐싱 리렌더링 시 함수가 새로 생성되지 않도록 방지
// 디바운스
export const debounce = <T extends (...args: any[]) => any>(
  fn: T,
  delay: number
) => {
  let timeout: ReturnType<typeof setTimeout>;

  return (...args: Parameters<T>) => {
    if (timeout) clearTimeout(timeout);
    timeout = setTimeout(() => {
      fn(...args);
    }, delay);
  };
};
  // 좌표를 주소로 변환
  const getAddressHandle = useMemo(
    () =>
      debounce((lat: number, lng: number) => {
        console.log("주소 변환 요청:", lat, lng);
        const geocoder = new window.kakao.maps.services.Geocoder();
        geocoder.coord2Address(lng, lat, (result, status) => {
          if (status === kakao.maps.services.Status.OK) {
            const address = result[0].address.address_name;
            setMapState((prev) => ({
              ...prev,
              address,
            }));
          } else {
            setMapState((prev) => ({
              ...prev,
              address: "주소를 찾을 수 없습니다.",
            }));
          }
        });
      }, 300),
    [isSaved]
  );
// 지도 위치에 따른 위경도 추출
const centerChangeHandler = (map: any) => {
    const newCenter = map.getCenter();
    const newPosition = {
      lat: newCenter.getLat(),
      lng: newCenter.getLng(),
    };

    // 마커 위치 즉시 업데이트
    setMarkerState(newPosition);
    setMapState((prev) => ({
      ...prev,
      center: newPosition,
    }));

    // 주소 검색은 디바운스 처리
    getAddressHandle(newPosition.lat, newPosition.lng);
  };
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
TAG
more
«   2025/02   »
1
2 3 4 5 6 7 8
9 10 11 12 13 14 15
16 17 18 19 20 21 22
23 24 25 26 27 28
글 보관함