1. Automatic Batching
- 자동 배치란 여러 개의 state 업데이트를 하나로 묶어 render 함수를 호출(리렌더링 성능 개선)하는 것을 말합니다.
- 기존 17 버전에서도 이러한 배칭 처리는 되었지만 비동기 부분에서는 자동 배치 처리가 되지 않았습니다. 하지만 18 버전부터는 비동기에서도 자동 배치 처리를 지원합니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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과 같습니다.
비동기에서 자동배치
- 기존 17 버전은 비동기에서 자동 배치를 지원하지 않았지만 18 버전에서는 비동기에서도 자동 배치를 지원합니다.
아래는 비동기에서 상태를 업데이트하는 소스코드입니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
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 버전은 비동기에서도 자동 배치를 지원하는 것을 확인할 수 있습니다.
아래와 같이 하나는 동기적 상태 업데이트와 비동기 상태 업데이트를 진행할 경우는 어떻게 될까?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
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 밖에서의 상태 업데이트를 그룹화합니다.
만약 자동 배치 처리를 원하지 않는다면?
- react-dom의 flushSync을 사용할 수 있습니다.
아래와 같이 flushSync를 활용하여 상태 업데이트하면 자동 배칭 처리가 되지 않습니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
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;
아래 화면과 같이 자동 배치가 적용되지 않은 것을 확인할 수 있습니다.
2. useTranstion
- 각 상태 업데이트에 대한 우선순위를 설정할 수 있는 hook입니다.
- isPending: boolean 은 state변경 직후에도 리렌더링 하지 않고 UI를 잠시 유지하는 상태입니다.
- startTransition은 클릭이나 키 입력에 의해 우선순위가 높은 상태 업데이트가 발생할 경우 내부에 선언한 상태 업데이트는 중단되고 클릭이나 키 입력이 끝난 후 이후에 해당 상태 업데이트가 발생합니다.
언제 사용할까?
한번 렌더링 연산이 시작되면 멈출 수가 없는 블로킹 렌더링 문제를 개선할 수 있습니다.
예를 들어, 사용자가 입력창에 검색어를 입력할 때 입력과 함께 검색 결과를 보여주는 경우 사용자가 입력을 하고 있음에도 렌더링 연산이 시작되면 멈출 수 없어 입력창이 버벅거리는 현상이 나타나 사용자 경험이 좋지 않게 됩니다.
아래와 같이 긴급 업데이트와 전환 업데이트가 있다면 전환 업데이트 때문에 긴급 업데이트가 방해되어 블로킹 렌더링 문제가 발생합니다.
- 긴급 업데이트: 입력, 클릭, 누르기와 같은 사용자가 직접적인 상호 작용
- 전환 업데이트: UI 전환
이전 버전에서는 검색 결과 등에서 사용했던 Debounce, Throttle 기능을 사용하여 일정 시간을 기다리는 것으로 문제를 해결하였습니다.
Debounce와 Throttle의 경우 일정 시간을 기다리는 것으로 문제를 잠시 미루는 방식이었다면 startTranstion을 사용해서 화면을 그리는 우선순위를 낮추고 사용자 입력에 우선순위를 높여 사용자 경험을 향상할 수 있습니다. 즉 긴급 업데이트를 전환 업데이트보다 우선순위를 높게 설정하여 문제를 해결하는 것입니다.
Debounce
- 입력이 다 끝나면 일정 시간 뒤 화면 업데이트를 할 수 있습니다.
- 입력하는 동안 화면을 그리지 않게 합니다.
throttle
- 일정한 주기로 화면을 그리게 한다
- 사용자가 일정 주기 동안 입력을 하지 않을 경우에도 기다려야 한다.
startTransition
- 화면 업데이트 중에도 사용자 입력을 받을 수 있습니다.
- 렌더링 하는 와중에도 우선순위가 높은 일이 생기면 그것을 먼저 처리할 수 있습니다.
- timeountMS 설정하면 해당 시간 동안 렌더링을 기다다가 결과를 갖지 못하면 강제로 렌더링 됩니다.
아래는 useTrasition을 구현한 소스입니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
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 className="box" key={i} />
);
})}
</div>
</div>
);
}
export default App;
3. Suspense and SSR
서버사이드 렌더링의 경우 서버에 데이터가 모두 채워진 html 리소스를 받아 렌더링하고 자바스크립트 코드를 로딩 후 Hydration 단계로 넘어갑니다. 자바스크립트를 로딩하기 전에는 Hydration(자바스크립트 연결) 단계로 넘어갈 수 없고 사용자가 앱과 상호 작용하기 위해서는 Hydration 단계까지 완료되어야 합니다.
이와 같은 문제를 Suspense와 lazy를 사용하여 렌더링 성능을 향상할 수 있습니다.
lazy & Suspense
모든 데이터가 fetch 되어야 렌더 할 수 있지만 Suspense에 컴포넌트를 감싸면 해당 컴포넌트의 데이터가 준비될 때까지 fallback으로 아직 처리되지 않았을 때의 컴포넌트(예: loading spinner)를 화면에 표시하고 데이터가 fetch 된 다른 컴포넌트 부분부터 보여줄 수 있습니다. lazy & Suspense를 활용해 구현할 경우 해당 컴포넌트가 아직 렌더링 되지 않았어도 상관없이 다른 컴포넌트들은 hydration을 시작할 수 있게 되었습니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
import React, { Suspense } from 'react';
const OtherComponent = React.lazy(() => import('./OtherComponent'));
function MyComponent() {
return (
<div>
<Suspense fallback={<div>Loading...</div>}>
<OtherComponent />
</Suspense>
</div>
);
}