10장 리액트 17과 18의 변경 사항 살펴보기

강석우's avatar
Apr 08, 2024
10장 리액트 17과 18의 변경 사항 살펴보기

10.1 리액트 17 버전 살펴보기

10.1.1 리액트의 점진적인 업그레이드

리액트 17 버전부터는 점진적인 업그레이드가 가능하다.
다시말해 리액트를 현재 프로젝트에서 18로 업데이트 하더라도 일부 트리와 컴포넌트에 대해서만 리액트 18을 선택하는 점진적인 버전 업이 가능해진다는 것이다.
하지만 이는 리액트에서 권장하는 방법이 아니며 한번에 업데이트하는 게 복잡성 감소 측면에서 좋다고 언급했다.

10.1.2 이벤트 위임 방식의 변경

  • 일반 버튼의 이벤트 추가 방식

    • 직접 DOM을 참조해서 가져온 뒤, DOM의 onclick 이벤트를 추가했다.

  • 리액트 버튼의 이벤트 추가 방식

    • 리액트 어플리케이션에서 DOM에 이벤트를 추가하는 방식

    • 이벤트 핸들러를 해당 이벤트 핸들러를 추가한 각각의 DOM요소에 부탁하는 것이 아니라, 이벤트 타입(click, change)당 하나의 핸들러를 루트에 부착한다.

이벤트 구성 단계

  1. 캡처

    1. 이벤트 핸들러가 트리 최상단 요소에서 부터 시작해서 실제 이벤트가 발생한 타깃 요소까지 내려가는 것을 의미한다.

  2. 타깃

    1. 이벤트 핸들러가 타깃 노드에 도달하는 단계다. 이 단계에서 이벤트가 호출된다.

  3. 버블링

    1. 이벤트가 발생한 요소에서 부터 시작해 최상위 요소까지 다시 올라간다.

이벤트 위임
이벤트 단계의 원리를 활용해 이벤트를 상위 컴포넌트에만 붙이는 것을 의미한다.

리액트 16 단계의 이벤트 위임
이벤트 위임이 document에서 수행된다.

리액트 17 단계의 이벤트 위임
이벤트 위임이 document에서 리액트 컴포넌트 최상단 트리, 루트요소로 바뀌었다.

왜 변경되었을까?

점진적인 업그레이드 지원, 그리고 다른 바닐라 자바스크립트 코드 또는 jQuery등이 혼재돼 있는 경우 혼란을 방지하기 위해서다.
만일 여러 리액트 버전이 한 서비스에서 공존한다고 가정해보자.
react-16-14 와 react-16-8 서비스가 공존할 경우 e.stopPropagation()을 실행할 경우
14에서만 적용시키려고 한다고 해도 Document까지 해당 실행문이 올라가기 때문에 8과 14 모두에게 적용이 된다.
따라서 아래와 같은 구조로 변경되게 되었다.

10.1.3 import React from ‘react’가 더 이상 필요 없다: 새로운 JSX transform

작성 코드

const Component = (
 <div>
   <span>hello world</span>
 </div>
)

구 버전의 코드
React.createElement 를 수행할 때 필요한 import React from ‘react’까지 추가해줘야한다.

  var Component = React.createElement(
    "div",
    null,
    React.createElement("span", null, "hello world")
  );

17 버전의 코드
JSX를 변환할 때 필요한 모듈인 react/jsx-runtime을 불러오는 require 구문도 같이 추가되기 때문에 react를 작성해줄 필요가 없다.

  var _jsxRuntime = require('react/jsx-runtime')

  var Component = (0,_jsxRumtime.jsx)('div',{
    children:(0,_jsxRuntime.jsx)('span',{
      children:"hello world"
    })
  })

10.1.4 그 밖의 주요 변경 사항

이벤트 풀링 제거

리액트 16의 이벤트 풀링 개념이 삭제 되었다.

useEffect 클린업 함수의 비동기 실행

