상세 페이지에서 뒤로 가기 실행시(=리렌더링) 검색 결과에 해당되는 지도 범위 유지하기
검색을 실행하거나 상세페이지에서 뒤로 가기를 실행하면 빈 화면 반환
Uncaught TypeError: a.B is not a function
Main.jsx
const Main = () => {
const sessionKeyword = sessionStorage.getItem("SearchKeyword");
const sessionMarkers = JSON.parse(sessionStorage.getItem("SearchMarkers"));
const sessionBounds = JSON.parse(sessionStorage.getItem("SearchBounds"));
useEffect(() => {
const ps = new kakao.maps.services.Places();
ps.keywordSearch(place, (data, status, _pagination) => {
if (status === kakao.maps.services.Status.OK) {
let bounds = new kakao.maps.LatLngBounds();
let markers = [];
for (var i = 0; i < data.length; i++) {
.
.
.
});
bounds.extend(new kakao.maps.LatLng(data[i].y, data[i].x));
}
setMarkers(markers);
setPlaces(data);
map.setBounds(bounds);
displayPagination(_pagination);
sessionStorage.setItem("SearchKeyword", place);
sessionStorage.setItem("SearchPlace", JSON.stringify(data));
sessionStorage.setItem("SearchBounds", JSON.stringify(bounds));
sessionStorage.setItem("SearchMarkers", JSON.stringify(markers));
}
});
}, [place]);
useEffect(() => {
if (!sessionMarkers) return;
setMarkers(sessionMarkers);
map.setBounds(sessionBounds);
}, [sessionKeyword]);
}
두 번째
useEffect
의map.setBounds(sessionBounds)
const sessionLat = sessionMarkers ? sessionMarkers[0].position.lat : null;
const sessionLng = sessionMarkers ? sessionMarkers[0].position.lng : null;
.
.
.
return (
<>
{sessionMarkers ? (
<Map
center={{
lat: sessionLat,
lng: sessionLng,
}}
onCreate={setMap}
/>
) : (
<Map
center={{
lat: 37.566826,
lng: 126.9786567,
}}
onCreate={setMap}
/>
)}
</>
);
sessionMarkes
의 여부에 따라 sessionMarkes
의 좌표 정보 or 기본값을 반환하는 방식이다. 로직 자체는 작동하나 근본적인 해결책은 될 수 없다. 두 가지 문제가 있다.
1. 코드량이 급증한다.
본문에는 생략되었지만 실제로는 <Map />
안에 훨씬 많은 속성이 있고 또 다른 컴포넌트도 있다.
2. 좁은 범위의 검색만 가능하다.
sessionMarkes
(= 검색 결과) 중 첫 번째 결과의 좌표 정보를 반환하기 때문에 넓은 범위에서 검색시 문제가 발생한다. 예를 들면 마포구에 있는 업체의 상세페이지로 들어갔는데 뒤로 가기를 실행하면 송파구에 있는 업체의 좌표가 설정되어 있는 식이다.
mount ➡️ setMap
➡️ useEffect
의 순서로 실행되어야 하는데 setMap
이 setBounds와
보다 늦게 실행되거나 혹은 동시에 실행되는 것으로 추측하고 여기서부서 접근하기 시작했다.
import { parse, stringify } from "flatted";
const Main = () => {
const searchMap = parse(sessionStorage.getItem("SearchMap"));
.
.
.
useEffect(() => {
if (!searchMarkers) return;
setMarkers(searchMarkers);
searchMap.setBounds(deserialized);
}, [searchKeyword]);
}
setMap
이 실행되지 않더라도 map
을 활용할 수 있도록 세션 스토리지에 map
정보를 저장해서 해당 데이터를 활용하고자 했다. 그러나 이번에도 Uncaught TypeError: searchMap.setBounds is not a function을 반환했다.
트러블슈팅 속 트러블슈팅
Converting circular structure to JSON
sessionStorage.setItem("SearchMap", JSON.stringify(map)); const searchMap = JSON.parse(sessionStorage.getItem("SearchMap"));
일반적인 세션 스토리지 방법대로 로직을 구현하고
searchMap
을 콘솔로 찍어보면 아래와 같은 에러 메시지가 발생한다.console.log(map); p {o: {…}, a: div#react-kakao-maps-sdk-map-container, Gh: Array(0), b: Mc, G: wb, …}
잘 보면 객체 앞에
p
가 있는데map
이 일반적인 객체 데이터가 아니라 발생한 오류다.yarn add flatted
를 설치해서JSON.stringify
,JSON.parse
가 아닌 flatted 패키지의stringify
,parse
를 사용하면 원하는 형태의 객체 정보를 얻을 수 있다.
useEffect
바깥에서 콘솔을 찍어보자 map
정보가 잘 들어오고 있었다. 대신 콘솔이 두 번씩 찍히고 있었는데 첫 번째 결과는 null
을 반환했다.
useEffect(() => {
if (!searchMarkers) return;
if (map !== null) {
setMarkers(searchMarkers);
map.setBounds(searchBounds);
}
}, [map, searchKeyword]);
그래서 null
을 예외처리 해봤지만 결과는 Uncaught TypeError: a.B is not a function
map
, searchBounds
도 콘솔로 찍어보았는데 값이 정상적으로 들어오고 있었다. 그래서 setBounds
가 원인이라고 추측을 했다. 결국 또 다시 원점으로...
하지만 결과적으로 null
예외처리가 필요한 건 맞았다. 이것만으로는 해결이 안돼서 그렇지😇
원인은 setBounds
가 아니라 searchBounds
에 있었다.
bounds
: 검색 결과에 해당하는 지도 범위searchBounds
: bounds
값을 세션 스토리지에 저장한 데이터같은 데이터를 반환하고 있지만 마지막줄을 보면 프로토타입이 다른 것을 알 수 있다. 바로 이 프로토타입에서 setBounds
를 저장하는데 세션 스토리지는 오리지널 프로토타입까지는 저장해주지 않기 때문에 계속해서 같은 에러가 발생했던 것이다.
참고로 프로토타입은 개발자가 직접 생성할 수 있다. setBounds
역시 카카오맵에서 정의한 메서드이며 프로토타입: Y는 카카오맵에서 제공하는 라이브러리이다.
📌 더 알아보기
배열이 아닌 문자형에서length
를 사용할 수 있는 것도 문자형의 프로토타입에length
가 정의되어 있기 때문이다.
const searchKeyword = sessionStorage.getItem("SearchKeyword");
const searchMarkers = JSON.parse(sessionStorage.getItem("SearchMarkers"));
const deserialized = JSON.parse(sessionStorage.getItem("SearchBounds"));
let searchBounds;
if (deserialized) {
searchBounds = Object.setPrototypeOf(
deserialized,
kakao.maps.LatLngBounds.prototype
);
}
useEffect(() => {
if (!searchMarkers) return;
if (map !== null) {
setMarkers(searchMarkers);
map.setBounds(searchBounds);
}
}, [map, searchKeyword]);
Object.setPrototypeOf
으로 스토리지에 저장했던 bounds
정보를 원래 프로토타입 정보로 변경하면
되는구나... 마침내
Object.setPrototypeOf(object, prototype)
object
: 프로토타입을 설정할 대상 객체prototype
: 프로토타입으로 사용할 객체(null
도 가능)
이 문제 해결 하는 데에만 10시간 넘게 소요한 것 같다. 물론 결과적으로는 튜터님의 도움을 받아서 해결했지만 틀린 답조차도 여러 각도에서 접근할 수 있었던 기회였기에 마냥 의미 없는 시간은 아니었다. 그리고 다양한 실패 끝에 얻은 해답이었기에 머릿 속에 더 잘 각인되었다. 프로토타입은 정말 생각치도 못했던 부분이었는데 이 에러 덕분에 두 세 단계쯤은 성장한 것 같다.
해결까지 오랜 시간이 걸렸지만 이 문제 덕분에 그동안 너무나 당연하게 사용해왔기 때문에 단 한 번도 생각해본 적 없던 str.length
가 프로토타입 때문이라는 사실을 알았고, 기능 구현하기에 급급해서 튜토리얼을 보면서 따라친 카카오맵 코드도 마침내 이해할 수 있게 되었다.
며칠 전까지만 해도 지도 API 괜히 골랐나 싶었는데 이 문제 해결하고 나서는 선택하길 너무너무 잘했다고 생각하는 중이다. 튜터님이 지도 API 사용해보는 사람이 제일 얻어가는 게 많은 프로젝트라고 하셨었는데 그 말이 사실이었다. 내배캠 시작한 이후로 스스로에게 단 한 번도 만족스러웠던 적이 없었는데 이번 프로젝트는 완성도가 어떻든 간에 스스로에게 최선을 다 했다고 자신있게 말할 수 있을 것 같다.
리얼 개그우먼 닉값 했던 이틀이었다....