- [React.js] Naver 지도 resize 이벤트 오류 해결하기 (`__event_relations__` 에러)2025년 04월 30일 19시 25분 16초에 업로드 된 글입니다.작성자: DandyNow728x90반응형

React에서 Naver 지도 resize 이벤트 오류 해결하기 (
__event_relations__에러)React 환경에서 네이버 지도를 사용할 때, 특정 상태(
hideStateMenu등)가 변경됨에 따라 지도의 크기가 변경될 때resize이벤트를 수동으로 트리거해야 하는 경우가 있다. 이 과정에서 간헐적으로Cannot read properties of null (reading '__event_relations__')와 같은 오류가 발생할 수 있다. 이 글에서는 해당 오류의 원인을 분석하고 안정적으로 해결하는 방법을 제시한다.1. 문제 원인 분석
이 오류는 주로
hideStateMenu와 같은 상태 값이 변경될 때,naverMap.current(네이버 지도 인스턴스 참조)에 대해resize이벤트를 트리거하는 비동기 로직(setTimeout) 내에서 발생한다. 지도 인스턴스나 관련 내부 속성에 접근하려는 시점에 해당 객체가 유효하지 않을 때 문제가 된다.주요 원인은 다음과 같다.
setTimeout콜백 함수가 실행되는 시점에naverMap.current가null이 되는 경우이다. (예: 컴포넌트가 이미 언마운트되었거나, 지도 인스턴스가 아직 준비되지 않았거나, 다른 이유로 참조가 해제된 경우)- 컴포넌트가 언마운트된 후에도 예약된
setTimeout콜백이 실행되는 경우이다. - 네이버 지도 인스턴스가 완전히 초기화되지 않은 상태에서
resize이벤트를 트리거하려고 시도하는 경우이다.
2. 기존 코드의 문제점
오류가 발생할 수 있는 일반적인 코드 패턴은 다음과 같다.
useEffect(() => { if (naverMap?.current) { // 지도가 표시된 후 잠시 뒤에 resize 이벤트를 발생시켜 렌더링 오류 방지 setTimeout(() => { const { naver } = window; // 여기서 naverMap.current가 null이거나 naver 객체가 없을 수 있다. naver.maps.Event.trigger(naverMap.current, 'resize'); }, 500); } // 다른 상태 업데이트 로직 (예시) setHideAside(hideStateMenu); }, [hideStateMenu]); // hideStateMenu 변경 시 실행이 코드의 잠재적 문제점은 다음과 같다.
setTimeout콜백 함수 내부에서naverMap.current가 콜백 실행 시점에도 여전히 유효한 객체인지 다시 확인하지 않는다. 500ms 사이에 컴포넌트가 언마운트되거나naverMap.current가null로 바뀔 수 있다.- 컴포넌트가 언마운트될 때
setTimeout으로 예약된 작업이 취소되지 않아, 언마운트된 후에 콜백이 실행될 위험이 있다. window.naver객체나naver.maps.Event가 로드되지 않았거나 존재하지 않는 경우에 대한 방어 코드가 없다.
3. 개선된 안전한 코드
이러한 문제점들을 해결하기 위해 컴포넌트의 마운트 상태를 추적하고, 타이머를 명시적으로 관리하며, 모든 객체 접근 전에 유효성을 검사하는 방어적인 코드를 작성해야 한다.
import React, { useEffect, useRef, useState } from 'react'; function MyMapComponent() { const naverMap = useRef(null); // 지도 인스턴스를 담을 ref const [hideStateMenu, setHideStateMenu] = useState(false); // 메뉴 숨김 상태 예시 const [hideAside, setHideAside] = useState(false); // 실제 UI 상태 예시 // 컴포넌트 마운트 상태 추적용 ref const isMountedRef = useRef(true); // setTimeout 타이머 ID 저장용 ref const timerRef = useRef(null); // 지도 초기화 로직 (실제 구현에 맞게 작성) useEffect(() => { // naverMap.current = new naver.maps.Map(...); // ... 지도 초기화 코드 ... // 컴포넌트 언마운트 시 실행될 정리 함수 return () => { isMountedRef.current = false; // 언마운트 상태로 변경 // 진행 중인 타이머가 있으면 취소 if (timerRef.current !== null) { clearTimeout(timerRef.current); } // 필요하다면 지도 인스턴스 destroy() 호출 // if (naverMap.current) { // naverMap.current.destroy(); // } }; }, []); // 마운트 시 한 번만 실행 // hideStateMenu 상태 변경 시 안전하게 resize 이벤트 트리거 useEffect(() => { // 이전에 예약된 타이머가 있다면 취소 (상태 변경이 짧은 간격으로 여러 번 발생 시 대응) if (timerRef.current !== null) { clearTimeout(timerRef.current); timerRef.current = null; } // 지도 인스턴스가 유효할 때만 타이머 설정 if (naverMap?.current) { timerRef.current = setTimeout(() => { timerRef.current = null; // 타이머 실행 후 ID 초기화 // 콜백 실행 시점에도 컴포넌트가 마운트 상태이고, 지도 인스턴스가 유효한지 확인 if (isMountedRef.current && naverMap?.current) { try { const { naver } = window; // naver 및 관련 객체/메서드가 모두 존재하는지 확인 if (naver && naver.maps && naver.maps.Event && naver.maps.Event.trigger) { // 안전하게 resize 이벤트 트리거 naver.maps.Event.trigger(naverMap.current, 'resize'); } else { console.warn('Naver Maps API가 완전히 로드되지 않았거나 유효하지 않다.'); } } catch (error) { // 이벤트 트리거 중 예상치 못한 오류 발생 시 로깅 console.error('네이버 지도 resize 이벤트 트리거 중 오류 발생:', error); } } }, 500); // 500ms 지연 } // 관련 상태 업데이트 (예: 사이드바 숨김 상태) setHideAside(hideStateMenu); }, [hideStateMenu]); // hideStateMenu 값이 변경될 때마다 이 effect 실행 // ... 컴포넌트의 나머지 렌더링 로직 ... return ( <div> {/* 지도를 렌더링할 DOM 요소 */} <div id="map" style={{ width: '100%', height: '400px' }}></div> <button onClick={() => setHideStateMenu(!hideStateMenu)}> {hideStateMenu ? '메뉴 보이기' : '메뉴 숨기기'} </button> </div> ); }주요 개선 사항은 다음과 같다.
- 컴포넌트 마운트 상태 추적:
isMountedRef를 사용하여 컴포넌트가 언마운트된 후에는setTimeout콜백 내부 로직이 실행되지 않도록 방지한다.useEffect의 정리(cleanup) 함수에서isMountedRef.current를false로 설정한다. - 타이머 참조 관리 및 정리:
timerRef를 사용하여setTimeout의 핸들(ID)을 저장한다.hideStateMenu가 변경될 때마다 새로운 타이머를 설정하기 전에 이전 타이머가 있다면clearTimeout으로 취소한다. 또한, 컴포넌트 언마운트 시에도 진행 중인 타이머를 정리한다. - 다중 안전 확인:
setTimeout콜백 함수 내부에서 로직을 실행하기 전에isMountedRef.current를 통해 컴포넌트가 여전히 마운트 상태인지 확인한다.naverMap.current가 콜백 실행 시점에도 유효한 객체인지 다시 한번 확인한다.window.naver,naver.maps,naver.maps.Event,naver.maps.Event.trigger등 필요한 모든 객체와 메서드가 존재하는지 확인한 후 호출한다.
- 예외 처리:
try...catch블록을 사용하여naver.maps.Event.trigger호출 과정에서 발생할 수 있는 예기치 않은 오류를 잡아내고 콘솔에 기록한다. 이는 디버깅에 도움이 된다. - 정리 함수: 마운트
useEffect의 반환 함수(cleanup function)에서 컴포넌트가 언마운트될 때isMountedRef를false로 설정하고, 혹시 남아있을 수 있는setTimeout타이머를clearTimeout으로 확실하게 제거한다.
4. 대안: 커스텀 훅으로 분리
만약 여러 컴포넌트에서 유사한 네이버 지도 리사이즈 로직이 필요하다면, 이 로직을 재사용 가능한 커스텀 훅으로 분리하는 것을 고려할 수 있다.
// useNaverMapResize.js (예시) import { useEffect, useRef } from 'react'; function useNaverMapResize(mapRef, dependencies = [], delay = 500) { const isMountedRef = useRef(true); const timerRef = useRef(null); useEffect(() => { isMountedRef.current = true; return () => { isMountedRef.current = false; if (timerRef.current !== null) { clearTimeout(timerRef.current); } }; }, []); useEffect(() => { if (timerRef.current !== null) { clearTimeout(timerRef.current); } if (mapRef?.current) { timerRef.current = setTimeout(() => { timerRef.current = null; if (isMountedRef.current && mapRef?.current) { try { const { naver } = window; if (naver && naver.maps && naver.maps.Event && naver.maps.Event.trigger) { naver.maps.Event.trigger(mapRef.current, 'resize'); } } catch (error) { console.error('네이버 지도 resize 이벤트 트리거 중 오류(Hook):', error); } } }, delay); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [...dependencies, mapRef, delay]); // 의존성 배열 전달 } // 컴포넌트 내 사용 예시 // import useNaverMapResize from './useNaverMapResize'; // ... // const naverMap = useRef(null); // const [hideStateMenu, setHideStateMenu] = useState(false); // useNaverMapResize(naverMap, [hideStateMenu], 500); // 훅 호출결론
React 컴포넌트 내에서 네이버 지도와 같은 외부 라이브러리의 인스턴스를 다룰 때, 특히 비동기 작업(
setTimeout등)과 결합될 경우, 컴포넌트의 라이프사이클과 외부 인스턴스의 상태를 신중하게 관리하는 것이 중요하다.Cannot read properties of null (reading '__event_relations__')오류는 주로 이러한 동기화 문제나 유효하지 않은 객체 참조 시도에서 발생한다. 제시된 개선된 코드와 같이 마운트 상태 확인, 타이머 관리, 객체 유효성 검사, 예외 처리, 그리고 적절한 클린업 로직을 추가함으로써 이 문제를 안정적으로 해결할 수 있다.728x90반응형'언어·프레임워크 > React.js' 카테고리의 다른 글
[React.js] `useEffect`와 `useLayoutEffect`의 차이: 깜박임 현상과 중간 값 노출 (0) 2025.05.23 [React.js] 네이버 지도 API 마커 중앙 정렬과 레이어 제어 (0) 2025.05.14 [React.js] React Hooks와 컴포넌트 생명주기, 함수 컴포넌트의 진화 살펴보기 (0) 2025.04.25 [React.js] Input 성능 최적화: 디바운싱과 스로틀링으로 불필요한 리렌더링 줄이기 (0) 2025.04.24 [React.js] 리스트 렌더링: map 인덱스를 key로 사용하면 안 되는 이유와 해결책 (0) 2025.04.24 다음글이 없습니다.이전글이 없습니다.댓글