React Deep Dive - React Event System (2)

2021. 12. 31. 22:47Frontend

 

 

위 아티클과 첨부된 Code Snippet은 React 17버전(17.0.1) 기준으로 작성되었습니다. 17버전 이전(16버전 이하)에서는 다르게 동작할 수 있습니다.

 

Overview

지난 포스팅에서 리액트가 어떤 식으로 이벤트를 핸들링 하는지에 대해서 살펴보았습니다. 이번 포스팅에서는 지난 포스팅에서 소개한 것처럼, 리액트가 17버전 이후부터 이벤트를 핸들링 하는 방식이 어떻게 달라졌는지에 대한 내용들을 살펴보면서 17버전으로 업데이트 할 때 어떤 영향을 미치는지, 또 어떤 점들을 주의해야 하는지를 살펴보려고 합니다. 

 


 

Changes to Event Delegation

리액트 17버전이 발표되면서 발표한 공식문서의 첫 번째 섹션에는 “No New Features”라는 타이틀이 달려 있습니다. 즉 리액트 17버전에는 이전 버전에서 새롭게 추가된 API(문서의 설명에 따르면 “new developer-facing features”)가 없다는 의미입니다. 하지만 바로 다음 섹션에서 “Gradual Upgrades”에 대한 언급을 하면서 리액트17버전은 새로운 기능상의 추가는 없지만, 이후에 나올 버전(18, 19…)이 17버전과 호환이 될 수 있도록 하여 커다란 애플리케이션을 점진적으로(Gradual) 업데이트하는데 아무런 문제가 없도록 지원하기 위한 목적의 Major Version Update 임을 분명히 하였습니다. 

 

“새로운 기능 추가는 없지만, Gradual Upgrade를 지원한다” 라는 목적을 달성하기 위해서 리액트 17에는 어떤 변화가 생긴 것일까요? 공식문서에서는 다음과 같이 설명하며 “이벤트를 다루는 방식에 대한 변화” 가 있음을 언급하였습니다.

In React 17, React will no longer attach event handlers at the document level. Instead, it will attach them to the root DOM container into which your React tree is rendered: — React Blog.

위 내용을 살펴보면 기존의 리액트(16버전을 기준으로 설명합니다)는 이벤트 핸들러를 HTML문서의 Document에 부착하지 않고, root DOM container에 부착하는 방식으로 변경됩니다. 

 

 

 

지난 포스팅에서 설명한 대로, 리액트는 이벤트 위임(“Event Delegation”) 방식을 통해 이벤트를 처리합니다. 간단히 말해서, 특정 컴포넌트의 버튼에서 클릭 이벤트가 발생하더라도, 실제로 이벤트 핸들러는 해당 버튼에 붙어있는 것이 아닌 root Element (16버전에는 Document, 17버전부터는 root DOM container)에 붙어있게 되는 것입니다.  

 

기존(17버전 이전)에는 규모가 큰 앱의 리액트 버전을 업데이트 할 때, 한번에 업데이트 하지 않고 부분부분 업데이트를 하는 상황, 즉 하나의 애플리케이션에서 서로 다른 버전의 리액트가 존재하는 상황에서 이러한 이벤트 핸들링의 방식이 버그 요인으로 지목 되곤 했습니다. 하나의 애플리케이션에서 서로 다른 두 버전의 리액트가 존재한다고 했을 때, 두 버전의 리액트가 모두 Document Level에 이벤트 리스너를 부착하기 때문에 버그가 생기기 쉬운 환경이 되는 것입니다. 

 

하지만 이번 업데이트에서는 이벤트 리스너를 Document Level이 아닌 root DOM container에 부착함으로써, 서로 다른 버전의 리액트가 각자 자신의 root DOM container에서 이벤트 리스너를 각기 운영할 수 있게 되었습니다. 따라서 이후 리액트 20버전에서 이벤트를 처리하는 방식이 매우 급진적으로 변경되어도 (Dramatic Change) 해당 버전의 이벤트 처리 방식의 적용은 해당 리액트를 사용한 root DOM container의 하위 요소들에만 적용이되고, 기존 17버전의 리액트를 사용한 컴포넌트들은 여전히 안정적으로 17버전의 이벤트 처리 방식을 사용할 수 있게 되는 것입니다. 이것이 리액트 팀에서 이벤트 핸들러를 부착하는 위치의 변경이 “Gradual Upgrade”를 위한 주춧돌이라고 설명한 이유입니다.

 

https://reactjs.org/

 

 

Event Pooling

이벤트 풀링에 대한 리액트 공식 문서에 따르면, 리액트 17버전부터는 더 이상 이벤트 풀링이 적용되지 않는다고 설명합니다. 이를 조금 더 이해하기 위해서는 우선 “Event Pooling” 이 무엇인지를 이해해야 합니다.

 

