React 외부에서 관리되는 값의 변화를 컴포넌트에서 관측할 수는 없을까?
현재 개발 중인 사이드 프로젝트에서는 TMap SDK 에서 제공되는 여러 정보와 지도와 관련된 기능을 Class 로 정의하여 사용하고 있다. (TMapModule)
그리고 실제 애플리케이션에는 해당 클래스 인스턴스를 최상단에서 생성한 후, 이를 Context API 를 통해 하위 컴포넌트들에게 주입하는 방식을 채택했다.
하지만 내부 로직에 의해 클래스 인스턴스의 속성 중 일부가 변경되었을 경우에는 이를 컴포넌트에 알릴 수단이 한정적이다. 게다가 Context 에서는 모듈 인스턴스를 ref 객체로 보관된 상태이기 때문에 사실상 없다고 봐도 무방하다.
따라서 현재 구조는 컴포넌트의 변화를 TmapModule 에 알리는 단방향 흐름이기 때문에 이를 역행하기가 대단히 어렵다는 결론에 이르렀다. 그렇다고 useEffect
를 사용하자니 이것도 별로 좋은 생각은 아니었다.
따라서 궁여지책 끝에 모듈 내 변경 사항을 컴포넌트가 인지할 수 있도록 내부적으로 CustomEvent 를 사용하기로 결정했다.
CustomEvent
는 Event
인터페이스를 상속하여 이벤트를 생성하고 발행하는 구조는 기존과 동일하다.dispatchEvent
함수를 통해 새롭게 정의한 이벤트를 발행한다.window.dispatchEvent(
new CustomEvent('markers:create', { detail: createdMarker }),
);
window.dispatchEvent(
new CustomEvent('markers:remove', { detail: removedMarker }),
);
detail
속성에서 전달된 데이터를 받아 내부 로직을 처리한다.useEffect(() => {
window.addEventListener('marker:create', (event) => setMarker([...event.detail]));
return () => {
window.removeEventListener('marker:create', (event) => setMarker([...event.detail]));
};
}, []);
useEventListener
커스텀 훅을 제작했다.DocumentEventMap
, WindowEventMap
, HTMLElementEventMap
에 있는 이벤트를 모두 포괄하도록 해야 한다.window
전역 객체에 이벤트 리스너를 부착하도록 설계했기에 타입 정의를 간소화했다.import { useEffect } from 'react';
export const useEventListeners = <T extends keyof WindowEventMap>(
eventName: T,
handler: (event: WindowEventMap[T]) => void,
options?: boolean | AddEventListenerOptions,
): void => {
useEffect(() => {
window.addEventListener(eventName, handler, options);
return () => {
window.removeEventListener(eventName, handler, options);
};
}, [eventName, handler, options]);
};
import { useState } from 'react';
import { useEventListeners } from '@/hooks/useEventListeners';
import { useTmap } from '@/hooks/useTmap';
import type { MarkersType } from '@/types/map';
const CourseView = () => {
const [markers, setMarkers] = useState<MarkersType[]>([]);
const { tmapModuleRef } = useTmap();
// 새로운 마커가 모듈 내부에서 추가되었을 때 이를 관측하여 컴포넌트 상태를 변경
useEventListeners('marker:create', (event) => {
setMarkers([...markers, event.detail]);
});
// 이하 중략...
};
export default CourseView;
declare global {
interface CustomEventMap {
'marker:create': CustomEvent<MarkerType>;
'marker:remove': CustomEvent<MarkerType>;
}
// 새롭게 정의한 이벤트 맵을 WindowEventMap 이 상속받도록 한다.
interface WindowEventMap extends CustomEventMap {}
}
marker:create
이벤트를 생성한 후 발행한다.dispatchEvent
메서드를 사용했고, detail 에는 새롭게 생성된 마커 인스턴스를 주입했다. // 마커 생성
createMarker({
name,
originName,
address,
id,
lat,
lng,
iconHTML,
}: {
name: string;
originName: string;
address: string;
id: string;
lat: number;
lng: number;
iconHTML?: string;
}) {
if (this.#markers.length >= this.#maxMarkerCount) return;
const createdMarker: MarkerType = {
marker: new Tmapv3.Marker({
position: new Tmapv3.LatLng(lat, lng),
iconHTML,
map: this.#mapInstance,
}),
name,
originName,
address,
id,
lat,
lng,
};
// ... 이하 중략
this.#markers.push(createdMarker);
window.dispatchEvent(
new CustomEvent('marker:create', {
detail: createdMarker,
}),
);
}
const CourseView = () => {
const [markers, setMarkers] = useState<MarkerType[]>([]);
useEventListeners('marker:create', (event) => {
setMarkers((previous) => [...previous, event.detail]);
});
// ... 이하 생략