React 이벤트 시스템 살펴보기
5 min read
![test](/posts/react/synthetic-event-deep-dive/images/thumbnail.png)
이벤트 처리의 핵심 파이프라인을 따라가보겠습니다. 리액트는 루트 컨테이너를 생성하면서 이벤트 리스너를 등록합니다.
export function createRoot(container, options) {
// rootContainer를 설정
const rootContainerElement = container.nodeType === COMMENT_NODE
? container.parentNode
: container;
// rootContainer에 지원하는 이벤트에 대한 리스너 등록
listenToAllSupportedEvents(rootContainerElement);
listenToAllSupportedEvents
는 Native Event Set을 순회하면서 각각의 이벤트 마다
개별적으로 리스너를 등록합니다. 이때 이벤트 위임 여부에 따라 적절한 플래그를 설정하고 이벤트를 등록합니다.
export function listenToAllSupportedEvents(rootContainerElement) {
...
allNativeEvents.forEach(domEventName => {
if (!nonDelegatedEvents.has(domEventName)) {
listenToNativeEvent(domEventName, false, rootContainerElement);
}
listenToNativeEvent(domEventName, true, rootContainerElement);
})
}
listenToNativeEvent
함수 내에서 flag에 대한 별도의 처리를 한 뒤 addTrappedEventListener
를 호출합니다. createEventListenerWrapperWithPriority
함수를 통해 어떤 우선순위에 따라 listener를 가져오고 해당 리스너를 flag 값에 따라 이벤트 버블 리스너, 이벤트 캡쳐 리스너로 분류하여 등록합니다.
function addTrappedEventListener(
targetContainer,
domEventName,
eventSystemFlags,
isCapturePhaseListener,
isDeferredListenerForLegacyFBSupport
) {
let listener = createEventListenerWrapperWithPriority(
targetContainer,
domEventName,
eventSystemFlags,
);
if (isCapturePhaseListener) { // 캡쳐 페이즈 리스너
unsubscribeListener = addEventCaptureListener(
targetContainer,
domEventName,
listener,
);
} else { // 버블 페이즈 리스너
unsubscribeListener = addEventBubbleListener(
targetContainer,
domEventName,
listener,
);
}
}
이제 리스너가 최종적으로 등록되는 시점은 알았으니 우선순위에 따라 리스너를 생성하는 로직을 살펴보겠습니다. 아래 getEventPriority
의 경우 이벤트 이름에 따라
리액트에서 지정한 우선순위를 구별하여 값을 가져옵니다. 여기서 우선순위에 따라 각각 다른 함수를 변수에 할당해주고 있는데 핵심은 모두 같은 dispatchEvent
를 기반으로
두고 있다는 것입니다. 결국 클라이언트에서 실제 이벤트가 발생되었을 때 dispatchEvent
가 트리거되게 됩니다.
export function createEventListenerWrapperWithPriority(
targetContainer,
domEventName,
eventSystemFlags
) {
const eventPriority = getEventPriority(domEventName);
let listenerWrapper;
switch (eventPriority) {
case DiscreteEventPriority:
listenerWrapper = dispatchDiscreteEvent;
break;
case ContinuousEventPriority:
listenerWrapper = dispatchContinuousEvent;
break;
case DefaultEventPriority:
default:
listenerWrapper = dispatchEvent;
break;
}
}
dispatchEvent
는 몇 가지 과정을 거쳐 dispatchEventsForPlugins
함수에 도달하게 됩니다.
해당 함수에서는 extractEvents
-> processDispatchQueue
순으로 호출됩니다.
먼저 extractEvent
의 흐름을 살펴보겠습니다.
- 해당 함수는 이벤트명을 기준으로 합성이벤트를 생성
- target 컨테이너를 기준으로 탐색하며 리스너를 수집 (
accumulateSinglePhaseListeners
) - 합성이벤트와 리스너들을 dispatchQueue에 삽입
위와 같은 과정을 통해 합성이벤트 생성과 target 컨테이너에서 리스너를 수집하여 등록하였다면 processDispatchQueue
를 통해 적절한 우선순위에 따라 큐 안의 이벤트들을 처리하게 됩니다.
실제 리액트에서는 훨씬 더 복잡한 엣지 케이스들을 고려하고 있지만 이번 분석을 통해 리액트 이벤트 시스템의 아키텍쳐를 이해할 수 있는 좋은 기회였습니다.
합성 이벤트 실제 구현
위와 같은 리액트의 접근 방식을 참고하여 합성 이벤트 시스템을 구현해보았습니다. 먼저 지원하는 이벤트명을 기준으로 각각 리스너를 등록합니다.
export function setupEventListeners(root) {
supportedEventNames.forEach((eventName) => {
listenToNativeEvent(root, eventName);
});
}
이때 리스너가 트리거될 경우 dispatchEvent
가 호출됩니다. 위에서 리액트의 흐름과 같이 extractEvent
함수를 통해
합성 이벤트를 생성하고, target 컨테이너를 기준으로 버블링 순회하며 핸드러를 수집합니다.
const syntheticEvent = extractEvent(
domEventName,
nativeEvent,
nativeEvent.target,
);
const dispatchQueue = accumulateListeners(
nativeEvent.target,
targetContainer,
domEventName,
);