React v17 Release Candidate 톺아보기

한영재
11 min readAug 11, 2020

--

작성배경

오랜만에 React Major 버전이 올라갑니다. 정식 출시가 아닌 RC(Release Candidate) 지만, 현업에서 웹개발시 메인으로 사용하고 있는 스택이기에, 그 방향성을 미리 살펴 보고자 공식문서와 몇몇 외부 자료를 사용해 해당 내용을 정리하는 글입니다.
* 글에 의역이 있으며, 추가하여 개인적인 지식과 견해를 덧붙였습니다.

Summary

React v17(이하 v17)은 개발자들이 새롭게 사용할 수 있는 API 추가 대신에, 추후 사용자들의 버전 관리를 용이하게 하기 위해서 내부 동작을 바꾸는데 초점을 두었습니다. 공식문서의 아래 문구가 v17을 한마디로 정리하고 있는 것 같습니다.

우리의 전략은 누구도 뒤쳐지지 않도록 하는 것이고, v17이 그것의 핵심이 될 것 입니다.

늘어난 선택지: Gradual Upgrades

앞으로 v18이나 먼 미래에 새로운 버전이 릴리즈 될 때 마다, 리액트 팀은 현재까지 그래 왔던 것 처럼, deprecated 된 API를 사용하는 앱들에 두가지 선택지(지속적으로 지원, 이전 버전을 사용 하도록 하기) 를 제시하는 것이 좋지 못하다고(not-great) 판단 하였고, 이번 v17을 통해 Gradual Upgrades 라는 새로운 해결책을 추가하였습니다.

이는 버전 관리가 되지 않은 오래된 코드베이스에서 주요 버전 업그레이드를 위해 전체 앱을 한번에 변경 하기 부담스러웠던 사용자들을 위해 하나의 해결책을 추가 제공하는 것 뿐이지, 여전히 최상의 해결책은 “전체 앱을 한번에 업그레이드 하는 것" 이라고 말하고 있습니다.

추가하여, 위와 같은 이유로 불가피하게, 오래된 코드베이스에서 새로운 API 를 사용하길 원하는 사용자들은 한 페이지에 두 가지 버전의 React를 사용 할 수 있었지만, 이는 이벤트 핸들링에 문제를 야기했습니다. (이는, v17에서 개선되었으며, 변경 사항 중 하나인 이벤트 위임 변경의 주된 이유 입니다.)

무엇을 유의해야 할까: breaking change

Semantic Versioning(이하 Semver) 에서 major 버전 변경은 breaking change 를 의미합니다. 그렇다면 무엇이 이에 해당되고, 추후 정식 릴리즈시 현업에 적용을 하기 위해서 무엇을 조심해야할까요? 이를 위해, 위에서 언급 했던 내용을 살펴봅시다.

한 페이지에 두 가지 버전의 React를 사용 할 수 있었지만, 이는 이벤트 핸들링에 문제를 야기했습니다.

위 문제를 해결하기 위한 React event system 변경을 필두로 작은 Breaking Changes 들이 수반되었고, 이것이 잠재적으로 기존코드의 이상동작을 유발 할 수 있습니다. 리액트 팀 테스트 결과 100000개당 20개 미만의 Component만이 변경을 요했고, 이는 대부분의 앱은 큰 문제 없이 v17로 업그레이드 가능함을 의미합니다. 이를 포함해 여러개의 Breaking Changes 들을 아래에서 더 자세히 다뤄보겠습니다.

이벤트 위임 변경: Changes to Event Delegation

일반적으로, Component 에서 이벤트 핸들러를 인라인으로 작성하고 상응하는 바닐라 DOM 은 다음과 같습니다.

// React
<button onClick={handleClick} />
// Vanila DOM
button.addEventListener('click', handleClick);

그러나 대부분의 이벤트에서 React는 실제로 이벤트를 선언하는 DOM 노드에 연결하는 것이 아닙니다. 대신 React는 하나의 핸들러를 이벤트 타입마다 바로 document 노드에 직접 연결(directly attaches)합니다. 이를 이벤트 위임(Event Delegation) 이라고 합니다.

여기서 더 나은 이해를 위해 이벤트 위임을 간단히 정리하자면 다음과 같습니다.

특정 노드 마다 이벤트 리스너를 추가하는 대신 이벤트 리스너를 임의의 부모(one parent)에게 추가하는것을 “이벤트 위임” 이라고 합니다. 이 위임된 이벤트 리스너는 버블링 된 이벤트를 분석하여 하위 요소에서 일치하는 항목을 찾습니다.

