모던 리액트 Deep Dive 스터디

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

려낭 2024. 12. 12. 23:29

10.1 리액트 17버전 살펴보기

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

 

리액트 16에서 17로의 업데이트는 기존 16버전에서 더 이상 호환되지 않는 API가 있거나 

새로운 리액트 17을 사용하는 데 있어 이전과 작동 방식이 달라질 수 있기 때문에 단행된 주 버전 업데이트다.

 

새로운 주 버전이 릴리스되면 이전 버전에서의 API제공을 완전히 중단해버리고, 전체 애플리케이션을 새롭게 업그레이드 하기를 요구한다.

 

그러나 이제 리액트 17 버전부터는 점진적인 업그레이드가 가능해진다.

리액트 18로 업데이트 되어도 일부 기능에 대해서는 리액트 17을 사용하는 것이 가능해진다.

 

이러한 점진적인 업그레이드를 지원하기 위한 리액트의 일부 컴포넌트 변경이 리액트 17 업데이트의 주요 변경 사항 중 하나다.

 

 

10.1.2 이벤트 위임 방식의 변경

 

이벤트 위임 방식의 변화를 이해하려면 리액트에서 이벤트가 어떻게 추가되는지를 이해해야 한다.

import { useEffect, useRef } from 'react';

export default function Button() {
  const buttonRef = useRef<HTMLButtonElement | null>(null);

  useEffect(() => {
    if (buttonRef.current) {
      buttonRef.current.onclick = function click() {
        alert('안녕하세요!');
      };
    }
  }, []);

  function 안녕하세요() {
    alert('안녕하세요!');
  }

  return (
    <>
      <button onClick={안녕하세요}>리액트 버튼</button>
      <button ref={buttonRef}>그냥 버튼</button>
    </>
  );
}

 

리액트 버튼은 일반적으로 리액트 애플리케이션에서 DOM에 이벤트를 추가하는 방식으로 onClick 이벤트를 추가했다.

 

그냥 버튼의 이벤트는 직접 DOM을 참조해서 가져온 다음, DOM의 onClick에 직접 함수를 추가하는 고전적인 이벤트 핸들러 추가 방식을 사용했다.

 

두 방법이 실제 웹에서 어떻게 다를까?

 

 

 

'그냥 버튼'은 해당 버튼의 이벤트 리스너에 click으로 추가되어있다. 해당 핸들러를 클릭해보면 

click함수를 가리키고 있다.

 

리액트로 부착한 이벤트는 <button>의 onClick이벤트에 noop이라는 핸들러가 추가되어 있다.

noop은 문자 그대로 (no operation) 아무런 일도 하지 않는다.

그러나 두 버튼 모두 동일하게 작용한다.

 

리액트는 이벤트 핸들러를 해당 이벤트 핸들러를 추가한 각각의 DOM요소에 부탁하는 것이 아니라,

이벤트 타입(click, change)당 하나의 핸들러를 루트에 부착한다.

이를 이벤트 위임 이라고 한다.

 

이벤트 위임을 이해하려면 이벤트가 어떤 단계로 구성돼 있는지 알아야 한다.

 

  1. 캡처(capture): 이벤트 핸들러가 트리 최상단 요소에서 부터 시작해서 실제 이벤트가 발생한 타깃 요소까지 내려가는 것
  2. 타깃(target): 이벤트 핸들러가 타깃 노드에 도달하는 단계. 이 단계에서 이벤트가 호출된다.
  3. 버블링(bubbling): 이벤트가 발생한 요소에서부터 시작해 최상위 요소까지 다시 올라간다.

 

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

 

리액트 16버전까지는 이벤트 위임이 모두 document에서 수행되고 있었다.

그러나 리액트 17부터는 이러한 이벤트 위임이 모두 document가 아닌 리액트 컴포넌트 최상단 트리, 즉 루트 요소로 바뀌었다.

 

이유는 무엇일까?

 

점진적 업그레이드 지원, 다른 바닐라 자바스크립트 또는 jQuery 등이 혼재돼 있는 경우 혼란을 방지하기 위해서다.

 

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

 

JSX는 브라우저가 이해할 수 있는 코드가 아니므로 바벨이나 타입스크립트를 활용해 JSX를 실행하기 위해 일반적인 자바스크립트로 변환하는 과정이 꼭 필요하다.

 

16버전까지는 JSX변환을 사용하기 위해 코드 내에서 React를 사용하는 구문이 없더라도 import React from 'react' 가 필요했고 , 이 코드가 없다면 에러가 발생했다.

 

그러나 리액트 17부터 이러한 import구문 없이도 JSX를 변환할 수 있게 됐다.

10.1.4 주요 변경 사항

 

이벤트 풀링 제거

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

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

 

 

 

10.2 리액트 18 버전 살펴보기

10.2.1 새로 추가된 훅 살펴보기

 

useId

 

컴포넌트 별로 유니크한 값을 생성하는 새로운 훅.

 

useId를 사용하면 클라이언트와 서버에서 불일치를 피하면서 컴포넌트 내부의 고유한 값을 생성할 수 있게 됐다.

 

 

useTransition

 

UI 변경을 가로막지 않고 상태를 업데이트할 수 있는 리액트 훅.

 

상태 업데이트를 긴급하지 않은 것으로 간주해 무거운 렌더링 작업을 조금 미룰 수 있고, 사용자에게 조금 더 나은 사용자 경험을 제공할 수 있다.

 

useDeferredValue

 

리액트 컴포넌트 트리에서 리렌더링이 급하지 않은 부분을 지연할 수 잇게 도와주는 훅.

 

