[문제 해결] - 정보창에 이벤트 핸들러 설정하기

Donggu(oo)·2025년 2월 25일
0

[Solo Project] - saveme

목록 보기
5/5
post-thumbnail

1. 문제 현상


  • 정보 창에 파노라마를 띄우는 버튼을 추가하려 했으나, 정보 창의 content는 string을 받기 때문에, 정보 창 컴포넌트에 이벤트 핸들러를 설정한 버튼을 추가해도 제대로 동작하지 않았다.
interface MarkerInfoWindowProps {
  FNAME: string;
  ANAME: string;
  jibunAddress: string;
  roadAddress: string;
}

export function MarkerInfoWindow({
  FNAME,
  ANAME,
  jibunAddress,
  roadAddress,
}: MarkerInfoWindowProps) {
  return (
    <div className='flex flex-col gap-y-1.5 whitespace-nowrap rounded-md border border-gray-200 bg-white px-5 py-4 shadow-[0_4px_16px_0_rgba(0,0,0,0.1)]'>
      <div className='flex items-center gap-x-2'>
        <p className='text-lg font-bold'>{FNAME}</p>
        <p className='text-sm font-medium text-gray-500'>{ANAME}</p>
      </div>
      {jibunAddress && (
        <div className='text-sm font-medium'>
          <span className='mr-1 rounded border border-gray-400 px-1 py-0.5 text-xs font-semibold text-gray-700'>
            지번
          </span>
          {jibunAddress}
        </div>
      )}
      {roadAddress && <div>(도로명) {roadAddress}</div>}
      <button
        onClick={() => console.log('click')} // 버튼에 클릭 이벤트 핸들러 설정
        className='mt-2 rounded bg-blue-500 px-2 py-1 text-white'
      >
        클릭
      </button>
    </div>
  );
}
// ...

toilets.forEach((toilet) => {
  const { Y_WGS84, X_WGS84, POI_ID, FNAME, ANAME } = toilet;

  const marker = new naver.maps.Marker({
    position: new naver.maps.LatLng(Y_WGS84, X_WGS84),
    icon: {
      url: POI_ID === closestToilet.POI_ID ? '/closetToilet.png' : '/aroundToilet.png',
      size: new naver.maps.Size(35, 35),
      scaledSize: new naver.maps.Size(35, 35),
    },
  });

  const infoWindow = new naver.maps.InfoWindow({
    // content에 정보 창 컴포넌트 전달
    content: renderToStaticMarkup(
      <MarkerInfoWindow FNAME={FNAME} ANAME={ANAME} jibunAddress='' roadAddress='' />,
    ),
    anchorSize: {
      width: 12,
      height: 14,
    },
    backgroundColor: 'transparent',
    borderColor: 'transparent',
  });

  naver.maps.Event.addListener(marker, 'click', () => {
    if (infoWindow.getMap()) {
      infoWindow.close();
    } else {
      naver.maps.Service.reverseGeocode(
        {
          coords: new naver.maps.LatLng(Y_WGS84, X_WGS84),
        },
        (status, response) => {
          if (status === naver.maps.Service.Status.OK) {
            const { jibunAddress, roadAddress } = response.v2.address;
            const updatedContent = renderToStaticMarkup(
              <MarkerInfoWindow
                FNAME={FNAME}
                ANAME={ANAME}
                jibunAddress={jibunAddress}
                roadAddress={roadAddress}
              />,
            );
            // 주소를 추가한 정보 창으로 content 재설정
            infoWindow.setContent(updatedContent);
          }
        },
      );

      infoWindow.open(mapRef.current, marker);
    }
  });
});

// ...
  • 버튼을 클릭해도 콘솔에 아무런 출력이 나타나지 않았다.

참고

2. 문제 원인


  • MarkerInfoWindow(정보 창) 컴포넌트를 HTML 문자열 형태로 Infowindow의 content에 전달하면 이벤트 핸들러가 작동하지 않으므로, 대신 빈 div 태그를 content로 전달하고, 사용자가 마커를 클릭할 때 해당 div 내부에 MarkerInfoWindow 컴포넌트를 렌더링하는 방법을 적용해보기로 했다. 이렇게 하면 Infowindow에 전달되는 것은 HTML 문자열 형태의 빈 div 태그뿐이며, 브라우저에서는 createRoot를 사용해 해당 DOM 노드에 대한 루트를 생성한 후, root.render를 호출하여 MarkerInfoWindow 컴포넌트를 동적으로 렌더링한다. React는 이 루트를 통해 DOM을 관리하기 때문에 이벤트 핸들러가 동작 할 것이라고 생각했다.

  • 이 방법으로 이벤트 핸들러는 정상적으로 동작했지만, 최초로 정보창을 열 때는 스타일이 제대로 적용되지 않은 상태로 렌더링되었다가, 정보창을 닫고 다시 열면 그때부터는 CSS가 정상 반영되었다.

  • 처음 열리는 시점에 InfoWindow가 아직 React 렌더링, Tailwind 스타일이 100% 완료되지 않은 DOM을 기반으로 레이아웃을 잡아버려, 즉시 스타일이 적용되지 않고 있었다.

