[React] 리액트를 처음부터 배워보자. — 06. 합성 이벤트와 Event Pooling
이 글을 남기는 계기는 Dan Ambramov의 블로그 Overreacted를 보며 React.js에 대해 더 깊이 공부할 필요성을 느꼈기 때문이다.
지난 글을 쓰고 다음 글감을 고민하던 중 스크롤 이벤트를 처리하면서 debounce와 throttle를 쓰는 경험을 하였다.
이 과정에서 그동안 잘 사용하던 event 객체의 내부 값이 null이 되어 사라지는 문제를 겪었고 그간 아무 생각 없이 사용하던 event.persist(
)에 대한 궁금증이 생겼다.
이에 대한 원리를 파악하기 위해 React의 이벤트 처리와 합성 이벤트(Synthetic Event) 문서를 읽으며 이벤트 풀링과 합성 이벤트라는 개념에 대해 더 구체적으로 알게 되었다.
React는 이벤트를 처리하기 위해 바닐라 자바스크립트와 달리 엘리먼트가 렌더링 될 때 이벤트 리스너(Event Listener)를 제공해 이벤트를 처리한다.
다만 이 이벤트 핸들러는 모든 브라우저에서 동일한 처리를 보장하기 위해 React에서 제공하는 SyntheticEvent 객체
를 전달 받는다.
소프트웨어에서 래핑(Wrapping)이란 기본 기능을 감싸는 새로운 기능을 만드는 것을 말한다. 유사 개념으로 GoF 디자인 패턴의 어댑터(Adapter), 브릿지(Bridge) 패턴이 있다.
즉, 한 단계 래핑(Wrapping)된 이벤트 객체를 통해 이벤트를 다루는 것이다.
SyntheticEvent 객체
는 모든 브라우저에서 이벤트를 동일하게 처리하기 위한 래퍼 객체이다. 대부분의 인터페이스는 브라우저 고유 이벤트와 같다.
이번 글은 SynctheticEvent객체
가 어떤 원리로 동작하고, 의사 코드를 통해 알게 된 이벤트 풀링의 대략적 원리에 대한 글이다.
01. 합성 이벤트(Synthetic Event)와 이벤트 풀링
React 공식 문서에는 이벤트 풀링에 대해 다음과 같이 써 있다.
SyntheticEvent
는Pooling
되며 성능상의 이유로SyntheticEvent객체
는 재사용 되고 모든 속성은 이벤트 핸들러가 호출된 다음 초기화 됩니다.
따라서 비동기적으로 이벤트 객체에 접근할 수 없습니다.
이 부분에 대한 설명이 내가 이 글을 쓰게 된 계기이다.
핵심은 React에서 엘리먼트의 이벤트를 처리하기 위해 제공하는 이벤트 리스너의 인자(event, e)가 매번 초기화가 된다는 것이다.
해당 개념에 대해 이해하기 위해 풀링(Pooling)이라는 개념을 먼저 이해해보자. 풀링은 컴퓨터 과학에서 풀링은 다음과 같이 정의한다.
컴퓨터 과학에서 풀(pool)은 재사용 될 준비를 하는 자원(Resource)의 집합입니다.
즉, 특정 이벤트가(클릭, 터치, 호버, 스크롤 등) 발생할 때 다음과 같은 순서로 이벤트 로직이 실행되는 것이다.
- 합성 이벤트 풀(Synthetic Event Pool)에서
SyntheticEvent 객체
에 대한 참조를 받음 - 이벤트 정보 Synthetic Event 객체에 넣어줌
- 유저가 정의한 이벤트 리스너가 수행
SyntheticEvent 객체
를 초기화 한다는 것이다.
즉, 내가 겪은 debounce 코드에서 event 객체의 속성값이 null이 되는 문제는 위와 같은 React 내부 동작에 의한 자연스러운 동작이었다.
또한, 해당 문제를 해결하기 위해서 React에서는 event.persist()
라는 기능을 제공한다.
02. 이벤트 풀링 Event Pooling
문제에 대한 원인과 해결책을 찾았지만, 사실 해당 부분에 대한 추상적 이해를 넘어 구현체에 대한 궁금증이 생겼다.
하지만 아쉽게도 해당 부분에 대한 명확한 파악이 불가능하여, 이벤트 풀링 구현체 대한 코드와 원리에 대한 질문을 Stack Overflow에 올렸다.
해당 질문에 대해 Alex Povar라는 유저가 답변을 해주었고, 해당 의사코드에 기반을 두어 이벤트 풀링이 어떤 원리로 동작 하는지 파악하고자 한다.
먼저 SyntheticEvent에 대한 의사코드를 보자. 해당 부분에서 확인할 수 있는 것은 SyntheticEvent
에서 3가지 상태를 가진다는 것을 알 수 있다.
실제 리액트 코드에서는
status
와 같은 부분이 없지만, 이벤트 풀링의 원리를 이해하기 위해 있다고 가정한다.
POOLED
이벤트 풀에 등록되어 대기중인 상태PERSISTED
이벤트 처리가 끝났는데도 남아있는 상태IN_USE
네이티브 이벤트를 할당 받아 SyntheticEvent로 래핑된 상태
이 status
에 따라 이벤트 풀은 Synthetic Event를 아래와 같이 관리한다.
위의 과정에서 핵심적으로 볼 것은 pullEvent
와 tryPushEvent
이다.
EventPool
인스턴스는 내부적으로 POOLED
상태의 비어있는 SyntheticEvent
객체의 배열을 가지고, 실제 이벤트가 발생할 때 pullEvent
메소드를 호출해 다음 동작을 한다.
allocateNewEvent()
를 통해 이벤트 풀에 대기중인SyntheticEvent
를 채움getFromPool()
메소드를 통해POOLED
상태의SyntheticEvent
를 반환populateEvent()
메소드를 통해nativeEvent
를SyntheticEvent
에 복사한 후IN_USE
상태로 변경SyntheticEvent
를 반환
이렇게 생성된 SyntheticEvent
는 이벤트 핸들러 동작이 완료된 후 tryPushEvent
메소드를 통해 다음 과정으로 정리 된다.
persist()
함수가 호출되어PERSITED
상태가 되면 아무 동작을 안함SyntheticEvent
가PERSITED
상태가 아니면clearEvent
를 통해 SyntheticEvent 안의 속성들을 null로 초기화
이제 위의 EventPool 인스턴스가 어떻게 동작할 수 있는지 파악해보자.
위의 예시를 보면 nativeEvent
를 어떻게 SyntheticEvent
로 변환하고, 이를 사용한 후 다시 Event Pool에 반환하는지에 대한 과정을 확인할 수 있다.
위의 구현체를 파악한 결과 userDefinedOnClikHandler
에서 event.persist()
를 호출하지 않는 경우 tryPushEvent
를 통해 syntheticEvent가 사라진다는 것을 확인할 수 있었다.
따라서, 해당 의사 구현체가 리액트의 이벤트 풀링의 원리를 설명하는 것에 부족함이 없을 것 같다.
03. 이벤트 델리게이션 Event Delegation
이 글을 쓰며, 이벤트에 대한 다양한 것을 배우며 리액트에서 이벤트를 다루기 위해 쓰는 이벤트 델리게이션 패턴에 대해서도 알게 되었다.
Vanilla Javascript에서는 이벤트 리스너를 달아준 후, 해당 요소가 사라질 때이벤트 리스너가 지워지지 않았으면 좀비 메모리가 발생한다.
델리게이션(Delegation)은 소프트웨어 공학에서 하나의 패턴과 같으며, 로직을 중앙에 집중(Centralization)시키기 위한 패턴이다.
React는 이벤트를 다룰 때 이벤트 델리게이션 개념을 사용해 자식 요소에 모든 이벤트 리스너를 달아주는 것이 아니라, 가장 상위의 부모 요소에 이벤트 리스너를 달아준다.
이벤트 델리게이션(Event Delegation)은 이벤트 캡처링과 버블링을 활용하는 방식이다.
공통된 부모 요소의event.target
을 이용해 실제 이벤트의 발생을 감지하고 핸들링 하는 것이 핵심이다.
즉, React는 자체적으로 최상위 요소에만 이벤트를 등록하고 자식 요소의 이벤트를 event.target
으로 감지해 핸들링 한다.
해당 부분에 대해 파악하기 위해 실제 리액트에서 이벤트 처리를 담당하는 메인 소스 코드 dispatchEvent
를 파악해보자.
해당 부분에서 nativeEvent
의 nativeEventTarget
을 통해 Fiber를 찾아내는 것을 확인할 수 있다.
이 부분에 대해서 정확하지 않지만 React에서 내부적으로 target node의 id를 통해 이벤트 델리게이션을 구현하고 있다고 한다.
event에서 target을 가져오는getEventTarget
은 다음과 같이 구현되어 있다.
해당 부분에서 target 외의 속성은 각각 이슈가 있어 추가를 한 것이라고 주석에 적혀있다.
해당 부분에 대해 궁금하면getEventTarget
에서 해당 이슈에 대해 파악해보면 좋을 것 같다.
결론
이 글을 쓰며, React의 합성 이벤트(Synthetic Event), 이벤트 풀링(Event Pooling), 이벤트 델리게이션(Event Delegation)에 대해 알 수 있었다.
프론트 엔드 개발을 하면 대부분은 이벤트 처리와 관련된 코드를 작성하면서 이벤트를 어떻게 다루고, 내부적으로 어떤 컨셉을 사용하는지 큰 관심이 없었던 것 같다.
우연한 기회로 이 부분에 대한 글을 쓰며 프론트 뿐만 아니라 소프트웨어에서 이벤트라는 것을 어떻게 정의하고 다루는 지 알 수 있었다.
특히, 이벤트 기반 아키텍처에 대해 읽으며 개발에 대해 기존에는 생각하지 못한 전략에 대해 고민해 볼 수 있었다.
예를 들어, 핵심 로직은 기존 구성에서 변경 없이 그 하나의 로직 메소드 마다 이벤트를 발생시키고, 새로운 기능(플러그인 기능)이 생길 때 마다 이벤트 리스너를 달아주는 전략에 대해 고민할 수 있게 되었다.
이 방식에도 반드시 문제점이 있을 수 있겠지만, 해당 방식으로 개발을 진행하면서 이 방법론이 왜 나왔고 어떻게 사용할 수 있을지에 대해 고민해 보며 소프트웨어를 만들어가는 새로운 방식을 배워 볼 예정이다.
맺음말
이 글을 쓰기 위해 Thread Pooling에 대해 조사하면서 이해가 되지 않는 부분이 많아 StackOverflow를 시작하였다.
올 초 부터 StackOverflow를 한 번 해보려고 했지만, 항상 영어의 진입 장벽과 “내가 이해할 수 있을까?” 하는 막연한 걱정으로 이를 겁냈지만, 막상 시도해 보니 개발을 하는데 새로운 활력이 되었다.
StackOverflow에서 내 질문에 대해 답을 해주지 않아, 명성(Reputation)을 올려야 하나 싶어 밤새 내가 할 수 있는 질문에 대해 답변하며 하루를 보내곤 했다.
약간의 명성을 얻고 Event Pooling 구현체에 관한 질문을 올렸는데, React에서는 Event Pooling으로 큰 효과를 얻지 못해 곧 릴리즈 되는 v17.x부터 Event Pooling이 사라진다는 소식도 들었다.
처음에는 그저 질문을 하기 위해서 시작했는데, 좋은 정보를 얻고 누군가에게 도움이 된다는 사실이 긍정적으로 느껴졌다.
이 글을 보는 사람 중에도 혹시 StackOverflow에 관심이 있지만 영어에 대한 장벽으로 두려워 하는 사람이 있다면, 이 기회에 한번 도전하길 바란다 :)
유용한 정보를 많이 얻을 수 있고 누군가에게 도움이 된다는 점, 그리고 점수가 쌓여 간다는 기쁨이 지쳐가는 개발 공부에 작은 단비같은 경험으로 남을 수 있을 것이다.