홍승아블로그

react17

v17

설치

npm install react@17.0.0 react-dom@17.0.0

yarn add react@17.0.0 react-dom@17.0.0

// CDN을 통해 React의 UMD 빌드를 제공
<script crossorigin src="https://unpkg.com/react@17.0.0/umd/react.production.min.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@17.0.0/umd/react-dom.production.min.js"></script>

No more ‘import React …’

JSX 를 사용하기 위해선 늘 React 를 import 해야 했다(그것을 명시적으로 사용하지 않더라도).

JSX 코드는 babel, typescript 등을 통해 일반 javascript transpile 되었기 때문이다.

  • <= v16

    import React from 'react';
    
    function App() {
      return <h1>Hello World</h1>;
    }
    
    // JSX -> 일반 자바스크립트
    function App() {
      return React.createElement('h1', null, 'Hello world');
    }

v17 버전 이후부터는 더 이상 ‘import React ~’ 구문을 직접 입력하지 않아도 된다.

  • v17

    function App() {
      return <h1>Hello World</h1>;
    }
    
    // Inserted by a compiler (don't import it yourself!)
    import { jsx as _jsx } from 'react/jsx-runtime';
    
    function App() {
      return _jsx('h1', { children: 'Hello world' });
    }

새 JSX 변환을 위해서 업그레이드 방법

CRA: v4.0.0 +

Next: v9.5.3 +

Gastby: v2.24.5 +

typescript: v4.1

ESLint

  • eslint-plugin-react
    {
      // ...
      "rules": {
        // ...
        "react/jsx-uses-react": "off",
        "react/react-in-jsx-scope": "off"
      }
    }

custom babel

  • @babel/plugin-transform-react-jsx

    # for npm users
    npm update @babel/core @babel/plugin-transform-react-jsx
    
    # for yarn users
    yarn upgrade @babel/core @babel/plugin-transform-react-jsx
  • @babel/preset-react

    # for npm users
    npm update @babel/core @babel/preset-react
    
    # for yarn users
    yarn upgrade @babel/core @babel/preset-react
  • babel config

    // If you are using @babel/preset-react
    {
      "presets": [
        ["@babel/preset-react", {
          "runtime": "automatic"
        }]
      ]
    }
    
    // If you're using @babel/plugin-transform-react-jsx
    {
      "plugins": [
        ["@babel/plugin-transform-react-jsx", {
          "runtime": "automatic"
        }]
      ]
    }

v17 버전 업한 이후에 불필요한 import 일괄삭제(선택)

  • react-codemod

    // general react
    import React from 'react';
    
    function App() {
      return <h1>Hello World</h1>;
    }
    
    --transpile;
    function App() {
      return <h1>Hello World</h1>;
    }
    // custom hook
    import React from 'react';
    
    function App() {
      const [text, setText] = React.useState('Hello World');
      return <h1>{text}</h1>;
    }
    
    import { useState } from 'react';
    
    function App() {
      const [text, setText] = useState('Hello World');
      return <h1>{text}</h1>;
    }

참고페이지: https://reactjs.org/blog/2020/09/22/introducing-the-new-jsx-transform.html

undefined를 return 할 경우 일관되게 에러 발생

16버전 이하에서는, 모든 컴포넌트에서 undefined를 리턴할 경우 항상 에러를 발생했다.

하지만 코딩 실수로 인해, forwardRef memo컴포넌트 에서는 이러한 에러 처리가 누락되어 있었는데, 이제 부터 에러처리가 추가되었다.

17버전부터는 forwardRef memo컴포넌트에서도 undefined를 리턴할 경우 에러 발생한다.

<= v16
function Button() {
  return; // Error: Nothing was returned from render
}

function Button() {
  // We forgot to write return, so this component returns undefined.
  // React surfaces this as an error instead of ignoring it.
  <button />;
}

v17 +
let Button = forwardRef(() => {
  // We forgot to write return, so this component returns undefined.
  // React 17 surfaces this as an error instead of ignoring it.
  <button />;
});

let Button = memo(() => {
  // We forgot to write return, so this component returns undefined.
  // React 17 surfaces this as an error instead of ignoring it.
  <button />;
});

의도적으로 렌더링하지 않으려면 null을 반환해야한다.

Effect Cleanup Timing

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