이벤트 위임이 없었다면, 어떤 하위 요소들이 빈번하게 추가되고 제거될 때마다, 해당하는 이벤트 리스너를 일일이 추가하고 제거해야하는 끔찍한 일이 발생했을 것 입니다. 더 알아보기

즉 리액트 출시 될 때부터, 내부적으로 이벤트 위임을 document 에 해주고 있었습니다. 이는 모든 리액트를 위임 받은 document 에서 이벤트가 발생하면, React event system 을 통해 실제 이벤트가 발생한 컴포넌트를 찾고, 이벤트 버블링을 통해 상위 컴포넌트로 전달합니다. 이 때 문제점이 하나 발생하는데, 실제 래핑된 리액트 이벤트가 아닌, native 이벤트는 document level(리액트가 위임 대상으로 삼고 있는 곳) 까지 이벤트가 버블링 됩니다.

이는 점진적 업그레이드에 큰 방해요소로 작용합니다. 위에서 언급 했듯이, 한 페이지에 여러개의 React 버전이 있는 경우 모두 한 document에 이벤트를 위임하게 되고, 이는 stopPropagation() 의 사이드이펙트(의도치 않게 다른 ROOT ELEMENT를 사용 하는 곳에도 영향을 끼침)를 유발합니다.

이것이 아래 이미지 처럼 ROOT ELEMENT 로 이벤트 위임 대상을 변경하는 이유 입니다.

공식 홈페이지 이미지 참조

이러한 변경 덕분에, v17 부터는 다음과 같은 경우들에서 더욱 안전하게 리액트를 사용할 수 있게 되었습니다.

1. 한 HTML 에서 여러개의 React가 상존하며 dom 을 생성하는 경우
2. 다른 기술로 빌드 된 앱 일부에 React 를 새롭게 적용하는 경우

* Note: 추가하여, 루트 컨테이너 외부에서 존재하는 Portals 은, 리액트 내부적으로 이벤트들을 리스닝 하도록 구현되어 있어서 걱정하지 않아도 됩니다.

브라우저 최적화: Aligning with Browsers

브라우저 최적화를 위해, 이벤트 시스템과 관련하여 몇 가지 작은 사항들이 변경되었습니다. 아래의 변경 사항은 React와 브라우저의 상호 운영성을 향상 시킵니다.

  1. onScroll 관련 이벤트 버블링 제거: 이는 네이티브 onScroll 이벤트가 버블링 되지 않는 것과 달리, React onScroll 이벤트가 버블링되고 있어서 발생하던 혼란을 해소 합니다.
  2. onFocus, onBlur 이벤트가 네이티브 focusin, focusout 이벤트를 사용하도록 변경
  3. Capture phase event (e.g. onClickCaptrue)가 실제 브라우저 캡쳐 페이즈 리스너를 사용하도록 변경

* Note: 리액트의 onFocus 이벤트 버블링은, 이전처럼 버블링 되는 것이 더욱 유용하기에, 기존과 같이 유지되었습니다. 다양한 용례 살펴보기

이벤트 풀링 최적화 제거: No Event Pooling

최신 브라우저에서 성능을 향상 시키지 않으며, 숙련된 리액트 사용자들 조차 혼란스럽게 했던 이벤트 풀링을 제거 합니다. 즉, SyntheticEvent 객체는 이벤트 핸들러가 호출된 후 초기화되기에, 비동기적으로 이벤트 객체에 접근할 수 없었던 것이 수정됩니다.

function handleChange(e) {
setData(data => ({
...data,
text: e.target.value <- before v17: null
}));
}

여기서 풀링이란, 자주 재사용되는 오브젝트들을 미리 만들어놓고 활용하는 기법을 말합니다. 즉 연못에 물고기 오브젝트들을 잔뜩 만들어 놓고 hide, show 메서드를 통해 탄생과 죽음을 표현하며 재사용하는 것과 유사합니다. 이는 어떤 경우에서는 물고기가 태어나고 죽을 때마다 오브젝트를 생성하고 파괴하는 리소스가 더욱 크기 때문입니다.

Effect Cleanup Timing

useEffect 라이프사이클 메서드의 Cleanup Timing을 보다 일관되게 동작하도록 만들고 있습니다.

