신입 개발자에서 시니어 개발자가 되기까지

[메인 프로젝트] 메인프로젝트 - 위치검색 기능/검색 페이지(feat. 카카오 맵) 본문

코드스테이츠

[메인 프로젝트] 메인프로젝트 - 위치검색 기능/검색 페이지(feat. 카카오 맵)

Jin.K 2023. 2. 21. 00:56

검색 페이지 지도 기능 요구사항

  • 현재위치 렌더링하기
  • 위치정보 접근권한 비동의한 유저에게는 강남구 렌더링하기
  • 위치 검색하면 검색한 위치를 렌더링하기(주소 -> 좌표로 변환)
  • 지도 클릭시 클릭한 위치의 주소로 주소정보 업데이트(좌표 -> 법정 주소로 변환)
  • 지도 클릭 후 다시 검색하면 검색한 위치로 되돌아가기
  • 등록한 주소록 사용하여 빠른 검색 지원
    요구사항을 구현하기 위해 사용한 지도 api는 kakao map api다.

    카카오 map api를 사용한 이유

  • 무료다. 하루에 30만건의 api요청을 무료로 할 수 있다.
  • 공식문서가 한글로 되어 있어서 이해하기에 쉬웠다.(다만 react버전은 아니어서 이건 알아서 바꿔야 한다)
  • 공식적이

1. 현재 위치 렌더링하기

현재 위치는 위치정보 접근 권한 동의를 했을 때는 본인의 실시간 위치가 렌더링된다. 본인의 실시간 위치는 geoLocation api를 사용해서 좌표를 얻어온다.
geoLocation을 사용했을 때의 문제점은 web에서 내 위치 정보를 정확하게 받아오지 않는다는 문제가 있다.
다음은 geoLocation 공식문서에서 현재위치를 받아오는 api에 대한 설명이다

참고: getCurrentPosition()의 기본값에서는 최대한 빠르게 낮은 정밀도의 응답을 반환합니다. 정확하지
않더라도 빠른 정보가 필요한 상황에서 유용합니다. 예를 들어, GPS 기능을 가진 장비는 보정 과정에
수 분이 걸릴 수도 있으므로 IP 위치와 WiFi 등 정확하지 않은 출처에 기반한 위치 정보를 반환할 수 있습니다.

이러한 이유로 정밀도는 낮을 수 있다. 그래서 이것을 보완하기 위해 지도를 클릭하면 상세 주소를 설정할 수 있는 기능을 추가했다.(이건 2번에서 다룬다.)

export const getCurrentLocation = (setLocation: any, setLocationError: any) => {
    //정확도를 높이는 옵션. 그러나 web에서는 적용되지 않는다.
  const geoLocationOptions = { enableHighAccuracy: true };
  if (navigator.geolocation) {
    navigator.geolocation.getCurrentPosition(
      (position) => {
        let lat = position.coords.latitude;
        let lng = position.coords.longitude;
        const center = { lat, lng };        
        if (lat === 0 || lng === 0) return;
        //setDefaultCoordsAndAddress는 좌표값으로 법정 주소정보를 받아오는 함수다.
        setDefaultCoordsAndAddress(center, (result, status) => {
          if (status === kakao.maps.services.Status.OK) {
            let detailAddr = !!result[0].address.address_name
              ? result[0].address.address_name
              : result[0].road_address.address_name;
              //setLocation으로 주소와 좌표정보를 업데이트해서 좌표에 해당하는 위치를 지도에 보여준다.
              //setLocation가 업데이트하는 대상은 search page에서 관리하는 좌표정보 state다.
            setLocation({ ...center, address: detailAddr });
          }
        });
      },
      //위치정보 접근 권한 동의를 하지 않은 경우 setLocationError 함수가 호출된다.
      setLocationError,
      geoLocationOptions
    );
  }
};

다음은 위치정보 접근 동의를 하지 않았을 때 강남구가 렌더링 되는 모습이다.

위치정보 권한 동의를 안 했을 때 렌더링되는 화면

2. 지도 클릭시 클릭한 위치의 주소로 주소정보 업데이트

지도를 클릭했을 때 클릭한 위치의 상세 주소로 주소정보를 업데이트 하는 기능이다. 지도를 클릭하면 파란색 marker가 해당 위치에 올라가고 상세 주소가 바뀌도록 구현했다.

마우스로 지도를 클릭했을 때
상세주소가 변경된 모습

api 코드

//type 지정
interface getMapAndMarkerPropsType {
  center: {
    lat: number;
    lng: number;
    mapLevel?: number;
    address: string;
  };
  setTargetCoord: React.Dispatch<
    SetStateAction<{ lat: number; lng: number; address: string }>
  >;
}

