📝강의를 시청하면서 궁금한 점이 있거나, 질문 또는 의견을 남기고 싶다면? 페이지 하단(↓) "토론" 섹션에 남겨주세요.

# React v17


2020년 10월 20일 React의 공식 버전이 17로 업데이트 되었습니다. (opens new window)

# 새로운 건 없음

React 17에 새롭게 추가된 새로운 API는 없지만, 새로운 JSX 트랜스폼을 제공합니다.
이번 버전의 업데이트 목적은 버전 관리를 용이하게 하기 위해 내부적인 작동 방식을 바꾸는데 있습니다.

# 점진적 업그레이드

React의 새로운 버전이 배포될 때 마다 더 이상 사용이 권장되지 않는(DPRECATED) API를 사용하는 앱에 2가지 선택지(지속적 지원 또는 이전 버전 사용)를 제시하는 것에 문제가 있다고 보아, 17 버전부터는 점진적 업그레이드(Gradual Upgrades) 방법으로 문제를 해결하고자 합니다.

즉, 버전 관리가 되지 않은 오래된 코드베이스에서 주요 버전 업그레이드를 위해 전체 앱을 한번에 업그레이드 하기 부담되는 사용자들을 위해 또 하나의 선택지를 추가 제공하는 것일 뿐, 가장 좋은 방법은 React 앱을 한번에 업그레이드 하는 것입니다. 오래된 코드베이스에서 새로운 API를 사용하길 원하는 사용자들은 2가지 버전의 React를 사용 할 수 있었지만, 2가지 버전을 사용함에 따라 이벤트 핸들링에 문제가 생기기도 했습니다. 이 문제는 17 버전에서 개선되어 더 이상 문제를 야기하지 않습니다.

# 이벤트 위임 대상 변경

점진적 업데이트를 반영하기 위헤 React 이벤트 시스템 방식을 다소 변경해야 했습니다. React 17 버전부터는 document에 이벤트 리스너를 연결해 위임하는 방식 대신, React의 가상 DOM 트리가 렌더링 되는 루트 DOM 컨테이너에 이벤트를 연결합니다.

ReactDOM.render(
  // React 가상 DOM 트리
  <App />,
  // 실제 DOM 요소 → 루트 DOM 컨테이너
  document.getElementById('root')
);
1
2
3
4
5
6

React 16 버전까지는 document.addEventListener()에 대부분의 이벤트를 연결해 위임했습니다.
하지만 React 17 부터는 rootNode.addEventListener()에 이벤트를 연결해 위임합니다. 아래 그림을 보면 이해하기 쉬울 겁니다.

이벤트 위임(Event Delegation)이란?

특정 노드에 일일이 이벤트 리스너를 추가하는 대신, 이벤트 리스너를 특정 노드들을 포함하는 상위 노드에 연결하여 이벤트를 전파하는 것을 이벤트 위임이라고 합니다. 위임된 이벤트는 포함된 하위 노드에 전파됩니다. 이벤트가 전파되는 방식의 기본 값은 버블링(bubbling)이며, 캡처링(capturing) 방식으로 변경해 사용할 수도 있습니다.

<ul class="parentNode">
  <li class="childNode"><a href="/child-node-1">하위 노드 1</a></li>
  <li class="childNode"><a href="/child-node-2">하위 노드 2</a></li>
  <li class="childNode"><a href="/child-node-3">하위 노드 3</a></li>
</ul>
1
2
3
4
5
// 상위(부모) 노드
const parentNode = document.querySelector(".parentNode")

