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

    React에서 Naver 지도 resize 이벤트 오류 해결하기 (__event_relations__ 에러)

    React 환경에서 네이버 지도를 사용할 때, 특정 상태(hideStateMenu 등)가 변경됨에 따라 지도의 크기가 변경될 때 resize 이벤트를 수동으로 트리거해야 하는 경우가 있다. 이 과정에서 간헐적으로 Cannot read properties of null (reading '__event_relations__') 와 같은 오류가 발생할 수 있다. 이 글에서는 해당 오류의 원인을 분석하고 안정적으로 해결하는 방법을 제시한다.

    1. 문제 원인 분석

    이 오류는 주로 hideStateMenu와 같은 상태 값이 변경될 때, naverMap.current (네이버 지도 인스턴스 참조)에 대해 resize 이벤트를 트리거하는 비동기 로직(setTimeout) 내에서 발생한다. 지도 인스턴스나 관련 내부 속성에 접근하려는 시점에 해당 객체가 유효하지 않을 때 문제가 된다.

    주요 원인은 다음과 같다.

    • setTimeout 콜백 함수가 실행되는 시점에 naverMap.currentnull이 되는 경우이다. (예: 컴포넌트가 이미 언마운트되었거나, 지도 인스턴스가 아직 준비되지 않았거나, 다른 이유로 참조가 해제된 경우)
    • 컴포넌트가 언마운트된 후에도 예약된 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.currentnull로 바뀔 수 있다.
    • 컴포넌트가 언마운트될 때 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.currentfalse로 설정한다.
    • 타이머 참조 관리 및 정리: 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)에서 컴포넌트가 언마운트될 때 isMountedReffalse로 설정하고, 혹시 남아있을 수 있는 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
    반응형
    댓글