posts
FE
08-react
React 18 버전 변경사항

React 18 버전 변경사항

1. Automatic Batching (자동 배치)

배치(Batch) : React가 더 나은 성능을 위해 여러 개의 상태 업데이트를 한 번의 리렌더링(re-render)으로 묶는 작업

이전 버전까지는 React 이벤트 핸들러 내에서만 배치 작업이 수행되었다.

function App() {
    const [count, setCount] = useState(0);
    const [flag, setFlag] = useState(false);
 
    const handleClick = () => {
        setCount((c) => c + 1);
        setFlag((f) => !f);
        // React는 마지막 함수가 실행되고 난 이후 리렌더링을 하게된다. (Batching)
    }
 
    return(
        <div>
          <button onClick={handleClick}>Next</button>
          <p>{count}</p>
        </div>
    )
}

React 18 버전부터는 Promise나 setTimeout, Native Event Handler 등의 작업들에 대해서도 자동으로 Batching 작업을 수행하게 되며, 이를 Automatic Batching(자동 배칭)이라고 한다.

// setTimeout
 
setTimeout(() => {
    setCount((c) => c + 1);
    setFlag((f) => !f);
}, 2000);
 
// Native Event Handler
 
window.addEventListener("click", () => {
    setCount((c) => c + 1);
    setFlag((f) => !f);
});
 

자동 배치를 사용하기 위해서는 ReactDOM.createRoot 함수를 사용해야 한다.

import { createRoot } from 'react-dom/client';
import App from './App';
 
const root = createRoot(document.getElementById('root'));
root.render(<App />);

또한, 상태 업데이트에 자동 배치가 적용되지 않은 경우 ReactDOM.flushSync함수를 사용하여 적용이 가능하다.

import { flushSync } from 'react-dom';
...
 
function handleClick() {
    flushSync(() => {
        setTest(true);
    });
 
    flushSync(() => {
        setCount((c) => c + 1);
    })
}
...

Transition (전환)

긴급한 업데이트와 그렇지 않은 업데이트를 명시적으로 구분하는 개념이다.

React 18 이전까지는 모든 상태의 업데이트가 동일한 우선순위를 가지고 있었다.

그렇기 때문에 Debounce / Throttle 등의 기법을 활용하여 간접적으로 우선순위를 구분하였다.

React 18부터는 startTransitionAPI를 통해 전환 업데이트를 명시적으로 구분한다.

import React, { useState, useTransition, Suspense } from 'react';
import { testApi } from './api';
 
function MyComponent() {
  const [data, setData] = useState(null);
  const [isPending, startTransition] = useTransition({
    timeoutMs: 3000, 
  });
 
  const fetchData = async () => {
    const receivedData = await testApi();
    setData(receivedData);
  };
 
  const handleFetchData = () => {
    startTransition(() => {
      fetchData();
    });
  };
 
  return (
    <div>
      {isPending ? (
        <div>Loading...</div>
      ) : (
        <Suspense fallback={<div>Loading...</div>}>
          <button
            onClick={handleFetchData}
            disabled={isPending}
          >
            {isPending ? 'Fetching...' : 'Fetch Data'}
          </button>
          {data && (
            <div>
              <h2>Data:</h2>
              <p>{data}</p>
            </div>
          )}
        </Suspense>
      )}
    </div>
  );
}
 
export default MyComponent;

3. 새로운 Hook들

React 18버전에서 새롭게 등장한 Hook들은 다음과 같다.

useId()

const id = useId();

useTransition()

...
const [isPending, startTransition] = useTransition(() => {
  timeoutMs: 3000
}) 
 
const handleLazyClick = () => {
  startTransition(() => {
    setFlag((f) => !f);
  })
}
...

useDeferredValue()

const [data, setData] = useState('');
const deferredData = useDeferredValue(data); 

useSyncExternalStore()

import { useSyncExternalStore } from 'react';
 
let nextId = 0;
let todos = [{id: nextId++, text: 'Todo #1}];
let listeners = [];
 
const todoStore = {
  addTodo() {
    todos = [...todos, {id: nextId++, text: 'Todo #' + nextId }]
    emitChange();
  },
  subscribe(listener){
    listeners = [...listeners, listener];
    return () => {
      listeners = listeners.filter(l => l !== listener);
    }
  },
  getSnapshot() {
    return todos;
  }
}
 
function emitChange() {
  for(let listener of listeners) {
    listener();
  }
}
 
function TodoApp() {
  const todos = useSyncExternalStore(todoStore.subscribe, todosStore.getSnapshot);
 
  return(
    <div> 
      <button onClick={() => todoStore.addTodo()}>Add</button>
      <hr />
      {todos.map(todo => (
        <li key={todo.id}>{todo.text}</li>
      ))}
    </div>
  )
 
} 
 
export default TodoApp;
 

useInsertionEffect()

 
let isInserted = new Set();
 
function useCSS(rule) {
  useInsertionEffect(() => {
    if(!isInserted.has(rule)){
      isInserted.add(rule);
      document.head.appendChild(getStyleForRule(rule));
    }
  });
  return rule;
}
 
function Button() {
  const className = useCSS('...');
  return <div className={className} />
}