React) 좌충우돌 구글맵API와 검색 기능 구현기

ynnsuis·2023년 8월 29일
0

요즘 구글맵API 기반의 웹 어플리케이션 사이드 프로젝트를 진행하고 있는데, 카카오, 네이버맵 스타일로 장소 검색기능을 연동시킨 기능을 개발중이다.

무슨 프로젝트인데?

구글맵 API와 폴리곤을 활용하여, 서울의 구별로 탐험 점수를 통해 안개같던 지도를 투명하게 걷어내면서 성취감을 느껴보자 하는 서비스이다. 일명 RTS게임의 전장의 안개 시스템. 그중에 내가 개발하고있는 부분은 구글맵과 검색기능을 연동한 탐험 및 검색 페이지 이다.

전체적인 구조는 검색바가 떠있는 지도 화면, 검색중에 나타나는 검색 패널 화면, 검색 완료시 나타는 검색 완료 패널 화면 이렇게 세가지 인데, 모바일 어플리케이션을 리액트 네이티브로 개발한다면 스택 네비게이션을 활용하여 지도 스크린을 기본으로 두고 두가지 화면을 스택으로 쌓아올리는 방식으로 구현 하면 되니까, 웹 같은 경우에는 다이얼로그 형식으로 비슷하게 구현할 수 있을 것 같았다. 지도화면에서 검색패널 까지는 무난하게 구현이 가능했는데, 문제는 검색결과 화면의 레이아웃 복잡도 때문에 많은 상태들이 엮여서 엄청 복잡해지게 되었다.

너무나도 복잡한 상태들


화면은 이런느낌인데,

이 페이지를 하나로 관리할시 필요한 상태는 검색 패널의 유무 기준이되는 [isFocusing], 검색 패널에서 최근 검색어를 보여줄지, 실시간 검색어를 보여줄지 기준이되는 [isTyping], 검색 결과 패널에서 리스트를 보여줄지 지도에 찍힌 마커들을 보여줄지 기준이되는 [isMapView] 세가지의 View로직 상태에따라 수많은 분기가 생기고 검색창 디자인도 보여주는 패널마다 디자인이 달라지며, 검색 처리에 따라 상태를 바꿔주는것도 너무 복잡했다. 어떻게라도 구현은 할수 있을 것 같았고 일단 구현은 해보자하고 하나하나 차근차근 코드를 짜봤지만 계속 이게 맞나 하는 의구심이 들었고 결국 반나절의 삽질끝에 다시 처음으로 돌아왔다.

페이지를 나눠버리기

그래서 아예 세가지 페이지로 나누어서 구현해봤다. 첫번째 페이지는 지도 페이지 이고 폴리곤을 활용해 원하는 구역을 클릭하여 접근할수있는 기능을 가지고 있고, 두번째 페이지는 검색페이지로 검색을 담당하는 페이지로 타이핑 하기전엔 최근 검색어, 타이핑 중엔 실시간 검색 로직 기능을 가지고있으며, 세번째 페이지는 검색결과 페이지로 검색 결과 장소들을 리스트 및 맵 뷰로 선택해서 볼수있는 기능을 가지고 있다.

아예 페이지를 나눠서 코드를 작성하니까 지도 페이지, 검색결과 페이지에서의 검색바는 input이 아니라 검색바인척 하는 링크 컴포넌트가 되어도 됐다. 클릭시 검색 페이지로 이동만 시켜주면 되기 때문에 불필요한 상태들로부터 자유로워 졌고, 상태에 따라 복잡했던 로직들도 각자의 페이지 기능의 충실하게 구현하면 되게 바뀌었다.

(인풋처럼 보이지만 사실 링크기능이다.)

지도 페이지


