React 18 주요 업데이트

전해림·2024년 10월 21일
1
post-thumbnail

필요 조건

React 18의 새로운 기능을 사용하기 위해서는 createRoot API를 사용해야 한다.

//이전 버전
import React fron 'react';
import ReactDOM from 'react-dom';

ReactDOM.render(
	<React.StrictMode>
		<App/>
	</React.StrictMode>
	document.getElementById('root')
);
//리액트18 버전
import React fron 'react';
import ReactDOM from 'react-dom/client';

const rootNode = document.getElementById('root')

ReactDOM.createRoot(rootNode).render(
	<React.strictMode>
		<App/>
	</React.strictMode>
);

기본 버전에서는 render만을 사용해 root를 생성했는데 React 18버전에서는 createRoot라는 새로운 Root API를 통해 root를 생성할 수 있게 되었다. createRoot API를 사용해 React 18버전의 새로운 기능과 API를 사용할 수 있게 된다.

ReactDOM을 불러오는 경로 또한 react-dom에서 react-dom/client로 변경된 것을 볼 수 있다.

관련 기술

React 18 새로운 기능 키워드

  • Automatic Batching
  • Concurrent Feature
  • New Hooks

1. Automactic Batching

여러 상태 업데이트(setState)를 통합하여 단일 리렌더링으로 처리해 리렌더링의 성능을 개선한 기능이다.
React 18이전엔 useState, setState를 통해 상태를 업데이트하고 업데이트된 결과를 바탕으로 리렌더링을 진행했다. 이때 , state의 개수가 많아지면 그만큼 리렌더링도 많이 발생하기 때문에 불필요한 리렌더링이 많아지고 성능 이슈가 발생할 수 있었다. 그래서 도입된 기능이 배치처리인데 배치처리는 여러 상태 업데이트(setState)가 발생해도 상태를 하나로 통합해 리렌더링을 진행하는 방식이다. React 18이전 버전에서는 이벤트 핸들러와 생명주기 메소드를 사용할 댄 배치처리를 지원하지만 콜백 함수가 포함된 경우 배치 처리를 지원하지 않는다.

	//react 17 (리렌더링 두번 유발)
	function handleClick() {
		fetchSomething().then(() => {
			// React 17 이전의 버전에서는 해당 작업을 Batching 처리하지 않는다.
			// 왜냐하면 해당 작업은 이벤트가 종료된 이후 (100ms 뒤) 에 실행되기 때문이다.
			setCount((c) => c + 1); // 리렌더링 유발
			setFlag((f) => !f); // 리렌더링 유발
		});
	}
//react 18 (리렌더링 한번으로 배칭)
function handleClick() {
		fetchSomething().then(() => {
			// React 18 이후에서는 해당 작업을 Batching 처리 한다.
			setCount((c) => c + 1);
			setFlag((f) => !f);
			// React 는 해당 작업을 일괄 처리하여 한 번의 리렌더링만 진행한다.
		});
	}

각각의 batching

const changeState = async () => {
    if (testRef.current) {
      clearTimeout(testRef.current);
      testRef.current = null;
    }

      // 요 안에서 한 개의 update function 은 하나로 batching 됨.
      trigger();
      trigger();

      // 1.5초 후에는 4개의 update function 을 하나로 batching 시킴.
      setTimeout(() => {
        trigger();
        trigger();
      }, 1500);
      setTimeout(() => {
        trigger();
        trigger();
      }, 1500);

      testRef.current = setTimeout(() => {
        // 1초 후에는 두 개의 update function 을 하나로 batching 시킴
        trigger();
        trigger();
      }, 1000);
      return;
  }

changeState 함수는 onClick 이벤트 핸들러를 통해 호출되며, 버튼 클릭 시 두 차례에 걸쳐 리렌더링이 발생한다. 이유는 이벤트 핸들러 내에 위치한 state update 작업은 하나의 queue에 묶여 batching처리되고,
이후 timeout이 끝난 후 실행되는 두 개의 state update 작업은 따로 queue에 묶여 리렌더링을 유발시키기
때문이다. 중요한 것은 위 작업이 하나의 queue에 batching 되지 않는다는 점이다. 이벤트 핸들러에서 실행된 setState와 setTimeout을 기반으로 실행된 setState는 서로 batching 되지 않고 독립적으로 실행된다.
한 가지 신기한 점은 이벤트 핸들러에서 실행되었고 딜레이가 같은 두 개의 setTimeut 내 update funcion도 하나로 batching 되었다는 점이다.

ReactDOM.flushSync()

react-dom 라이브러리에 추가된 ReactDOM.flushSync() 메서드는 Auto Batching을 무시하고 즉시 DOM을 렌더링해준다. React에서는 공식적으로 해당 메서드의 사용을 추천하진 않으며 필요한 상황이 있을 경우에만 사용할 것을 강조했다.

import { flushSync } from "react-dom";

function handleClick() {
	// React 는 flushSync 메서드가 실행되는 즉시 DOM을 업데이트 한다.
	flushSync(() => {
		setCounter((c) => c + 1);
	});
	// React 는 flushSync 메서드가 실행되는 즉시 DOM을 업데이트 한다.
	flushSync(() => {
		setFlag((f) => !f);
	});
	// 따라서 해당 함수가 실행될 경우 React는 총 두 번의 리렌더링을 수행한다.
}

2. Concurrent Feature

Concurrent Feature을 알아보기 전, Concurrent Mode라는 개념에 대해 먼저 알아야한다. Concurrent Mode는 자바스크립트가 싱글 스레드 기반의 언어이기 때문에 여러 작업을 동시에 처리할 수 없는 문제점을 보완하기 위해 나왔다.