// 상위 노드에 이벤트 리스너 연결
parentNode.addEventListener('click', (e) => {
  const nodeName = target.nodeName.toLowerCase()
  // 하위 노드에 이벤트 전파 
  // (해당 대상 노드에 처리 코드 작성)
  switch(nodeName) {
    case 'li': 
      console.log('<li> 노드 클릭') 
      break
    case 'a': 
      // 이벤트 기본 동작 차단
      e.preventDefault()
      console.log('<a> 노드 클릭') 
      break
    defualt:
      console.log(`${nodeName} 노드 클릭`)
  }
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

이벤트 위임을 사용할 수 없다면, 하위 노드들이 실시간으로 추가 또는 제거될 때마다 이벤트 리스너를 매번 연결하거나 제거해야 해서 끔찍했을 것입니다. 이벤트 위임을 사용할 수 있기에 아래와 같이 새로운 노드가 추가, 제거되어 업데이트 되어도 별도로 이벤트 리스너를 연결, 제거하지 않아도 됩니다.

<ul class="parentNode">
  <li class="childNode"><a href="/child-node-1">하위 노드 1</a></li>
  <!-- 실시간 제거된 노드 -->
  <!-- <li class="childNode"><a href="/child-node-2">하위 노드 2</a></li> -->
  <li class="childNode"><a href="/child-node-3">하위 노드 3</a></li>
  <!-- 실시간 추가된 노드 -->
  <li class="childNode"><a href="/child-node-3">하위 노드 4</a></li>
  <li class="childNode"><a href="/child-node-3">하위 노드 5</a></li>
  <li class="childNode"><a href="/child-node-3">하위 노드 6</a></li>
</ul>
1
2
3
4
5
6
7
8
9
10

이러한 변경 덕분에, 17 버전부터는 다음과 같은 경우에 더욱 안전하게 React를 사용할 수 있게 되었습니다.

  1. HTML에서 여러 버전의 React가 공존하며 DOM에 앱을 생성하는 경우
  2. 다른 기술로 빌드 된 앱 일부에 React를 사용해 적용할 경우

포털(Portal) 사용 시 문제는 없을까?

루트 컨테이너 외부에 생성되는 포털(Portals) (opens new window)은 React 내부적으로 이벤트를 리스닝 하도록 구현되어 있어 문제가 발생하지 않습니다.

# 새로운 JSX 트랜스폼

React 17은 새로운 JSX 트랜스폼 방식을 지원 (opens new window)합니다. 이 방식은 옵션(선택사항)으로 반드시 사용해야 할 필요는 없으니 기존 방식 사용자는 걱정하지 않아도 됩니다. 클래식 JSX 트랜스폼은 지속적으로 작동될 것이고, 향후에도 지원을 중단 할 계획이 없습니다.

# 새로운 트랜스폼의 특징

  • React를 불러오지 않고도 JSX를 사용할 수 있습니다.
  • 클래식 JSX 트랜스폼에 비해 번들링 시 번들 크기를 약간 개선할 수 있습니다.
  • React를 학습하는데 알아야 할 개념에 대한 노력을 줄일 수 있고 향후 개선을 가능하게 합니다.

# 새로운 JSX 트랜스폼 vs 클래식 JSX

클래식 JSX는 다음과 같이 작성합니다.

import React from 'react'

const App = () => <h1>안녕 React :-)</h1>
1
2
3

내부적으로 JSX는 다음의 일반 JavaScript 코드로 변환(Transform) 됩니다.

import React from 'react';

function App() {
  return React.createElement('h1', null, '안녕 React :-)');
}
1
2
3
4
5

하지만 이 방식은 완벽하지 않습니다.

  • JSX 코드가 React.createElement() 코드로 변환해야 하므로 반드시 import React from 'react' 코드가 필요합니다.
  • React.createElement()는 성능 문제가 다소 있습니다.

이러한 문제를 해결한 React 17은 클래식 JSX와 달리 React를 반드시 불러올 필요는 없습니다. 예를 들어 아래와 같이 React 코드를 작성하면

const App = () => <h1>안녕 React :-)</h1>
1

새로운 JSX 트랜스폼에 의해, 컴파일 과정에 자동으로 JSX를 변환하는 코드가 추가됩니다.

 





import { jsx as _jsx } from 'react/jsx-runtime';

function App() {
  return _jsx('h1', { children: '안녕 React :-)' });
}
1
2
3
4
5

JSX를 사용하기 위해 React를 반드시 호출해야 했던 클래식 JSX와 비교되는 부분입니다.
단, React 훅(Hooks)을 사용할 경우 여전히 React를 가져와야 함에 주의하세요.

 


 




import { useState } from 'react'

const App = (props) => {
  const [message] = useState('안녕 React :-)')
  
  return <h1>{message}</h1>
}
1
2
3
4
5
6
7

# 새로운 JSX 트랜스폼을 사용하는 방법

새로운 JSX 트랜스폼을 지원하는 도구는 다음과 같습니다.

  • React v17.0.0+
  • Create React App v4.0.0+
  • Next.js v9.5.3+
  • Gatsby v2.24.5+

# 불 필요한 React 호출 코드 제거

새로운 JSX 트랜스폼은 react/jsx-runtime 모듈을 자동으로 가져 오기 때문에 JSX를 사용할 때 더 이상 React를 호출할 필요가 없습니다. 즉, 코드에서 사용되지 않은 React 호출 코드를 제거해도 무방합니다. 물론 코드를 놔둬도 문제는 없지만, 제거하려면 codemod (opens new window) 스크립트를 실행하여 자동으로 제거할 수 있습니다.