useEffect(() => {
return () => {
// 이곳이 클린업 타이밍 (componentWillUnmount 와 유사)
}
}

대부분의 경우에 스크린 업데이트를 지연시킬 필요가 없으므로, useEffect cleanup은 변경사항이 스크린에 반영된 직후 비동기적으로 동작하도록 변경되었습니다. (드물게, 페인팅을 지연시킬 필요가 있는 경우{e.g. 툴팁 포지션을 측정}에는 useLayoutEffect 를 사용하면 됩니다.)

기존에 useEffect cleanup 기능은 v16 에서 동기적으로 실행하기 위해 사용되 었습니다. 이는 기존 클래스 컴포넌트에서 동기적으로 동작하는componentWillUnmout 와 유사하게, 탭(페이지) 변경 과 같은 큰 페이지(Large Screen)전환 시 성능저하를 유발합니다. (느린 페이지 전환)

이 변경 사항이후, 컴포넌트가 언마운트 되면 화면이 업데이트 된 후 cleanup 이 실행 됩니다. 드물게, 동기적으로 실행해야 하는 경우에는 위에서 언급했듯, useLayoutEffect 를 사용할 수 있습니다.

추가하여, v17은 클린업 기능을 돔 트리에 위치한 순서와 같은 순서로 실행하는 것을 보장합니다. (이전에는 때때로 순서가 보장되지 않는 경우가 있었습니다.)

잠재적 문제와 해결법: 캡쳐링

useEffect 내부에서, Ref 를 아래와 같이 사용하고 있는 코드가 있는 경우에는 문제가 발생하고, 수정되어야 합니다.

문제 코드
수정 코드

이는 useEffect 클린업 동작이 비동기적으로 바뀌면서, mutable 한 ref.current 특성상 실행되는 타이밍에 null 일 수도 있습니다. 그리하여 위 수정코드 처럼 mutable 한 값들은 내부에서 캡쳐링 해주셔야 합니다.

Returning Undefined 에 대한 일관된 에러

v16 및 이전 버전에서는 모든 컴포넌트에서의 undefined 리턴은 항상 에러 처리되었습니다.

이는 의도치 않은 실수를 방지하기 위함이였습니다. 하지만 코딩 실수로, forwardRef 및 memo 컴포넌트들에서는 에러 처리를 하지 않고 있었습니다.

v17 부터는 forwardRef 및 memo 컴포넌트의 undefined 리턴은 항상 에러 처리됩니다.

* 의도적으로 아무것도 렌더링 하고 싶지 않은 경우에는 null 을 리턴하세요.

브라우저 에러 메시지 개선: Native Component Stacks

브라우저에서 에러가 발생하면, 자바스크립트 함수이름들과 해당하는 위치를 추적하여 에러 메시지에 담아 알려줍니다. 그러나 종종, Javascript Stacks들은 리액트 트리 구조를 파악하고 진단하기에 충분하지 않습니다.

즉 우리는, 리액트 <Button /> 컴포넌트에 에러가 발생했다는 것 뿐만아니라, 해당 컴포넌트의 위치도 알고 싶을 것입니다.

이를 해결하기 위해 기존 React component Stacks을 개선한 새로운 메커니즘을 사용하여 component stacks을 생성합니다.

* 새로운 메커니즘을 설명하는 부분에 오역을 방지하고자 아래 원문을 남깁니다.

In React 17, the component stacks are generated using a different mechanism that stitches them together from the regular native JavaScript stacks. This lets you get the fully symbolicated React component stack traces in a production environment.

맺으며

이 외에도 여러 Change Log 들이 있지만, 글의 길이상 주요 변경 사항만 정리하였습니다. 더 자세한 내용을 보고 싶으신 분들은 아래 공식문서 링크를 남겨드릴테니 살펴보시면 좋을 것 같습니다. 추후 정식 릴리즈 시 v17로의 업그레이드는 분명 적지 않은 작업과 테스트를 수반 하겠지만, 추후 지원될 개선된 API 들을 사용하기 위해, v17 이라는 디딤돌에 올라서는 것이 분명 의미있는 도전 일 것 같습니다.

끝으로, 정말 다양한 개발 환경에 놓여있는 사용자들을 배려하는 리액트 팀에게 더욱 신뢰를 가지게 되는 마음이 따뜻해지는 업데이트 였던 것 같습니다.

우리의 전략은 누구도 뒤쳐지지 않도록 하는 것이고, v17이 그것의 핵심이 될 것 입니다.

참고자료

--

--