리액트 16 버전까지 useEffect 의 클린업 함수가 동기적으로 처리되었다.
이는 클린업 함수가 완료되기 전까지 다른 작업을 방해하기 때문에 불필요한 성능 저하로 이어지는 문제가 존재했다.
리액트 17버전부터는 화면이 완전히 업데이트된 이후에 클린업 함수가 비동기적으로 실행된다.

컴포넌트의 undefined 반환에 대한 일관적인 처리

컴포넌트 내부에서 undefined를 반환하면 오류가 발생해야 정상이지만 리액트 16에서는 useMemo 또는 forwardRef를 사용할 경우 에러가 발생하지 않는 문제가 있었다.
17부터는 정상적으로 에러가 발생한다.

10.2 리액트 18 버전 살펴보기

10.2.1 새로 추가된 훅 살펴보기

useId

컴포넌트별로 유니크한 값을 생성하는 새로운 훅이다.
같은 컴포넌트여도 서로 인스턴스가 다르면 다른 랜덤한 값을 만들어 내며
서버사이드와 클라이언트 간에 동일한 값이 생성되어 하이드레이션 이슈도 발생하지 않는다.
Math.random() 값을 서버사이드 렌더링 시 사용하게 된다면 하이드레이션 이슈가 발생한다.

const id = useId();

useTransition

UI 변경을 가로막지 않고 상태를 업데이트할 수 있는 리액트 훅이다.
이 훅은 상태 업데이트를 긴급하지 않은 것으로 간주해 무거운 렌더링 작업을 조금 미룰 수 있으며, 사용자에게 조금 더 나은 사용자 경험을 제공할 수 있다.
렌더링이 오래걸리는 컴포넌트의 렌더링을 동기적으로 기다리지 않고 비동기적으로 렌더링 할 수 있게 해주는 훅이다.

사용법

useTransition은 매개변수를 받지 않는다.
useTransition은 정확히 두 개의 항목이 있는 배열을 반환하는데
- 보류 중인 트랜지션이 있는지 여부를 알려주는 isPending 플래그
- state 업데이트를 트랜지션으로 표시할 수 있는 startTransition 함수

const [isPending, startTransition] = useTransition();

useDeferredValue

리액트 컴포넌트 트리에서 리렌더링이 급하지 않은 부분을 지연할 수 있게 도와주는 훅이다.
useTransition과의 차이점은 감싸는 부분이 useTransition 은 state값을 업데이트하는 함수이고 useDeferredValue 는 state값 자체만을 감싸서 사용한다는 것이다.
방식에서만 차이가 있을 뿐 상황에 맞는 방법을 선택하여 사용하면 된다.

const deferredText = useDeferredValue(text);

useSyncExternalStore

useSubsription이 리액트 18버전에서 useSuncExternalStore로 대체되었다.
리액트 17에서는 일어날 여지가 없었던 tearing 현상 (하나의 state값이 서로 다른 값으로 렌더링되는 현상) 이 18버전에서 useTransition, useDeferredValue의 훅처럼 렌더링을 일시 중지하거나 뒤로 미루는 등의 최적화가 가능해지며 동시성 이슈가 발생할 수 있게 되었고 리액트 밖의 값에 대해서 (innerWidth 같은 경우) 해당 현상에 대한 이슈 해결을 위해 등장하게 된 훅이다.

구성

  • 첫번째 인수는 subscribe로, 콜백 함수를 받아 스토어에 등록하는 용도로 사용된다. 스토어의 값이 변경되면 이 콜백이 호출되어야 한다.

  • 두번째 인수는 컴포넌트에 필요한 현재 스토어의 데이터를 반환하는 함수다.
    스토어가 변경되지 않았다면 매번 함수를 호출할 때마다 동일한 값을 반환해야 한다.

  • 마지막인수는 옵셔널 값으로 서버 사이드 렌더링 시에 내부 리액트를 하이드레이션하는 도중에만 사용된다. 서버사이드에서 렌더링하는 훅이라면 반드시 이 값을 넘겨줘야 하며, 클라이언트의 값과 불일치가 발생할 경우 오류가 발생한다.