$ npx react-codemod update-react-imports
1
? On which files or directory should the codemods be applied? .
? Which dialect of JavaScript do you use? JavaScript
? Destructure namespace imports (import *) too? No
Executing command: jscodeshift --verbose=2 --ignore-pattern=**/node_modules/** --parser babel --extensions=jsx,js --transform /Users/yamoo9/.npm/_npx/42946/lib/node_modules/react-codemod/transforms/update-react-imports.js .
Processing 35 files... 
Spawning 15 workers...
Sending 3 files to free worker...
Sending 3 files to free worker...
Sending 3 files to free worker...
.
.
.
SKIP src/utils/dom/getNodeList.js
SKIP src/utils/dom/index.js
SKIP src/utils/delay.js
.
.
.
OKK src/pages/Home/RecommendContainer.js
OKK src/pages/Home/RecommendList.js
OKK src/pages/Home/Notice.js

All done. 
Results: 
0 errors
0 unmodified
15 skipped
20 ok
Time elapsed: 1.382seconds
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29

더 이상 사용되지 않는 라이프 사이클 훅을 자동 개선하려면?

codemod 명령 중 rename-unsafe-lifecycles 명령을 사용하면 손쉽게 자동으로 처리합니다.

npx react-codemod rename-unsafe-lifecycles --force
1

# 브라우저 최적화

브라우저 최적화를 위해 이벤트 시스템과 관련한 작은 변화가 있었습니다. 아래의 변경 사항은 React와 브라우저의 상호 운용성을 향상시킵니다.

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

React onFocus 이벤트 버블링은 이전 방식(버블링)이 유용해 기존과 동일하게 처리됩니다.

# 이벤트 풀링(Pooling) 최적화 제거

최신 브라우저에서는 성능 향상 효과가 미미하고, React 사용자를 혼란스럽게 했던 이벤트 풀링을 제거했습니다. 다시말해 합성 이벤트(SyntheticEvent) (opens new window) 객체는 이벤트 핸들러가 호출된 후 초기화되므로 비동기적으로 이벤트 객체에 접근할 수 없었던 것이 수정되었습니다.

React v16




 



function handleChange(e) {
  setData(data => ({
    ...data,
    text: e.target.value // null
  }));
}
1
2
3
4
5
6

16 버전까지는 이벤트 대상의 값을 가져오기 위해서는 e.persist() 메서드를 아래와 같이 사용해야 했습니다.


 







function handleChange(e) {
  e.persist();

  setData(data => ({
    ...data,
    text: e.target.value // null
  }));
}
1
2
3
4
5
6
7
8

하지만 17 버전부터는 사용자가 기대한대로 이벤트 대상의 값을 가져올 수 있어 e.persist() 사용이 더 이상 필요하지 않습니다.

React v17




 



function handleChange(e) {
  setData(data => ({
    ...data,
    text: e.target.value // 이벤트 대상의 값
  }));
}
1
2
3
4
5
6

이벤트 풀링(Pooling)이란?

자주 재사용되는 객체들을 미리 만들어 놓고 활용하는 방법을 말합니다. 하늘에서 내리는 눈송이 객체들을 만들어 놓고 show(), hide() 메서드를 사용해 생성제거을 표현하며 재사용하는 것과 유사합니다. 눈송이가 생성되거나 제거될 때마다, 객체를 생성하고 파괴하는 것에 리소스 사용량이 더 크므로, 풀링 방법을 사용하는 것이 보다 효과적이기 때문입니다.

# 이펙트 클린업 타이밍

useEffect() 훅의 클린업 타이밍(Cleanup Timing)을 보다 일관되게 작동되도록 변경되었습니다.

useEffect(() => {
  // ...
  return () => {
    // 클린업 타이밍 
    // 클래스 컴포넌트의 componentWillUnmount 라이프 사이클 훅과 유사
  }
})
1
2
3
4
5
6
7

useEffect()는 화면 업데이트를 지연시킬 필요가 거의 없어, 화면이 업데이트 된 직후 비동기적으로 이펙트가 실행되도록 변경되었습니다. 다만, 화면 업데이트를 지연시킬 필요가 있는 경우(예: 툴팁 위치 측정)는 useLayoutEffect()를 사용해 화면 업데이트를 지연시킬 수 있습니다.

이전 16버전까지 useEffect() 클린업(cleanup)은 동기적으로 실행 처리되었습니다. 하지만 동기식 처리가 큰 화면 업데이트 시 성능 저하를 유발함을 확인했습니다. 17버전 부터는 컴포넌트가 언마운트(unmount) 되어 화면이 업데이트 된 후 클린업이 실행됩니다. 그리고 이전 버전과 달리 클린업 함수가 DOM 트리의 순서와 같은 순서로 실행되는 것을 보장합니다.

# 잠재적인 문제

useEffect() 내부에서 ref를 아래와 같이 사용하고 있는 코드가 있는 경우에는 문제가 발생됩니다.

useEffect(() => {
  someRef.current.someSetupMethod();
  return () => {
    someRef.current.someCleanupMethod();
  };
});
1
2
3
4
5
6

이러한 문제를 해결하려면 다음과 같이 ref를 참조하여 클린업 코드에서 사용해야 합니다.

useEffect(() => {
  const _ref = someRef.current;
  _ref.someSetupMethod();
  return () => {
    _ref.someCleanupMethod();
  };
});
1
2
3
4
5
6
7

이와 같은 방법을 사용해야 하는 이유는 useEffect() 클린업 함수 실행이 비동기적으로 바뀌면서 수정 가능한(mutable) ref.current 특성상 실행되는 타이밍에 null 일 수도 있기 때문입니다. 그러므로 수정 가능한 값들을 useEffect() 내부에서 캐시해 사용해야 하는 것입니다.

# undefined 반환에 대한 일관된 오류 처리

이전 버전까지 모든 컴포넌트가 undefined를 반환할 경우 모두 오류를 출력했습니다.

const Button = () => {
  return; // 오류: 렌더링에서 반환 된 항목이 없습니다.
}
1
2
3

이러한 조치는 의도치 않은 실수를 방지하기 위함이였습니다. 다음과 같이 코딩 실수를 범해 undefined를 반환하는 경우도 있기 때문입니다.

function Button() {
  // 코딩 실수로 return을 쓰는 것을 잊어서 컴포넌트는 undefined를 반환합니다.
  // React는 이것을 무시하는 대신 오류로 표시합니다.
  <button />;
}
1
2
3
4
5

하지만 forwardRef 또는 memo 컴포넌트를 사용할 경우 React는 이를 오류로 처리 하지 않았습니다.

let Button = forwardRef(() => {
  // 코딩 실수로 return을 쓰는 것을 잊어서 컴포넌트는 undefined를 반환합니다.
  // React 16은 오류를 출력하지 않습니다.
  <button />;
});

let Button = memo(() => {
  // 코딩 실수로 return을 쓰는 것을 잊어서 컴포넌트는 undefined를 반환합니다.
  // React 16은 오류를 출력하지 않습니다.
  <button />;
});
1
2
3
4
5
6
7
8
9
10
11

React 17부터는 forwardRefmemo 컴포넌트에서도 함수 및 클래스 컴포넌트와 동일하게 undefined를 반환하는 경우 오류를 출력합니다.

let Button = forwardRef(() => {
  // 코딩 실수로 return을 쓰는 것을 잊어서 컴포넌트는 undefined를 반환합니다.
  // React 17은 오류를 출력합니다.
  <button />;
});

let Button = memo(() => {
  // 코딩 실수로 return을 쓰는 것을 잊어서 컴포넌트는 undefined를 반환합니다.
  // React 17은 오류를 출력합니다.
  <button />;
});
1
2
3
4
5
6
7
8
9
10
11

의도적으로 아무 것도 렌더링 하고 싶지 않다면? null을 반환합니다.

# 네이티브 컴포넌트 오류 스택

브라우저에서 오류가 발생하면 브라우저는 JavaScript 함수 이름과 해당 위치가 포함된 스택 추적을 제공합니다. 하지만 React 앱 개발 시엔느 React 트리 구조가 더 중요하므로 JavaScript 스택은 종종 문제를 진단하는데 방해가 되기도 합니다. 알고 싶은 건 <Button/> 컴포넌트에서 발생된 오류의 원인이니까요.

이러한 문제 해결을 위해 16버전에서 오류가 발생했을 때 컴포넌트 스택을 출력할 수 있도록 하였으나, 소스 코드에서 컴포넌트의 위치를 몰라 Console에서 클릭하여 문제가 발생한 컴포넌트로 이동할 수 없어 거의 쓸모가 없었습니다. 하지만 17 버전부터는 컴포넌트 스택에 추적 기능을 추가하였습니다. 즉, 문제가 되는 컴포넌트로 바로 이동할 수 있는 방법이 제공되어 보다 효과적으로 컴포넌트 스택을 활용할 수 있게 되었습니다.