트러블 슈팅: 카카오맵이 중복 렌더링되는 문제 해결하기
카카오맵이 중복 렌더링되는 문제 해결하기
프로젝트 기간 동안 지도 API를 관리하면서 카카오맵이 중복 렌더링되는 문제를 겪었다. 이를 해결하기까지 꽤 여러 시도가 있었기 때문에, 이 과정을 기록해두고 향후 유사한 문제를 마주했을 때 시행착오를 줄이려고 한다.
문제 인식
지도 컴포넌트의 중복 렌더링으로 인한 잔상현상 발생
내가 담당한 여행지(목적지) 리스트 페이지는 위치 데이터를 기반으로 카카오 맵에 여행지를 마커로 표시해주는 기능을 제공했다.
그러나 페이지를 테스트 하는 과정에서, 지도를 줌 인, 줌 아웃을 했을 때 지도가 처음 렌더링 될 때 표시되는 위치가 잔상처럼 남는 문제를 발견했다.
가설 설정 및 검증
가설 1. 지도를 생성하는 useEffect 로직이 잘못 설계되었다.
가설 검증하기: 지도 생성에 필요한 option 좌표 수정 후 기존의 지도와 차이점 확인
첫번째로 지도를 생성하는 useEffect 로직이 잘못 작성되었다는 가설을 검증했다.
지도의 줌인, 줌 아웃 양상을 확인했을 때, 지도가 나타내는 장소와 상관없이 동일한 좌표가 잔상으로 남았다.
이에 따라, 코드를 살펴 불변하는 좌표가 지도 컴포넌트에 사용되는 경우를 확인했다. 그 결과, 카카오맵을 생성할 때 반드시 작성해야 하는
option에 설정한 좌표가 잔상으로 나타나는 점을 파악할 수 있었다.
option의 좌표가 잔상으로 남는다는 가정을 확실히 확인하고자,option의 좌표를 수정했다.다시 지도에서 줌 인, 줌 아웃을 했을 때, 이번에는 변경된 좌표가 잔상으로 표시되었다.
option은 지도를 생성하는useEffect가 사용하고 있었다. 따라서 해당useEffect의 로직이 잘못 설계되어 있다는 가정하에, 문제에 접근했다.
문제 해결
1. 공식문서를 토대로 코드의 문제점 분석
나는 카카오맵의 공식 문서를 토대로 코드를 작성했다.
이에 따라 우선, 공식문서의 샘플 코드와 나의 코드를 비교하여 차이점을 파악하고자 했다.
문제가 발생한 코드는 다음과 같다.
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 29 30 31 32 33 34
useEffect(() => { const container = document.getElementById("map"); const options = { center: new kakao.maps.LatLng( DEFAULT_LOCATION.LATITUDE, DEFAULT_LOCATION.LONGITUDE ), level: 3, }; const map = new kakao.maps.Map(container, options); const bounds = new kakao.maps.LatLngBounds(); markers?.forEach((marker) => { const position = new kakao.maps.LatLng( Number(marker?.mapy), Number(marker?.mapx) ); const newMarker = new kakao.maps.Marker({ title: marker.title, position, map, }); newMarker.setMap(map); bounds.extend(position); kakao.maps.event.addListener(newMarker, "click", function () { setClickedDestination(marker); }); }); map.setBounds(bounds, 36, 32, 32, 650); }, [markersLocations]);
- 그러나 프로젝트에서 사용하는 React, TypeScript에 적합하게 수정한 부분 외에는 내가 가진 지식으로 유의미한 차이를 찾을 수 없었다. 이에 따라, 검색매체를 활용하여 유사한 문제를 해결한 사례를 찾기로 하였다.
2. 유사한 트러블 슈팅 사례 찾기
우선 카카오맵 커뮤니티에 유사한 문제를 질의하거나 해결한 사례를 검색했다.
카카오맵 커뮤니티에서는 현직자분이 직접 남긴 답변을 확인할 수 있었다. 또한 카카오맵에 관한 질문들만 모여있기 때문에 커뮤니티를 이용하면 효율적으로 사례를 찾을 수 있으리라 판단했다.
- ‘잔상’, ‘겹침’, ‘options’를 키워드로 검색한 끝에, 내가 겪고 있는 문제와 동일한 사례를 찾을 수 있었다.
- 위 글을 통해 지도가 중복 생성되면서 잔상 문제가 생긴다는 사실을 알게되었다.
3. 예제를 토대로 코드 수정하기: Type 에러 발생
앞서 조사한 사례에서 잔상 문제의 원인이 지도의 중복 생성임을 파악했다. 그러나 지도가 1번만 생성되도록 하는 구체적인 방법에 대해서는 감을 잡지 못했다.
이에 따라, 문제를 해결하기 위해 지도가 1번만 생성되도록 하는 방법을 추가로 조사했다.
그 결과, 지도 API를 활용한 리액트 예제를 찾을 수 있었다.
이미 공식문서를 토대로 코드를 점검했지만, 내가 원하는 결과물과 유사하고 동일한 라이브러리(리액트)를 활용한 예제를 참고하면 미처 발견하지 못한 문제점을 파악할 수 있으리라 생각했다.
그리고 예제와 비교한 결과, 내 코드는 예제와 달리, 지도 생성 로직, 마커 생성 로직, 범위 설정 로직이 모두 동일한
useEffect에 작성되어 있었다.즉, 어느 한 로직이 실행될 때마다 나머지 로직들도 함께 실행되고 있었던 것이다.
이 깨달음을 토대로 ‘마커 생성 로직, 또는 지도 범위 설정 로직의 내부에 지도를 생성하는 코드가 포함되어 있어, 이들을 한
useEffect에 넣었을 때 지도가 중복 생성된다’는 가설을 새롭게 설정했다.그리고 다음과 같이
useEffect를 분리했다.
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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
const [renderedMap, setRenderedMap] = useState(null);
const cachingMarkers = useMemo(() => {
return markersLocations;
}, [markersLocations]);
useEffect(() => {
const container = document.getElementById("map");
const options = {
center: new kakao.maps.LatLng(
DEFAULT_LOCATION.LATITUDE,
DEFAULT_LOCATION.LONGITUDE
),
level: 3,
};
const map = new kakao.maps.Map(container, options);
setRenderedMap(() => map);
}, []);
useEffect(() => {
const bounds = new kakao.maps.LatLngBounds();
cachingMarkers?.forEach((marker) => {
const position = new kakao.maps.LatLng(
Number(marker?.mapy),
Number(marker?.mapx)
);
const newMarker = new kakao.maps.Marker({
title: marker.title,
position,
map: renderedMap,
});
newMarker.setMap(renderedMap);
bounds.extend(position);
renderedMap.setBounds(bounds, 36, 32, 32, 650); //renderedMap이 null일 가능성으로 type error
kakao.maps.event.addListener(newMarker, "click", function () {
setClickedDestination(marker);
});
});
}, [cachingMarkers, renderedMap]);
그러나 수정한 코드는 에러가 발생하여 실행이 불가능했다.
renderedMap의 타입을 정의내릴 수 없었기 때문이다.프로젝트는 TypeScript로 진행되고 있었는데, 로직을 분리하기 위해서는 반드시 map 객체(
renderedMap)을 상태관리하여 모든useEffect에서 공유되도록 만들어야 했다.map 객체는 카카오맵에서 자체적으로 만들어 관리하고 있었다.
하지만 API 이용자들이 활용할 수 있도록 공식문서 정도만 제공하고, 구체적인 type을 정의내리지 않아, 상태관리를 할 수 없었다.
이에 따라 TypeScript 환경에서 map 객체의 메소드인
setBounds에 never type 에러가 발생했다.
4. map 객체 type 정의하기
위와 같은 이유로, map 객체의 type을 아예 직접 정의하기로 했다.
map 객체의 생김새라도 파악하고자
console.log를 활용해 직접 콘솔창에 찍어보았다.그리고 다음과 같은 복잡하기 그지없는 결과를 얻었다.
프로젝트 마일스톤이나 시간 상, 이 복잡한 type을 정의하기 위해 매달리는 건 다소 무리가 있었다.
결국 type 문제에 대한 조언을 구하고자 직접 커뮤니티에 질문을 올리기로 결정했다.
5. 커뮤니티에 직접 질문을 올리기
map 객체의 type은 여전히 제공되지 않아, any로 지정해야 했다.
질문 글을 올린 배경에는 any type을 최대한 지양하고 싶은 마음도 있었지만 아쉬운대로 renderedmap의 type을 any로 정의하기로 결정했다.
type 문제 외에도 중복 렌더링 문제가 수정한 코드에서 해결되지 않는 점을 알게 된 게 뜻밖의 수확이었다.
나는
useEffect에서renderedMap을 사용하기 때문에 의존성 배열에 추가했었다.그러나 첫 번째
useEffect의setRenderedMap상태관리 함수로 인해 두 번째useEffect가 같이 실행되면서 지도가 중복 생성되는 문제가 발생하게 되는 것이다.이에 따라, 마커를 생성하는
useEffect의 의존성 배열에서renderedMap을 제거했다.
- 기능적 측면에서 수정한 부분을 제외하고, 코드의 변화를 종합하면 이렇게 정리할 수 있다.
첫번째로, renderedMap과 markers의 type을 any로 정의했으며
두번째로, 마커를 설정하는 useEffect의 의존성 배열에서 renderedMap을 제거하고, 지도 객체가 없을 때(renderedMap === null) useEffect가 실행되지 않도록 if문을 추가했다.
- 이후,
anytype을 어떻게든 바꾸고 싶어서 카카오맵 측에서 제공하지 않는 type들을 정의해주는 모듈을 설치하긴 했지만, 일단 트러블 슈팅에 성공한 직후의 코드는 다음과 같다.
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
const [renderedMap, setRenderedMap] = useState < any > null;
const [markers, setMarkers] = useState < any > null;
useEffect(() => {
if (renderedMap === null) {
return;
} //변경점1
const positions = markersLocations?.map(
(marker) =>
new kakao.maps.LatLng(Number(marker?.mapy), Number(marker?.mapx))
);
if (markers !== null) {
const removeMarkers = markers.map((marker: any) => marker.setMap(null));
setMarkers(removeMarkers);
}
const newMarkers = positions.map(
(position) =>
new kakao.maps.Marker({
position,
map: renderedMap,
})
);
setMarkers(newMarkers);
}, [markersLocations]); //변경점2
고찰
이번 트러블 슈팅은 외부 API를 처음 사용해본 점, 첫 React 프로젝트인 점, 첫 TypeScript 사용인 점이 맞물려 생각보다 오래 걸렸다.
하지만 문제의 원인을 파악하기 위해 가설 설정 및 검증하기, 콘솔 로그 찍어보기, 직접 사례 찾아보기, 모두 시도해도 해결이 되지 않으면 직접 다른 개발자분들에게 조언 구하기, 이 과정을 따라가면 처음 도전하는 일이라도 문제가 발생했을 때 해결할 수 있다는 자신감을 가질 수 있었다.
외부 API와 연관된 트러블 슈팅은 여러모로 색다른 경험이었다.
상황에 따라서는, 내 선에서 해결하기 어려운 (map 객체 타입 정의하기..) 문제는 API 제작자에게 직접 질문하는 게 빠르고 정확한 방법이라는 생각도 들었다.