React 또한 JS 기반이기 때문에 동일한 문제점이 발생했고 Concurrent Mode(동시성)을 통해 이를 해결하고자 했다. Concurrent Mode는 여러 작업을 작은 단위로 나눠 우선순위를 정해 그에 따라 작업을 번갈아 수행하는 것을 말한다. 동시에 수행되는 것은 아니지만 작업 간 전환이 빠르기 때문에 동시에 수행되는 것처럼 보여 사용자 경험을 UX적으로 향상시킨다.

이러한 배경에서 탄생한 Concurrent Feature은 Concurrent Mode에서 용어가 바뀐 것으로 이번 React 18 업데이트에서 가장 중요한 추가 기능이다. 또한 동시성을 효율적으로 진행할 수 있는 기능인 Suspense와 Transitions를 지원한다. 해당 기능들은 한 번에 여러 동작을 수행해 사용자 경험을 향상시킬 수 있다.

image.png

2-1. Suspense

Suspense는 사용자에게 보여주고 싶은 컴포넌트를 먼저 렌더링 할 수 있게 하는 기능이다.

특징

  • 렌더링 작업과 비동기 데이터 호출 과정이 동시에 이루어진다.
    • 비동기 데이터를 호출하는 과정, fallback UI를 보여주는 과정, 완성된 UI를 보여주는 과정 등 기존의 렌더링 과정들이 여러 작은 태스크들로 쪼개진 뒤 번갈아가며 진행된다.
  • 비동기 데이터 호출을 통해 로딩이 발생하면 Suspense가 이를 포착하여 UI는 fallback으로 보여주고 로딩이 완료되면 완성된 UI를 보여준다.
    • 이를 통해 컴포넌트 내부에선 로딩 상태에 대한 분기 처리가 필요없어져 코드의 가독성이 높아진다.
    • 비동기 데이터에 대한 분기처리로 인해 waterfall 현상 역시 사라진다.

2-2. Transitions

Transition은 setState(상태 업데이트)의 우선순위를 구분한다. Urgent updates는 입력, 클릭, 누르기와 같은 사용자가 직접적인 상호 작용을 하는 기능을 말하고 Transition updates(non-urgent)는 UI 전환과 같은 기능을 말한다.

useTransition(새롭게 나온 리액트 훅)

isPending 상태 값을 가져와 렌더링 결과를 분기 처리하는 기능이다. 여기서 isPending은 state를 업데이트했음에도 리렌더링 하지 않고 UI를 잠시 유지하는 상태를 말한다. isPending은 boolean 값으로 transition 완료를 알려주게 된다.

startTranstion

내부에 존재하는 상태 업데이트는 모두 전환 업데이트일때 우선순위가 높은 상태 업데이트가 발생할 경우 내부에 선언한 상태 업데이트는 중단된다.

const [isPending, startTransition] = useTransition();
const [one, setOne] = useState<number>(0);
const [two, setTwo] = useState<number>(0);

const onClick = () => {
	setOne(one + 1);// 긴급 업데이트// startTransition으로 전환 업데이트 선언// 내부에 있는 상태 업데이트는 모두 전환 업데이트
	startTransition(() => {
		  setTwo(two + 2);// 전환 업데이트로 setOne보다 우선순위가 낮음
	});
};

return (
	<>
		<button onClick={onClick} />
		<button disabled={isPending} />
	<>
);

isPending의 값이 true일 경우 startTransition 내부에 있는 setTow가 우선순위에서 밀리게 된다. setOne의 상태 업데이트 진행시 isPending의 값은 ture이다. setOne의 상태 업데이트가 완료되면 isPending의 값이 false가 되며 setTwo의 상태 업데이트를 시작한다.

3. New Hooks

useId()

서버와 클라이언트에서 사용될 고유 ID를 생성하며 key를 생성하는 것은 아니다.

  • id를 하나만 전달
function CreateID() {
  const id = useId();

  return <input id={id} type="text" />;
};
  • id를 두 개 이상 전달할 때는 접미사를 추가해 아이디 구분
function CreateID() {
  const id = useId();

  return (
		<input id={id + '-firstID'} type="text" />
		<input id={id + '-secondID'} type="text" />
	);
};

useDeferredValue()

트리에서 긴급하지 않은 부분의 리렌더링을 연기하는 기능을 제공한다. 고정된 지연 시간이 없고 긴급한 작업이 완료된 후 즉시 실행하며 사용자 입력을 차단하지 않는다.

import { useDeferredValue, useState } from "react";

const SlowUI = () => (
  <>
    {Array(50000)
      .fill(1)
      .map((_, index) => (
        <span key={index}>{100000} </span>
      ))}
  </>
);

function App() {
  const [value, setValue] = useState<string>("");
  const deferredValue = useDeferredValue(value);// 긴급하지 않은 값으로 지정

  const handleClick = (e: any) => {
    setValue(e.target.value);
  };

  return (
    <>
      <input onChange={handleClick} />
      <div>DeferredValue: {deferredValue}</div>
      <div>
        <SlowUI />
      </div>
    </>
  );
}

export default App;

value가 변경되더라도 deferredValue에는 변경된 값이 아닌 이전 값을 리턴한다. 사용자의 인터랙션이 종료되면 그때 새로운 값을 리턴하게 된다.

Ref.

https://tech.nexr.kr/e14cd3fa-58be-4983-9e54-a1d157af08c2

https://velog.io/@seungchan__y/React-18-Concurrent-맛보기

https://velog.io/@rookieand/React-18에서-추가된-Auto-Batching-은-무엇인가#️-batching-이란

profile
프론트엔드 개발자 전해림입니다

0개의 댓글