import { useState } from "react";
import "./App.css";
function App() {
const [number, setNumber] = useState(0);
const [boolean, setBoolean] = useState(true);
const onClick = () => {
setNumber((prev) => prev + 1);
setBoolean(!boolean);
};
console.log("렌더링");
return (
<div className="App">
<h1>{number}</h1>
<h2>{boolean.toString()}</h2>
<button onClick={onClick}>버튼</button>
</div>
);
}
export default App;
위 소스는 아래 화면을 나타냅니다.
위 화면은 초기에 화면이 출력될 때 렌더링이 콘솔에 찍힙니다.
위와 같이 버튼을 클릭하면 onClick함수가 호출되고 setNumber와 setBoolean의 상태 변경을 하나로 묶어 리렌더링을 한 번만 수행하는 것을 확인할 수 있습니다.
여기까지는 버전 17과 같습니다.
아래는 비동기에서 상태를 업데이트하는 소스코드입니다.
import { useState } from "react";
import "./App.css";
function App() {
const [number, setNumber] = useState(0);
const [boolean, setBoolean] = useState(true);
const onClick = () => {
setTimeout(() => {
setNumber((prev) => prev + 1);
setBoolean(!boolean);
}, 2000);
};
console.log("렌더링");
return (
<div className="App">
<h1>{number}</h1>
<h2>{boolean.toString()}</h2>
<button onClick={onClick}>버튼</button>
</div>
);
}
export default App;
아래 화면과 같이 18 버전은 비동기에서도 자동 배치를 지원하는 것을 확인할 수 있습니다.
import { useState } from "react";
import "./App.css";
function App() {
const [number, setNumber] = useState(0);
const [boolean, setBoolean] = useState(true);
const onClick = () => {
setTimeout(() => {
setNumber((prev) => prev + 1);
}, 2000);
setBoolean(!boolean);
};
console.log("렌더링");
return (
<div className="App">
<h1>{number}</h1>
<h2>{boolean.toString()}</h2>
<button onClick={onClick}>버튼</button>
</div>
);
}
export default App;
아래와 같이 비동기에서 상태 업데이트와 일반 상태 업데이트를 따로 리렌더링 합니다.
setTimeout 안에서 상태 업데이트를 하나로 그룹화하고 onClick 함수의 setTimeout 밖에서의 상태 업데이트를 그룹화합니다.
아래와 같이 flushSync를 활용하여 상태 업데이트하면 자동 배칭 처리가 되지 않습니다.
import { useState } from "react";
import { flushSync } from "react-dom";
import "./App.css";
function App() {
const [number, setNumber] = useState(0);
const [boolean, setBoolean] = useState(true);
const onClick = () => {
flushSync(() => {
setNumber((prev) => prev + 1);
});
flushSync(() => {
setBoolean(!boolean);
});
};
console.log("렌더링");
return (
<div className="App">
<h1>{number}</h1>
<h2>{boolean.toString()}</h2>
<button onClick={onClick}>버튼</button>
</div>
);
}
export default App;
아래 화면과 같이 자동 배치가 적용되지 않은 것을 확인할 수 있습니다.
한번 렌더링 연산이 시작되면 멈출 수가 없는 블로킹 렌더링 문제를 개선할 수 있습니다.
예를 들어, 사용자가 입력창에 검색어를 입력할 때 입력과 함께 검색 결과를 보여주는 경우 사용자가 입력을 하고 있음에도 렌더링 연산이 시작되면 멈출 수 없어 입력창이 버벅거리는 현상이 나타나 사용자 경험이 좋지 않게 됩니다.
아래와 같이 긴급 업데이트와 전환 업데이트가 있다면 전환 업데이트 때문에 긴급 업데이트가 방해되어 블로킹 렌더링 문제가 발생합니다.
이전 버전에서는 검색 결과 등에서 사용했던 Debounce, Throttle 기능을 사용하여 일정 시간을 기다리는 것으로 문제를 해결하였습니다.
Debounce와 Throttle의 경우 일정 시간을 기다리는 것으로 문제를 잠시 미루는 방식이었다면 startTranstion을 사용해서 화면을 그리는 우선순위를 낮추고 사용자 입력에 우선순위를 높여 사용자 경험을 향상할 수 있습니다. 즉 긴급 업데이트를 전환 업데이트보다 우선순위를 높게 설정하여 문제를 해결하는 것입니다.
아래는 useTrasition을 구현한 소스입니다.
import { useState, useTransition } from "react";
import "./App.css";
function App() {
const [isPending, startTransition] = useTransition({ timeoutMs: 5000 });
const [keyword, setKeyword] = useState("");
const [list, setList] = useState([]);
const onChange = (e) => {
setKeyword(e.target.value); // 긴급 업데이트
startTransition(() => {
// 전환 업데이트
setList([...Array(e.target.value.length * 1000)]);
});
};
return (
<div className="App">
<input type={"text"} value={keyword} onChange={onChange} />
<div>
{isPending && <p>...isPending</p>}
{list.map((el, i) => {
return (
<div
key={i}
style={{
width: 100,
height: 100,
margin: 3,
backgroundColor: "black",
}}
></div>
);
})}
</div>
</div>
);
}
export default App;
서버사이드 렌더링의 경우 서버에 데이터가 모두 채워진 html 리소스를 받아 렌더링하고 자바스크립트 코드를 로딩 후 Hydration 단계로 넘어갑니다. 자바스크립트를 로딩하기 전에는 Hydration(자바스크립트 연결) 단계로 넘어갈 수 없고 사용자가 앱과 상호 작용하기 위해서는 Hydration 단계까지 완료되어야 합니다.
이와 같은 문제를 Suspense와 lazy를 사용하여 렌더링 성능을 향상할 수 있습니다.
모든 데이터가 fetch 되어야 렌더 할 수 있지만 Suspense에 컴포넌트를 감싸면 해당 컴포넌트의 데이터가 준비될 때까지 fallback으로 아직 처리되지 않았을 때의 컴포넌트(예: loading spinner)를 화면에 표시하고 데이터가 fetch 된 다른 컴포넌트 부분부터 보여줄 수 있습니다. lazy & Suspense를 활용해 구현할 경우 해당 컴포넌트가 아직 렌더링 되지 않았어도 상관없이 다른 컴포넌트들은 hydration을 시작할 수 있게 되었습니다.
import React, { Suspense } from 'react';
const OtherComponent = React.lazy(() => import('./OtherComponent'));
function MyComponent() {
return (
<div>
<Suspense fallback={<div>Loading...</div>}>
<OtherComponent />
</Suspense>
</div>
);
}