특정 시간 동안 발생하는 이벤트를 하나로 인식해 한 번만 실행하게 해주는 디바운스와 비슷하지만 

useDeferredValue만이 가진 장점이 몇 가지 있다.

 

디바운스는 고정된 지연 시간을 필요로 하지만 useDeferredValue는 고정된 지연 시간 없이 첫 번째 렌더링이 완료된 이후에 이 useDeferredValue로 지연된 렌더링을 수행한다.

이 지연된 렌더링은 중단할 수 도 있고, 사용자의 인터랙션을 차단하지도 않는다.

 

useSyncExternalStore

 

일반적인 애플리케이션 코드를 작성할 때는 사용할 일이 별로 없는 훅이다.

 

외부 상태(store)를 React 컴포넌트에서 안전하게 구독(subscribe)하고 렌더링할 수 있도록 도와준다.

주로 전역 상태 관리 라이브러리(Redux, Zustand 등)나 브라우저 API(예: window.navigator.onLine) 같은 React 외부에서 관리되는 데이터를 React와 동기화할 때 사용된다.

 

useInsertionEffect

 

CSS- in-js 라이브러리를 위한 훅이다.

 

리액트 17과 styled-components에서는 클라이언트 렌더링 시에 이러한 작업이 발생하지 않도록 서버 사이드에서 스타일 코드를 삽입했다.

그러나 이 작업을 훅으로 처리하는 것은 지금까지 쉽지 않았는데,

훅에서 이러한 작업을 할 수 있도록 도와주는 새로운 훅이 바로 useInsertionEffect이다.

 

 

 

10.2.2 react-dom/client

 

 

createRoot

 

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

리액트 18의 기능을 사용하고 싶다면 createRoot와 render를 함께 사용해야 한다.

 

hydrateRoot

 

서버 사이드 렌더링 애플리케이션에서 하이드레이션을 하기 위한 새로운 메서드다.
React DOM 서버 API와 함께 사용된다.

 

대부분의 서버 사이드 렌더링은 프레임워크에 의존하고 있을 것이라 사용하는 쪽에서 수정할 일은 거의 없을 코드다.

하지만 자체적으로 서버 사이드 렌더링을 구현해서 사용하고 있다면 이 부분 역시 수정해야 한다.

 

 

10.2.3 react-dom/server

 

추가된 API들을 살펴보자.

 

renderToPipeableStream

 

리액트 컴포넌트를 HTML로 렌더링하는 메서드다.

 

스트림을 지원하는 메서드로, HTML을 점진적으로 렌더링하고 클라이언트에서는 중간에 script를 삽입하는 등의 작업을 할 수 있다.

 

서버는 Suspense를 사용해 빠르게 렌더링이 필요한 부분을 먼저 렌더링할 수 있고, 값비산 연산으로 구성된 부분은 이후에 렌더링되게끔 할 수 있다.

 

renderToReadableStream

 

renderToPipeableStream이 Node.js 환경에서의 렌더링을 위해 사용된다면,

이 메서드는 웹 스트림을 기반으로 작동한다.

 

이는 서버 환경이 아닌 클라우드플레어나 디노 같은 웹 스트림을 사용하는 모던 엣지 런타임 환경에서 사용되는 메서드다.

실제로 웹 애플리케이션을 개발하는 경우에는 이 메서드를 사용할 일이 거의 없을 것이다.

 

10.2.4 자동 배치(Automatic Batching)

 

자동 배치는 리액트가 여러 상태 업데이트를 하나의 리렌더링으로 묶어서 성능을 향상시키는 방법을 의미한다.

 

예를 들어 , 버튼 클릭 한 번에 두 개 이상의 state를 동시에 업데이트 한다면, 자동 배치에서는 이를 하나의 리렌더링으로 묶어서 수행할 수 있다.

 

 

10.2.5 더욱 엄격해진 엄격 모드

 

리액트의 엄격 모드

 

리액트에서 제공하는 컴포넌트 중 하나로, 리액트 애플리케이션에서 발생할 수도 있는 잠재적인 버그를 찾는 데 도움이 되는 컴포넌트다.

 

리액트에서 널리 알려져 있는 Fragment나 Suspense와 마찬가지로 컴포넌트 형태로 선언해서 사용할 수 있다.

 

리액트 엄격 모드에서 하는 작업

  • 더 이상 안전하지 않은 특정 생명주기를 사용하는 컴포넌트에 대한 경고
  • 문자열 ref 사용 금지
  • findDOMNode에 대한 경고 출력
  • 구 ContextAPI 사용 시 발생하는 경고
  • 예상치 못한 부작용(side-effects)검사

리액트 18에서 추가된 엄격 모드

 

컴포넌트가 최초에 마운트될 때 자동으로 모든 컴포넌트를 마운트 해제하고 두번째 마운트에서 이전 상태를 복원한다.

 

10.2.6 Suspense 기능 강화

 

Suspense는 컴포넌트를 동적으로 가져올 수 있게 도와주는 기능이다.

 

18 이전의 Suspense에는 몇 가지 문제점이 있었다.

  • 컴포넌트가 아직 보이기도 전에 useEffect가 실행되는 문제.
  • 서버에서 사용할 수 없다.

 

리액트 18에서 변경된 내용

  • 마운트 직전임에도 effect가 빠르게 실행되는 문제 수정. 이제 컴포넌트가 실제로 화면에 노출될 때 effect가 실행된다.
  • Suspense로 인해 컴포넌트가 보이거나 사라질 때도 effect가 정상적으로 실행된다.
  • Suspense를 이제 서버에서도 실행할 수 있다.
  • Suspense 내에 스로틀링이 추가됐다.