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} />
}