리액트에서는 이벤트 처리를 위해 브라우저의 native event를 그대로 사용하지 않고 이를 한번 Wrapping한 Synthetic Event를 사용합니다. native event를 한번 Wrapping한 인스턴스를 사용하는 것이기 때문에 이벤트가 발생할 때마다 인스턴스를 생성해야 하며, 이 인스턴스를 저장하기 위한 메모리가 할당됩니다. 또한 이벤트가 처리되고 나면 GC(Garbage Collector)가 이 메모리를 해제해주는 작업도 필요합니다. 실제로 모던 웹 애플리케이션은 수많은 유저 인터렉션을 제공하고, 유저 인터렉션은 이벤트를 발생시킵니다. 여기서 “이벤트가 발생할때마다 Synthetic Event 객체를 생성하고 제거하고 하지 말고, Synthetic Event Pool을 만들어서 이벤트가 발생할때 이 Pool을 사용하면 어떨까?” 라는 아이디어로 만들어진 것이 바로 Event Pool 이라는 개념입니다.

 

Event Pool은 다음과 같은 방식으로 동작합니다.

  • Event Pool에 Synthetic Event Instance를 구비한다.
  • 이벤트가 트리거되면 해당 이벤트의 native event는 Event Pool의 인스턴스를 사용해서 Synthetic Event로 래핑한다. (populate its properties)
  • 이벤트 핸들러가 종료되어 콜 스택에서 빠져나오면 해당 인스턴스는 초기화되어 다시 풀로 돌아간다.

 

https://hub.packtpub.com/how-to-perform-event-handling-in-react-tutorial/

 

언뜻 보기에는 메모리도 아낄 수 있고, 불필요하게 GC가 자주 동작할 필요도 없어서 효율적인 시스템인 것 같아 보입니다. 하지만 이벤트가 트리거 되었을 때 인스턴스 풀을 받아오고 이벤트 핸들러가 종료되자마자 다시 인스턴스를 release하는 방식은 필연적으로 비동기 이벤트에 대한 추가적인 대응 (e.persist())을 필요로 했습니다. 따라서 아무런 대응 없이 동기 이벤트와 동일한 방식으로 비동기 이벤트 콜백을 작성하면 문제가 발생했고, 이는 “직관적이지 않은” 개발 경험을 주게 됩니다.

 

 
After Nullyfy뒤에 나오는 이벤트는 정상적인 Synthetic Event를 반환하지 않습니다.

 

“Warning: This synthetic event is reused for performance reasons”

이를테면, 위와 같은 코드를 작성하면 개발 환경에서 리액트는 위와 같은 Warning Message를 전달하며 이를 해결하기 위해 개발자는 e.persist 메서드를 사용해서 별도의 메모리 공간에 해당 이벤트의 Synthetic Event 객체를 할당해야 했습니다. 따라서 실제로 Event Pooling이 성능이 좋아진 모던 브라우저에서는 유의미한 성능 개선을 보여주지 않는 다는 이유와 더불어서 리액트 17버전 부터는 이벤트 풀링이 더 이상 적용되지 않습니다. (기존의 e.persist 메서드는 사용가능하지만, 실제로는 아무것도 수행하지 않습니다). 따라서 17버전부터는 이벤트 핸들러가 동기 함수인지, 비동기 함수인지를 신경쓰지 않고 비즈니스 로직에 집중해서 개발할 수 있게 됩니다.

 

 

Conclusion

React Deep Dive — React Event System (1), (2)를 통해 리액트에서 이벤트를 어떤 식으로 처리하는지, 17버전부터는 리액트에서 이벤트를 처리하는 방식이 어떻게 바뀌었는지에 대해 실제 리액트의 구현 코드를 기준으로 살펴보았습니다. 결론적으로는 React 16에서 17로 마이그레이션을 고려하고 계시다면 Event Listener가 더 이상 document Level이 아닌 root DOM container에 붙게 되므로 다른 위치에서 이벤트 핸들링이 일어난다는 점(하지만 비즈니스 로직을 작성하는 방식에는 변화가 없을 것입니다.)과 Event Pooling이 제거되어 비동기와 동기 함수를 같은 방식으로 처리할 수 있으므로 e.persist와 같은 메서드를 호출할 필요가 없다는 점을 추가적으로 고려하면 좋을 것입니다

 

반응형

'Frontend' 카테고리의 다른 글

[Web.dev] Web Security (2)  (0) 2022.02.18
[Web.dev] Web Security (1)  (0) 2022.02.10
[React] Atomic Design Pattern에 대한 고찰  (0) 2021.09.18
React Deep Dive - React Event System (1)  (0) 2021.07.19
Async / Await Under the Hood  (1) 2021.06.29