toilets.forEach((toilet) => {
  const { Y_WGS84, X_WGS84, POI_ID, FNAME, ANAME } = toilet;

  const marker = new naver.maps.Marker({
    position: new naver.maps.LatLng(Y_WGS84, X_WGS84),
    icon: {
      url: POI_ID === closestToilet.POI_ID ? '/closetToilet.png' : '/aroundToilet.png',
      size: new naver.maps.Size(35, 35),
      scaledSize: new naver.maps.Size(35, 35),
    },
  });
  markers.push(marker);

  // 1) InfoWindow에 사용할 빈 컨테이너를 만들고
  const container = document.createElement('div');
  // 2) ReactDOM.createRoot(container)를 통해 이 컨테이너에 React 컴포넌트를 렌더링할 수 있는 Root 생성
  const root = ReactDOM.createRoot(container);

  // 3) InfoWindow 생성 시 content는 빈 div 태그로 지정 후 마커를 클릭하여
  // 정보 창 오픈 시 정보 창 컴포넌트를 렌더링 하는 방식
  const infoWindow = new naver.maps.InfoWindow({
    content: container,
    anchorSize: {
      width: 12,
      height: 14,
    },
    backgroundColor: 'transparent',
    borderColor: 'transparent',
  });

  naver.maps.Event.addListener(marker, 'click', () => {
    if (infoWindow.getMap()) {
      infoWindow.close();
    } else {
      naver.maps.Service.reverseGeocode(
        {
          coords: new naver.maps.LatLng(Y_WGS84, X_WGS84),
        },
        (status, response) => {

          if (status === naver.maps.Service.Status.OK && mapRef.current) {
            const { jibunAddress, roadAddress } = response.v2.address;

            // 5) 컴포넌트 렌더링
            root.render(
              <MarkerInfoWindow
                FNAME={FNAME}
                ANAME={ANAME}
                jibunAddress={jibunAddress}
                roadAddress={roadAddress}
              />,
            );
          }
        },
      );

      if (mapRef.current) infoWindow.open(mapRef.current, marker);
    }
  });
});

참고

3. 문제 해결


  • flushSync를 사용하면 React의 모든 업데이트와 DOM 반영이 완전히 끝날 때까지 동기적으로 처리된다. 이 덕분에, infoWindow.open()이 실행되는 시점에는 이미 React DOM 렌더링과 Tailwind 스타일 적용이 완료된 상태가 된다. 그래서 첫 번째 클릭부터 스타일이 깨지지 않고 정상적으로 InfoWindow가 보이게 된다.

  • 기존에는 reverseGeocode 안에서 주소 정보를 가져온 뒤, 그 외부에서 infoWindow.open()을 호출하고 있었다. 이 경우 React 렌더링이 아직 끝나지 않은 상태에서 InfoWindow가 먼저 표시되어, CSS가 적용되지 않은 레이아웃이 잠깐 노출될 수 있었다. 그러나 flushSync와 함께 reverseGeocode 콜백 내부에서 렌더링과 infoWindow.open()을 순차적으로 처리하도록 코드를 수정하니, React 렌더링이 완료된 다음에 InfoWindow가 열리도록 구조가 바뀌었다.

  • 즉, 기존 로직에선 마커 클릭 → 즉시 infoWindow.open() 실행이라는 흐름 때문에, 아직 React DOM 업데이트와 Tailwind 스타일이 적용되지 않은 상태로 InfoWindow가 열려 잠깐 스타일이 깨져 보이는 문제가 있었다.

  • 그러나 수정 후에는 flushSync가 먼저 React 렌더링을 모두 끝낸 뒤, 그다음에 infoWindow.open()을 호출하도록 순서를 조정했다. 이 덕분에 첫 번째 클릭 시점부터 이미 완전히 렌더링된 DOM(컨테이너)이 사용자에게 표시되므로, 스타일이 정상 적용된 InfoWindow가 곧바로 보이게 된 것이다.

toilets.forEach((toilet) => {
  const { Y_WGS84, X_WGS84, POI_ID, FNAME, ANAME } = toilet;

  const marker = new naver.maps.Marker({
    position: new naver.maps.LatLng(Y_WGS84, X_WGS84),
    icon: {
      url: POI_ID === closestToilet.POI_ID ? '/closetToilet.png' : '/aroundToilet.png',
      size: new naver.maps.Size(35, 35),
      scaledSize: new naver.maps.Size(35, 35),
    },
  });
  markers.push(marker);

  const container = document.createElement('div');
  const root = ReactDOM.createRoot(container);

  const infoWindow = new naver.maps.InfoWindow({
    content: container,
    anchorSize: {
      width: 12,
      height: 14,
    },
    backgroundColor: 'transparent',
    borderColor: 'transparent',
  });

  naver.maps.Event.addListener(marker, 'click', () => {
    if (infoWindow.getMap()) {
      infoWindow.close();
    } else {
      naver.maps.Service.reverseGeocode(
        {
          coords: new naver.maps.LatLng(Y_WGS84, X_WGS84),
        },
        (status, response) => {
          if (status === naver.maps.Service.Status.OK && mapRef.current) {
            const { jibunAddress, roadAddress } = response.v2.address;

            flushSync(() => {
              root.render(
                <MarkerInfoWindow
                  FNAME={FNAME}
                  ANAME={ANAME}
                  jibunAddress={jibunAddress}
                  roadAddress={roadAddress}
                />,
              );
            });
            infoWindow.open(mapRef.current, marker);
          }
        },
      );
    }
  });
});
  • 정보 창이 문제 없이 렌더링되고 있고 클릭 이벤트도 동작하는 것을 확인할 수 있다.

참고

profile
FE Developer

0개의 댓글