useInsertionEffect

기본적인 훅 구조는 useEffect와 동일하다.
다른 차이점은 실행 시점인데, useInsertionEffect는 DOM이 실제로 변경되기 전에 동기적으로 실행된다.
이러한 차이는 브라우저가 다시 스타일을 입혀서 DOM을 재계산하지 않아도 된다는 점에서 매우 크다.
하지만 라이브러리를 작성하는 경우가 아니라면 참고만 하고 실제 애플리케이션 코드에는 가급적 사용하지 말도록 하자

10.2.2 react-dom/client

createRoot

기존의 react-dom에 있던 render 메서드를 대체할 새로운 메서드다.

const root = ReactDOM.createRoot(container)
root.render(<App/>)

hydrateRoot

서버 사이드 렌더링 애플리케이션에서 하이드레이션을 하기 위한 새로운 메서드다.

const root = ReactDOM.hydrateRoot(container)

서버 사이드 렌더링을 자체적으로 구현해서 사용할 경우 이 부분을 수정해야 한다.

10.2.4 자동배치(Automatic Batching)

automatic batching 은 여러개의 상태변화를 하나의 리렌더링으로 묶어서 성능을 향상시키는 방법을 말한다.
리액트 17버전에서는 settimeout , promise 같은 비동기 이벤트에서는 자동배치가 이뤄지고 있지 않았기 때문에 해당 부분에서 자동배치가 일어나지 않았지만
리액트 18버전에서 부터는 루트 컴포넌트를 createRoot를 사용해서 만들며 모든 업데이트가 배치 작업으로 최적화 할 수 있게 되었다.

10.2.5 더욱 엄격해진 엄격 모드

리액트의 엄격모드

리액트 엄격모드는 리액트에서 제공하는 컴포넌트 중 하나로 개발을 하는 동안 잠재적인 버그를 찾아내는데 도움이 되는 컴포넌트다.
해당 모드는 개발모드에서만 작동하고 프로덕션 모드에서는 작동하지 않는다.

  • 더 이상 안전하지 않은 특정 생명주기를 사용하는 컴포넌트에 대한 경고

  • 문자열 ref 사용 금지

  • findDOMNode 에 대한 경고 출력

    • findDOMNode 는 클래스 컴포넌트 인스턴스에서 DOM요소에 대한 참조를 가져올 수 있는 현재는 사용하지 않는 것을 권장하는 메서드다.

  • 구 Context API 사용 시 발생하는 경고

    • childContextTypes 와 getChildContext를 사용하는 구 contextApi 를 사용하면 에러가 발생한다.

  • 예상치 못한 부작용 검사

    • 리액트 엄격 모드 내부에서는 아래의 내용을 의도적으로 이중으로 호출한다.
      이는 리액트 함수가 항상 순수한 결과물을 내고 있는지 개발자에게 알려주기 위함이다.

      • 클래스 컴포넌트의 constructor, render, shouldComponentUpdate, getDerivedStateFromProps

      • 클래스 컴포넌트의 setState의 첫번째 인수

      • 함수 컴포넌트의 body

      • useState, useMemo, useReducer 에 전달되는 함수

10.2.6 Suspense 기능 강화

suspense는 컴포넌트를 동적으로 가져올 수 있게 도와주는 기능이다.
인수로 두 개를 받게 되는데 하나는 fallback prop 으로 지연시켜 불러온 컴포넌트가 아직 불러와지지 않았을 때 보여주는 fallback을 나타낸다.
그리고 children 으로는 React.lazy로 선언한 지연 컴포넌트를 받는다.
즉 지연 컴포넌트를 로딩하기 전에 fallback prop을 보여주고 로딩이후에는 지연 컴포넌트를 보여주는 것이다.
리액트 18버전에서는 Next에서도 해당 suspense 기능을 사용할 수 있게 공식적으로 지원이 되고 마운트 되기 이전에 effect가 실행되는 버그가 수정되었다 .

Share article

석우의 개발블로그