//지도와 마커를 생성하고, 주소-좌표 변환을 해주는 함수. 지도 클릭 이벤트를 바인딩하는 코드도 들어있다.
//원래라면 하나의 함수가 하나의 역할을 해야하지만, kakao map api의 특성상 생성된 지도 위에서 
//마커를 올리고, 클릭이벤트를 바인딩해야 해서 여러 역할을 하게 되었다.
export const exchangeCoordToAddress = async (
  center: getMapAndMarkerPropsType['center'],
  setTargetCoord: React.Dispatch<
    React.SetStateAction<{
      lat: number;
      lng: number;
      address: string;
    }>
  >
) => {
  let mapContainer =
      document.getElementById('map') || document.createElement('div'), // 지도를 표시할 div
    mapOption = {
      center: new kakao.maps.LatLng(center.lat, center.lng), // 지도의 중심좌표
      level: 5, // 지도의 확대 레벨
    };

  // 지도를 생성합니다
  let map = new kakao.maps.Map(mapContainer, mapOption);

  // 주소-좌표 변환 객체를 생성합니다
  let geocoder = new kakao.maps.services.Geocoder();

  let marker = new kakao.maps.Marker({ position: map.getCenter() }); // 클릭한 위치를 표시할 마커입니다
  marker.setMap(map);
  setTargetCoord(center);
  // 지도를 클릭했을 때 클릭 위치 좌표에 대한 주소정보를 표시하도록 이벤트를 등록합니다
  kakao.maps.event.addListener(map, 'click', function (mouseEvent: any) {
  //좌표정보를 가지고 상세 주소를 가져오는 함수.
    searchDetailAddrFromCoords(
      mouseEvent.latLng,
      function (result: any, status: any) {
        if (status === kakao.maps.services.Status.OK) {
        //법정 주소가 있으면 법정주소를 가져오고 없으면 도로명주소를 가져오도록한다.(이런 경우는 거의 없다)
          let detailAddr = !!result[0].address.address_name
            ? result[0].address.address_name
            : result[0].road_address.address_name;
          // 마커를 클릭한 위치에 표시합니다
          marker.setPosition(mouseEvent.latLng);
          marker.setMap(map);
          let latlng = mouseEvent.latLng;

          //타겟의 좌표와 법정주소로 주소정보를 업데이트하고, 최종적으로 이 주소로 게시물을 검색한다.
          setTargetCoord({
            lat: latlng.getLat(),
            lng: latlng.getLng(),
            address: detailAddr,
          });
        }
      }
    );
  });

  //좌표정보로 상세 주소 정보를 요청하는 api
  function searchDetailAddrFromCoords(
    coords: any,
    displayMarkerOnClick: (result: any, status: any) => void
  ) {
    // 좌표로 법정동 상세 주소 정보를 요청합니다
    geocoder.coord2Address(
      coords.getLng(),
      coords.getLat(),
      displayMarkerOnClick
    );
  }
};

search page코드는 너무 길어서 지도기능과 관련된 코드만..!

const Search = () => {
  const router = useRouter();
  const [targetCoord, setTargetCoord] = useState<any>({
    lat: 37.517331925853,
    lng: 127.047377408384,
    address: '서울 강남구',
  });
  const [isSearch, setIsSearch] = useState(false);
  const [center, setCenter] = useState<any>({
    lat: 37.517331925853,
    lng: 127.047377408384,
    address: '서울 강남구',
  });
  const [error, setError] = useState({ code: 0, message: '' });
  const [searchAddress, setSearchAddress] = useState('');
  const [selectedAddressBook, setSelectedAddressBook] = useState<any>({
    address: '',
    latitude: '',
    locationId: -1,
    locationName: '',
    longitude: '',
    memberId: 0,
    nickName: '',
  });
  const handleSearchAddress = (e: {
    target: { value: React.SetStateAction<string> };
  }) => {
    setSearchAddress(e.target.value);
  };
  //search 페이지 컴포넌트가 렌더링 되었을 때 실행되는 함수. 현재정보를 받아오는 api함수다.    
  //getCurrentLocation에 setCenter를 전달해서 위치 정보를 업데이트 한다.
  useEffect(() => {
    getCurrentLocation(setCenter, setError);
    setIsSearch((prev) => !prev);
  }, []);
  //center의 좌표가 바뀔 때 혹은 위치 검색을 했을 때 실행되어 center의 위치정보에 따라 
  //지도와 marker를 렌더링해주고,최종적으로 사용할 위치 정보를 targetCoord에 담아놓는다.
  useEffect(() => {
    exchangeCoordToAddress(center, setTargetCoord);
  }, [center.lat, center.lng, isSearch]);
  ...중략
  return하는 부분
     <FormButton
              variant="contained"
              className="bg-[#63A8DA] text-[white] ml-[10px] h-[52px] screen-maxw672:px-[0.625rem]
              screen-maxw430:ml-0 screen-maxw430:w-[48%]"
              content="주소검색"
              onClick={() => {
                setSelectedAddressBook({});
                setIsSearch((prev) => !prev);
            //검색 버튼 눌렀을 때 searchMap api 실행
            //Input에도 onKeyDown이벤트를 바인딩해서 엔터를 입력해도 검색하도록 구현함
                searchMap(searchAddress, setCenter);
              }}
            ></FormButton>

검색 키워드를 이용해 검색한 위치를 보여주기

이번엔 검색을 하고 검색한 키워드를 기반으로 상세주소, 좌표를 받아온다. 방배동을 검색하면 서울 서초구 방배동 주소가 입력되는 것을 볼 수 있다.

방배동을 입력했을 때 모습

//setCenter를 parameter로 받아 center 정보를 업데이트시켜 exchangeCoordToAddress 함수를 실행시켜 
//지도와 마커를 렌더링한다. exchangeCoordToAddress는 어차피 setTargetCoord를 parameter로 받아 
//center정보와 동일하게 targetCoord정보를 업데이트 시킨다.
export const searchMap = (searchAddress: string, setCenter: any) => {
  const geocoder = new kakao.maps.services.Geocoder();
  let switchLocationToCoordinate = function (result: any, status: any) {
    if (status === kakao.maps.services.Status.OK) {
      const newSearch = result[0];
      setCenter({
        lat: newSearch.y,
        lng: newSearch.x,
        address: newSearch.address_name,
      });
    }
  };
  geocoder.addressSearch(`${searchAddress}`, switchLocationToCoordinate);
};

주소록으로 빠른 검색

이건 마이페이지 주소록 등록 블로깅할 때 같이 쓰겠슴다..

정리

여기까지가 지도를 이용한 위치검색 기능이다. 이 검색페이지를 통해서 게시물을 검색할 수 있고, 검색결과로 게시물 리스트를 볼 수 있다. 다음 편은 내주변 목록페이지에서 구현한 지도기능!