지도 페이지의 주 기능은 우리 프로젝트의 핵심 기능인 서울의 구별 탐험 정도에 따른 폴리곤의 투명도 변화와 해당 구 클릭시 확대 기능 및 해당 구의 탐험 장소들을 마커로 표시해주는 기능을 가지고 있다. 이 부분은 팀원 분께서 서울의 geojson을 활용하여 만들어 주셨는데, 서울 외곽 지역은 지도를 가려 접근이 불가능하고, 서울 내부 지역은 개인 유저의 탐험정도에따라 지도가 밝아지는 시스템이다.

지도 관련 로직들은 각 관심사에 맞게 커스텀 훅으로 분리하여 관리하였다. 지도 관련 페이지를 세가지로 나눠서 이동하다 보니까, 현재 보고있는 지도의 위치 정보를 전역상태로 관리해야 했고, 지도의 센터, 줌 레벨이 변할때마다 트래킹을 하는 로직이 필요했다.

위에 스크린샷에도 나와있듯이 나는 지도의 센터 좌표가 이동할때와 줌 레벨이 바뀔때마다 전역상태를 업데이트 하는 방법을 사용했다. 처음엔 useEffect와 cleanUp 함수를 활용하여 페이지를 이탈할때만 정보를 업데이트하는 방식을 사용했지만, 중간중간에 업데이트된 지도 정보를 활용해야 했고, 매번 지도 이동시마다 업데이트하면 리렌더링이 빈번하게 일어나기 때문에 상태를 업데이트 하는 함수에 디바운싱기능을 넣어주어서 불필요한 리렌더링을 줄이는 방법을 선택했다.

검색 페이지


검색 페이지는 크게 두가지 기능으로 나누어져있다. typing 중이 아닐때 보여지는 최근 검색어 패널과 typing 중일때 실시간 검색 기능. 최근 검색어 기능은 로컬스토리지 접근과 따로 JSON 파싱없이 쉽게 사용할수있는 recoil-persist를 활용하여 구현하였고, 실시간 검색 기능은 navigator 객체의 geolocation api를 활용한 현재 위치기반으로 검색어 디바운싱을 활용하여 구현하였다.

검색 결과 페이지

대망의 검색결과 페이지다. 검색결과 페이지는 총 4가지 방법으로 접근할수 있다. 검색바 컴포넌트 밑의 카테고리 태그 버튼, 최근 검색어 클릭, 실시간 검색 클릭, 검색바에 검색어 입력후 엔터.

실시간 검색 클릭인 경우 이렇게 바로 맵뷰 상태에 마커와 장소의 간략 정보를 같이 띄워줘야 하고

반면의 다른 3가지 접근방법은

이런식으로 리스트뷰 상태로 검색결과가 먼저 뜨고 그다음에

클릭시 맵뷰로 바뀌면서 마커와 장소의 간략 정보를 같이 띄워주어야 했다.

처음 기능을 구현할때는 검색 결과를 클릭해도 계속 서울 폴리곤의 가운데인 숭례문으로 가지는 이슈가 있었다. 구글맵 API를 사용하면서 공부한 결과 구글맵의 상태를 컨트롤하는 방법은 center, zoom 값 자체를 상태로 지정해서 업데이트 하는방법과, onLoad를 통해 map 객체를 ref나, 상태 값으로 넣고 panTo(), setZoom() 같은 메서드로 업데이트 하는방법이 있었다.

     <GoogleMap
          center={center}
          zoom={zoom}
          onLoad={(map) => {
            mapRef.current = map;
            setMap(map);
          }}
	/>

숭례문의 저주

처음에는 그냥 검색 결과를 클릭하는 핸들러 함수에 바로 좌표와 줌레벨 상태를 업데이트 하는식으로 구현했는데, 검색 페이지에서 클릭하면 의도한대로 클릭한 검색 결과 장소에 이동되면서 마커가 떴지만, 검색 결과 페이지에서 클릭하면 처음엔 무조건 숭례문을 띄워주었다. 지도 컴포넌트의 초기 좌표가 숭례문은 맞지만, 분명히 줌은 확대되는데 센터 좌표만 이동하지 않았다.

