카카오 맵 API를 활용한 강력한 커스텀 훅 만들기

🤔 커스텀 훅을 사용한 이유?

사실 처음부터 커스텀 훅을 사용해 만들 생각은 없었다.

처음 프로젝트를 만들 당시 카카오 맵 API를 활용한 중복된 코드가 많았고, 이를 불편하게 생각한 나는 리팩토링 과정에서 꼭 수정해야겠다고 다짐했다. 

 

가장 불편했던 점은 지도와 인터랙션을 하기 위해서는 맵 객체를 생성해야 한다는 점이였다. 나는 프로젝트 요구사항을 만족시키기 위해 마커생성, 클릭이벤트, 클러스터러 기능 등 여러 가지를 수행해야 했다. 따라서 그럴 때마다 Map 객체를 만드는 과정을 반복해야 했다. 

 

그래서 나는 Map객체를 공용으로 사용하는 인스턴스를 가진 커스텀 훅을 만들기로 했다. 

export default function useMap(containerRef: RefObject<HTMLElement>) {

    // 클릭한 곳 마커 생성 및 주소 반환
    const displayInitMarker = async () => {
       ...
    };

    // 지도 움직일 때마다 중심 좌표 구하는 함수
    const getCenterLatLng = async () => {
       ...
    };

    // 현위치로 이동 함수
    const placeCurLocation = async () => {
       ...
    };

    // 모든 리뷰 보기 && 클릭한 마커의 리뷰 보기 && 클러스트러 기능
    const viewAllReviews = async () => {
	   ...
    };

    // 지도 영역 마커만 출력
    const viewReviewInMap = async () => {
       ...
    };

    // 검색한 주소로 이동 함수
    const placeSearchLocation = async () => {
        ...
    };

    // 지도 초기 위치
    useEffect(() => {
        (async () => {
            if (containerRef.current) {
               // 맵 객체 생성
            }
        })();
    }, [containerRef]);
    return {
        map,
        placeCurLocation,
        placeSearchLocation,
        displayInitMarker,
        getCenterLatLng,
        viewAllReviews,
        viewReviewInMap,
    };
}

 

대략 이런식으로 만들 예정이다. 구조는 다 비슷하게 만들었기 때문에 하나의 인스턴스만 예를 들어 설명을 해보겠다.

 

실습 : 현위치로 이동 하기

요구조건은 다음과 같다.

 

  1. 현위치로 이동한다.
  2. 이동한 위치의 주소를 함께 반환한다.

1. 맵 객체 생성

export default function useMap(containerRef: RefObject<HTMLElement>) {
    // 지도 초기 위치
    useEffect(() => {
        (async () => {
            if (containerRef.current) {
                // 초기 위치(현재위치)
                const initMap = new window.kakao.maps.Map(
                    containerRef.current,
                    {
                        center: new window.kakao.maps.LatLng(mylat, mylng),
                        level: `${centerLevel}`,
                    },
                );
                // 마커 이미지를 생성합니다
                let markerImage = new window.kakao.maps.MarkerImage(
                    mapMascot,
                    new window.kakao.maps.Size(64, 69),
                );
                let markerPosition = new window.kakao.maps.LatLng(mylat, mylng);
                // 마커를 생성합니다
                let initmarker = new window.kakao.maps.Marker({
                    position: markerPosition,
                    image: markerImage, // 마커이미지 설정
                    map: initMap,
                });
                setMap(initMap); // 지도 생성하기
                setMarker(initmarker); // 마커 생성하기
                const addr = await coordToAddress(mylat, mylng);
                setAddress(addr);
            }
        })();
    }, [containerRef]);
    return {map};
}

 

우선 공통으로 사용될 맵 객체를 생성해준다. 여기서 매개변수로 사용된 containerRef는 맵 객체가 들어갈 DOM요소다.

(여기서 사용되는 mylat, mylng, centerLevel 변수는 위도, 경도, 지도 크기 값을 나타낸다. 나는 두개의 컴포넌트에서 하나의 지도를 컨트롤 하다보니 컴포넌트를 이동했을 때 지도의 동기화 작업이 필요했다. 따라서 위도, 경도, 지도 크기값을 전역 상태에서 관리를 해주는 방법을 사용했다. coordToAddress는 좌표를 주소로 변환시켜주는 함수로 좀 이따 설명 할 예정이다.)

 

 

   const newMap = useRef(null);
    const {
		... // 인스턴스들
    } = useMap(newMap); // 지도 관련 훅

<_kakaoMapWrapper ref={newMap} className="newMap">

 

따라서 사용될 컴포넌트 DOM 요소에 위 코드와 같이 넣어주면 된다. 그러면 지도가 생성되는 모습을 볼 수 있다.

 

2. 현위치로 이동하는 인스턴스 만들기

const placeCurLocation = async () => {
    if (map) {
        // 현위치로 이동 함수
    }
};

 

나는 현위치 이동 코드를 인스턴스에 작성하면 길이가 너무 길어질 것 같아서 함수로 로직을 빼내서 관리했다.

 

3. 현위치 이동 함수

const curLocation = (map: any): Promise<number[]> => {
    return new Promise((resolve, reject) => {
        if (navigator.geolocation) {
            // GeoLocation을 이용해서 접속 위치를 얻어옵니다
            navigator.geolocation.getCurrentPosition(function (position) {
                let lat = position.coords.latitude, // 위도
                    lng = position.coords.longitude; // 경도

                let locPosition = new window.kakao.maps.LatLng(lat, lng);
                map.panTo(locPosition);
                resolve([lat, lng]);
            });
        } else {
            let locPosition = new window.kakao.maps.LatLng(
                33.450701,
                126.570667,
            );
            map.setCenter(locPosition);
            resolve([33.450701, 126.570667]);
        }
    });
};

export { curLocation };

 

프로미스를 사용해서 await 할 수 있도록 만들어 주었다. 왜냐하면 여기서 resolve된 좌표값을 가지고 주소로 변환해야 하기 때문이다. 

 

4. 좌표 - 주소 변경 함수

const coordToAddress = async (lat: number, lng: number): Promise<string> => {
    let geocoder = new window.kakao.maps.services.Geocoder();
    return await new Promise((resolve) => {
        geocoder.coord2Address(lng, lat, (result: any, status: any) => {
            if (status === window.kakao.maps.services.Status.OK) {
                let addr = !!result[0].road_address
                    ? result[0].road_address.address_name
                    : result[0].address.address_name;
                resolve(addr);
            }
        });
    });
};

export { coordToAddress };

 

이전 현위치 이동 함수에서 반환한 위도, 경도 값을 가지고 주소로 변환한 코드다. 여기서는 주소를 반환해 준다.

 

4. 완성된 인스턴스

    // 현위치로 이동 함수
    const placeCurLocation = async () => {
        if (map) {
            marker.setMap(null); // 남아있는 마커 지우기
            const [lat, lng] = await curLocation(map);
            const addr = await coordToAddress(lat, lng); // 좌표 - 주소 변환
            setAddress(addr);
        }
    };

 

완성된 코드다. 나는 현위치로 이동했을 때 마커를 지우고 싶어서 3번째 줄 처럼 작성했다.