새로운 React, React 18
2021년 6월 8일 리액트 팀에서 리액트 18 버전에 관한 사항들을 발표한 이후 7개월의 시간이 흘렀다.
Alpha와 Beta 버전을 거쳐 현재 RC 단계로, 한달 내에 온전한 React 18 버전을 사용할 수 있을 것으로 기대된다.
최초 알파 버전에서 소개된 내용과 현재 RC 버전에 소개된 내용에서 큰 차이는 없으며, 대부분의 기능이 확정되었다고 보아도 무방하다.
리액트 18의 개선사항들은 아래와 같다.
- 즉시 사용 가능한 개선 (out-of-the-box improvements)
- automatic batching (fewer renders)
- SSR support for Suspense
- Fixes for Suspense behavior quirks
- 동시성 특성 (Concurrent features)
- startTransition
- useDeferredValue
- SuspenseList
- Streaming SSR with selective hydration
- 추후 변경될 예정
- Server Components
개선 사항 중 가장 기대되는 기능 중 하나로, 렌더링 최적화와 관련된 automatic batching과 관련된 내용을 번역 및 이해해보고자 한다.
추후 나머지 기능들에 대해서 원문서를 읽으며 다루어보고자 한다.
Automatic Batching
(원글: https://github.com/reactwg/react-18/discussions/21)
Batching
automatic batching에 들어가기 앞서 batching이 무엇인지 알아야 한다.
배칭(batching)은 리액트가 더 나은 성능을 위해 여러개의 state(상태) 업데이트를 하나의 re-render가 발생하도록 그룹화 하는 것을 의미한다.
예를 들어, 하나의 클릭 이벤트 안에 두 개의 state 업데이트를 가지는 경우, React는 항상 이러한 작업을 하나의 렌더링으로 일괄 처리하였다. 다음 코드를 실행할 경우, state를 두 번 변경하지만, React는 한 번의 렌더링만 수행한 것을 확인할 수 있다.
function App() {
const [count, setCount] = useState(0);
const [flag, setFlag] = useState(false);
function handleClick() {
setCount(c => c + 1); // 아직 리렌더링 X
setFlag(f => !f); // 아직 리렌더링 X
// React는 함수가 끝나면 리렌더링 할 것이다 (-> 이것이 배칭)
}
return (
<div>
<button onClick={handleClick}>Next</button>
<h1 style={{ color: flag ? "blue" : "black" }}>{count}</h1>
</div>
);
}
- ✅ Demo: React 17 batches inside event handlers. (클릭시 렌더가 한 번만 콘솔에 찍히는 것을 확인)
import { useState, useLayoutEffect } from "react";
import * as ReactDOM from "react-dom";
function App() {
const [count, setCount] = useState(0);
const [flag, setFlag] = useState(false);
function handleClick() {
console.log("=== click ===");
setCount((c) => c + 1); // Does not re-render yet
setFlag((f) => !f); // Does not re-render yet
// React will only re-render once at the end (that's batching!)
}
return (
<div>
<button onClick={handleClick}>Next</button>
<h1 style={{ color: flag ? "blue" : "black" }}>{count}</h1>
<LogEvents />
</div>
);
}
function LogEvents(props) {
useLayoutEffect(() => {
console.log("Commit");
});
console.log("Render");
return null;
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
이 과정은 불필요한 리렌더링을 줄이기 때문에 성능에 아주 좋다. 또한, 컴포넌트가 '반만 완료된' state를 렌더링하는 방지하여 버그의 발생을 예방할 수 있다. 비유를 하자면 레스토랑의 웨이터가, 요리를 고를 때 마다 주방으로 달려가지 않고, 모든 주문을 마칠 때까지 대기하는 것과 같다.
하지만, React는 업데이이트 배칭 시점에 대한 일관성이 없었다. 예를 들어, 데이터터를 가져온 다음 handleClick 함수 내부에서 state를 업데이트를 할 경우, React는 업데이트를 배칭(일괄 처리)하지 않고, 두 개의 독립적인 업데이트를 수행했다.
일관적이지 못한 이유는 React가 클릭과 같은 브라우저 이벤트의 업데이트만 배칭을 사용해왔기 때문이고, 이 경우 fetch callback에서 이벤트가 처리된 후 state를 업데이트하기 때문에 배칭이 적용되지 않은 것이다.
- 🟡 Demo: React 17 does NOT batch outside event handlers. (클릭시 렌더가 두번 콘솔에 찍히는 것을 확인)
import { useState, useLayoutEffect } from "react"; import * as ReactDOM from "react-dom"; function App() { const [count, setCount] = useState(0); const [flag, setFlag] = useState(false); function handleClick() { console.log("=== click ==="); fetchSomething().then(() => { // React 17 and earlier does NOT batch these: setCount((c) => c + 1); // Causes a re-render setFlag((f) => !f); // Causes a re-render }); } return ( <div> <button onClick={handleClick}>Next</button> <h1 style={{ color: flag ? "blue" : "black" }}>{count}</h1> <LogEvents /> </div> ); } function LogEvents(props) { useLayoutEffect(() => { console.log("Commit"); }); console.log("Render"); return null; } function fetchSomething() { return new Promise((resolve) => setTimeout(resolve, 100)); } const rootElement = document.getElementById("root"); ReactDOM.render(<App />, rootElement);
automatic batching이란 무엇인가?
React 18부터 createRoot를 통해, 모든 업데이트들은 어디서 왔는가와 무관하게 자동으로 배칭된다.
function App() {
const [count, setCount] = useState(0);
const [flag, setFlag] = useState(false);
function handleClick() {
fetchSomething().then(() => {
// React 18과 이후 버전에서는 아래 항목들을 배칭
setCount((c) => c + 1);
setFlag((f) => !f);
// React는 이 콜백이 끝났을 때만 리렌더링을 하게 된다 (이제 여기도 배칭이 들어간다!)
});
}
return (
<div>
<button onClick={handleClick}>Next</button>
<h1 style=>{count}</h1>
</div>
);
}
- ✅ Demo: React 18 with createRoot batches even outside event handlers! (콘솔에 렌더가 한 번만 찍힘)
- 🟡 Demo: React 18 with legacy render keeps the old behavior (콘솔에 렌더가 두 번 찍힘)
import { useState, useLayoutEffect } from "react";
import * as ReactDOM from "react-dom";
function App() {
const [count, setCount] = useState(0);
const [flag, setFlag] = useState(false);
function handleClick() {
console.log("=== click ===");
fetchSomething().then(() => {
// React 18 WITHOUT createRoot does not batches these:
setCount((c) => c + 1); // Causes a re-render
setFlag((f) => !f); // Causes a re-render
});
}
return (
<div>
<button onClick={handleClick}>Next</button>
<h1 style={{ color: flag ? "blue" : "black" }}>{count}</h1>
<LogEvents />
</div>
);
}
function LogEvents(props) {
useLayoutEffect(() => {
console.log("Commit");
});
console.log("Render");
return null;
}
function fetchSomething() {
return new Promise((resolve) => setTimeout(resolve, 100));
}
const rootElement = document.getElementById("root");
// This keeps the old behavior:
ReactDOM.render(<App />, rootElement);
Note: React 18을 도입할 때 createRoot로 업그레이드 하는 것을 권장. render를 통해 확인 가능하도록 한 유일한 이유는 프로덕션 환경에서 테이팅이 용이하기 때문
React는 업데이트의 발생지점과 무관하게 자동으로 업데이트를 배칭한다. 아래 이 예제는,
function handleClick() {
setCount((c) => c + 1);
setFlag((f) => !f);
// React는 이 함수가 끝날 때만 리렌더링을 한다 (배칭이다!)
}
아래 예제와 동일하게 동작하고
setTimeout(() => {
setCount((c) => c + 1);
setFlag((f) => !f);
// React는 이 함수가 끝날 때만 리렌더링을 한다 (배칭이다!)
}, 1000);
아래 예제와 동일하게 동작하고,
fetch(/*...*/).then(() => {
setCount((c) => c + 1);
setFlag((f) => !f);
// React는 이 함수가 끝날 때만 리렌더링을 한다 (배칭이다!)
});
아래 항목과도 동일하게 동작
elm.addEventListener("click", () => {
setCount((c) => c + 1);
setFlag((f) => !f);
// React는 이 함수가 끝날 때만 리렌더링을 한다 (배칭이다!)
});
뱃칭을 원하지 않는 경우
일반적으로 뱃칭은 안전한 절차지만, 일부 코드는 state 변경 후 즉시 DOM으로부터 값을 가져오는 것에 의존한다. 이러한 경우, ReactDOM.flushSync()를 사용하여 배칭하지 않을 수 있다.
import { flushSync } from "react-dom"; // Note: react가 아닌 react-dom이다
function handleClick() {
flushSync(() => {
setCounter((c) => c + 1);
});
// 이 과정이 끝났을 때 React는 DOM을 업데이트한 상태
flushSync(() => {
setFlag((f) => !f);
});
// 이 과정이 끝났을 때 React는 DOM을 업데이트한 상태
}
일반적인 과정은 아닐 것이다.
참고
'IT > React' 카테고리의 다른 글
[React-Hook-Form] react hook form validate debounce 적용기 (0) | 2022.08.07 |
---|---|
[React-Query] react-query를 활용한 무한 스크롤 구현 에러 처리 (0) | 2022.08.05 |
[React] "리액트를 다루는 기술" 15장-18장 키워드 (0) | 2021.11.01 |
[React] "리액트를 다루는 기술" 13.6 리액트 라우터 부가 기능 (함수형 컴포넌트로 변경해보기) (0) | 2021.09.19 |
[React] "리액트를 다루는 기술" 9장 컴포넌트 스타일링 / React Sass-loader 설정 커스터마이징 오류 이슈 (0) | 2021.09.17 |
댓글