계속 이방법 저방법 시도해보다가 구글맵 api는 상위의 리액트 컴포넌트가 리렌더링 되어도 별도의 렌더링 엔진을 사용하여 화면에 표시되기 때문에 리액트 컴포넌트와 분리되어 작동한다는걸 알았다. 그래서 center, zoom 상태의 초기값에 검색 결과 좌표를 할당하는건 의미가 없었고,

  const handleMoveSelectedPlace = (place: Place) => {
    navigate(`/search/result?query=${place.place_name}`);
    setIsMapView(true);
    map?.panTo({ lat: +place.y, lng: +place.x });
    map?.setZoom(18);
  };

이렇게 검색 결과 장소 클릭 핸들러 함수에 map객체의 panTo(),setZoom()메서드를 사용하여 구현해보았는데, 이번엔 또 검색 페이지에서는 숭례문으로 가지는 이슈가 있었다. 무슨짓을해도 숭례문의 지옥에서 벗어날수가 없었다.

결국 검색 결과 클릭시 구글맵 컴포넌트에 검색 결과의 좌표 정보를 넘겨서

 useEffect(() => {
    if (selectedPlace) {
      map?.panTo({ lat: +selectedPlace.y, lng: +selectedPlace.x });
      map?.setZoom(18);
    }
  }, [selectedPlace, map]);

이렇게 구현하니까 검색 페이지에서는 정상 작동하게 됐다. 이제 완벽하게 해결된줄 알고, 기존의 클릭 핸들 함수에서의 map객체 메서드를 통한 좌표 설정 로직은 지웠더니 또 검색 결과 페이지에서 숭례문으로 이동이 되었다.
결국 클릭핸들 함수에서의 좌표 설정 로직과, 구글맵 컴포넌트에서의 useEffect를 통한 좌표 설정 로직을 둘다 설정해야 두 페이지 다 정상작동이 됐다.

한번 곰곰히 생각해 보았다.

  • 검색 페이지에서 검색 결과를 클릭시 -> 검색 결과페이지로 이동한다. 구글맵 컴포넌트가 새로 마운트 되면서, 설정되어 있는 기본좌표인 숭례문으로 렌더링된다. useEffect로 인해 방금 클릭된 함수 핸들러를 통한 selectedPlace 상태가 업데이트되면서 클릭한 장소 좌표로 이동한다.

    여기는 이해가 갔다.

  • 검색 결과 페이지에서 검색 결과를 클릭시 -> 기존 페이지에서 url의 query값이 바뀌지만 구글맵은 리렌더링되지 않는다. 하지만 useEffect로 인해 방금 클릭된 함수 핸들러를 통한 selectedPlace 상태가 업데이트되면서 클릭한 장소 좌표로 이동해야 한다. 그런데 결과는 숭례문 이였다.

    이해불가

기존처럼 클릭 핸들러에

map?.panTo({ lat: +place.y, lng: +place.x });
map?.setZoom(18);

이 부분을 추가하면 원하는데로 작동은 하지만, 끝내 숭례문의 저주는 풀지 못하였다. 아직 해당 기능을 100% 구현한것이 아니기때문에, 계속 리팩토링을 하면서 원인을 찾아봐야겠다.

소감

CRUD를 벗어나 여러가지 기능을 직접 구현해보니 더 재미는 있었다. 하지만 기능을 구현할때 기능 구현 방법 자체에 대한 검색 없이 오롯이 스스로 구현하려다 보니까 많이 어려웠고, 머릿속의 이론상 되겠지 하고 구현했을때 뜻대로 되지않는 것도 있어서 혼란스러웠지만, 그래도 나름 가설을 세워보고 동작원리를 이해해 보려고 하다보니까 개발자로서 더 성장할 수 있었던것 같다.

0개의 댓글