<= v16
useEffect(() => {
  // effect -> asynchronous
  return () => {
    // cleanup -> synchronous
  }
}

v17 +
useEffect(() => {
  // effect -> asynchronous
  return () => {
    // cleanup -> asynchronous
  }
}

cleanup에 많은 동작을 수행해야할 경우, 페이지 전환 같은 동작에서 성능 저하를 유발하게 된다.

(혹시, effect/cleanup 동기적으로 수행해야 할 경우는 useLayoutEffect를 사용)

v17 버전이 적용된 이후에는 컴포넌트가 unmount → 컴포넌트/화면 업데이트 → cleanup 수행

단, unmount → cleanup이 수행되다보니 페이지 전환이 일어난 경우 아래 코드가 문제가 발생할 수있다.

useEffect(() => {
  someRef.current.someSetupMethod()
  return () => {
    someRef.current.someCleanupMethod()
  }
})

useEffect(() => {
  const instance = someRef.current -> closure를 통해서 unmounted 하더라도 gc가 사라지지 않게 처리
  instance.someSetupMethod()
  return () => {
    instance.someCleanupMethod()
  }
})

No Event Pooling

  • 이벤트 풀링 오래된 브라우저에서 이벤트 객체를 사용 시 성능 저하되는 이슈가 있어서 리액트에서는 Synthetic Event pool를 만들어서 관리하고 한다.
    • 사용자가 특정 이벤트가 발생할 경우
      • Synthetic Event pool 에서 syn Synthetic Event 객체의 참조 넘겨줌(→ 객체 생성시간 축소)
      • 이벤트 정보의 Synthetic Event 객체를 넣어줌
      • 사용자가 정의한 이벤트 리스너 수행
      • Synthetic Event 객체 초기화( → null 넣어줌으로써 GC 메모리 가져가도록 처리, 메모리 효율화를 위한 작업)) 해당 기능으로 인해서 인해서 비동기 동작에서 에러가 나는 경우가 생기기 됩니다.
    onChange={
    (e)=>{
      console.log(e.type);
      console.log(e.target.value);
      setTimeout(()=>{
        console.warn(e.type); // 초기화로 인한 null
       })
     }
    }
    해당 이슈를 해결하기 위해서 React에서 persist 사용해서 이벤트풀에서 제거해서 기존 이벤트 방식으로 사용하면 동작에 문제는 없지만, 성능상 이점을 버리게 된다.

v17 부터는 더 이상 구형 브라우저에서 성능 이슈 보완을 위한 동작을 지원하지 않기로 결정함

단, event.persist() 메서드는 그대로 존재하지만, 호출하더라도 아무런 동작을 하지 않음(하위호환을 위한 작업)

이벤트 위임의 변화

먼저 리액트에서 이벤트 핸들러를 붙이는 코드를 살펴보자

<button onClick={handleClick}>

바닐라 DOM에서는 이렇게 작동할 것이다.

myButton.addEventListener('click', handleClick);

리액트에서는 이벤트 핸들러를 실제 선언된 DOM에 붙지 않고 document node에 추가한다.

이런걸 이벤트 위임이라고 한다.

document.addEventListener('click', (e) => { // 실행된 컴포넌트 찾게 됨 });

리액트 이벤트 위임 방식

  • document에서 이벤트가 발생
  • 리액트 이벤트 시스템이 실제로 이벤트가 발생한 컴포넌트 찾음
  • 이벤트 버블링으로 상위 컴포넌트에 이벤트 전달

문제점

여러 리액트 컴포넌트가 중첩으로 수행될 경우 이벤트를 Block 하는 stopPropagation 동작하지 않는 문제점 발생

// legacy react root
import React from 'react'; // 16.12
import ReactDOM from 'react-dom'; //16.12

function modernReact() {
  return ReactDOM.render(
    React.createElement(
      'div',
      {
        onClick: () => console.log('modern react!'),
      },
      null,
    ),
    document.getElementById('modernroot'),
  );
}

import React from 'react'; // 16.8
import ReactDOM from 'react-dom'; // 16.8

function legacyReact() {
  return ReactDOM.render(
    React.createElement(
      'div',
      {
        onClick: () => console.log('legacy react!'),
      },
      null,
    ),
    document.getElementById('legacyroot'),
  );
}

<html>
  <body>
    <div>
      <div id="modernroot">
        {' '}
        // click
        <div id="legacyroot"> // click -> stopPropagation</div>
      </div>
    </div>
  </body>
</html>;
  • React 17 부터는 Event가 document 가 아닌 React Tree Root 로 delegate 된다.

Untitled

여러 리액트 컴포넌트 수행되더라도 기본이 되는 Root Element 이벤트를 처리하기 때문에 구조/이벤트 방식이 독립적으로 수행가능하다.

리액트가 다른 기술과 사용하는 것을 더욱 용이하게 한다. 만약 외부에 jQuery가 존재하고, 내부에는 리액트가 존재한다면 이제 예상대로 이벤트 전파를 jQuery단까지 막을 수 있다.

단, DOM에 **document.addEventListener**를 활용하여 수동으로 이벤트를 붙여서 리액트의 모든 이벤트를 감지하는 코드가 존재할 수 있다. 리액트 16버전 에서는 이러한 코드가 가능했지만, 리액트 17 부터는 전파가 막히게 되므로 **document**에서도 이벤트가 발생하는지 알 수 없기때문에 capturing 방식으로 변경이 필요하다.

// <= v16
document.addEventListener('click', function () {
  // This custom handler will no longer receive clicks
  // from React components that called e.stopPropagation()
});

// v17+
document.addEventListener(
  'click',
  function () {
    // Now this event handler uses the capture phase,
    // so it receives *all* click events below!
  },
  { capture: true },
);

참고페이지

https://reactjs.org/blog/2020/10/20/react-v17.html

https://leo.works/2012130

https://yceffort.kr/2020/09/react-17-release-candidates#리액트-17에서는-점진적으로-업그레이드가-가능하다

이전글
redux-saga
다음글
typescript