React 앱이 빠른 반응속도를 유지하고 사용자 장치 및 네트워크 속도에 적절하게 맞추는 기능 집합체
Git과 같은 버전 관리 툴이 있기 전에는 브랜치라는 개념이 없어서 협업에 어려움이 있었음.
React에서 업데이트 렌더링(새로운 DOM 노드 생성 및 컴포넌트 내 코드 실행)을 시작하면 이 작업은 방해받지 않는다.
즉, 렌더링을 차단할 수 있다.
Concurrent Mode에서는 렌더링은 차단되지 않지만 인터럽트가 가능하다.
차단
필터링을 예로 들어,
목록 필터를 입력하고 모든 키를 누를 때마다 버벅거림이 있었다면 이를 debouce나 throttle 기법 등을 통해 해결할 수 있었지만 이들은 최적이 아니다.
버벅거리는 원리를 알아보면, 일단 렌더링이 시작하고 나면 중간에 중단될 수 없다.
즉, 브라우저는 텍스트를 입력하는 동시에 즉시 업데이트 할 수 없다.
인터럽트 렌더링
Concurrent Mode에서는 렌더링을 인터럽트 가능하도록 만듦으로써 근본적인 문제를 수정했다.
사용자가 다른 키를 누를 때, React는 브라우저에 텍스트 입력 업데이트를 차단할 필요가 있다.
대신, React는 브라우저가 입력에 대한 업데이트를 paint하고 메모리 내에 있는 업데이트 목록을 계속 렌더링할 수 있도록 한다.
렌더링이 끝나면 DOM을 업데이트하고 변경 사항들을 화면에 반영한다.
Git으로 비유하면 React가 브랜치 내에 작업을 중지하거나 브랜치 사이에서 전환이 자유로운 것처럼 Concurrent Mode에서 React는 더 중요한 일을 위해 진행중인 업데이트를 중단할 수 있고 이전 작업으로 돌아갈 수도 있다.
즉, 버벅거림을 피하고자 작업을 지연시킬 필요가 없다.
일반적으로 필요한 코드와 데이터를 가져오는데에 그렇게 많은 시간이 소요되지 않는다.
하지만, 충분한 Progress를 보여주기 위해 필요한 코드와 데이터를 불러오지 못하는 상황이 발생할 수도 있다.
React가 기존 화면에서 조금 더 오래 유지할 수 있고 새 화면을 보여주기 전에 로딩을 건너뛸 수 있다면 더 좋을 것이다.
React는 새로운 화면을 준비하기 시작하고 더 많은 콘텐츠를 불러올 수 있도록 DOM을 업데이트하기 전에 기다릴 수 있다.
즉, Concurrent Mode에서는 React가 이전 화면을 계속 표시하도록 지시할 수 있다.
Concurrent Mode에서 React는 여러 작업을 동시에, 다른 팀원들이 각자 작업을 할 수 있는 브랜치처럼 진행할 수 있다.
createRoot
ReactDOM.createRoot(rootNode).render(<App />);
Suspense는 컴포넌트가 렌더링되기 이전에 무언가를 기다리며 그 동안 fallback을 보여준다.
<Suspense fallback={<h1>Loading...</h1>}>
<ProfilePhoto />
<ProfileDetails />
</Suspense>
ProfilePhoto, ProfileDetails 컴포넌트에서 비동기 API를 통해 데이터를 받아오고 호출을 기다리고 있는 상황에 Suspense는 Loading...을 보여준다.
데이터를 가져올 때, 의도한 순서로 도착하지 않을 수 있다.
SuspenseList는 컴포넌트가 사용자에게 표시되는 순서를 조정하여 일시 중단할 수 있는 많은 컴포넌트를 조정하는데 도움을 준다.
<SuspenseList revealOrder="forwards">
<Suspense fallback={'Loading...'}>
<ProfilePicture id={1} />
</Suspense>
<Suspense fallback={'Loading...'}>
<ProfilePicture id={2} />
</Suspense>
<Suspense fallback={'Loading...'}>
<ProfilePicture id={3} />
</Suspense>
...
</SuspenseList>
props
주의사항
다음 화면으로 transition하기 전에 콘텐츠가 로드될 때까지 대기함으로써 컴포넌트가 바람직하지 않은 로딩 상태를 피할 수 있게 해준다.
또한 컴포넌트가 더 중요한 업데이트를 즉시 렌더링할 수 있도록 후속 렌더링까지 느린 데이터 가져오기를 지연시킬 수 있다.
useTransition Config
timeoutMS가 포함된 선택적인 Suspense Config, 새로운 페이지를 표시하기 전에 기다리는 시간
(주의: 여러 모듈 간에 Suspense Config를 공유하는 것이 좋다)
const SUSPENSE_CONFIG = { timeoutMs: 2000 };
useTransition hook은 배열에서 두개의 값을 반환
const [startTransition, isPending] = useTransition(SUSPENSE_CONFIG);
사용 예시)
const SUSPENSE_CONFIG = { timeoutMs: 2000 };
function App() {
const [resource, setResource] = useState(initialResource);
const [startTransition, isPending] = useTransition(SUSPENSE_CONFIG);
return (
<>
<button
disabled={isPending}
onClick={() => {
// Here
startTransition(() => {
const nextUserId = getNextId(resource.userId);
setResource(fetchProfileData(nextUserId));
});
}}
>
Next
</button>
{isPending ? " Loading..." : null}
<Suspense fallback={<Spinner />}>
<ProfilePage resource={resource} />
</Suspense>
</>
);
}
데이터 조회를 startTransition으로 감싸서 2초동안 지연시킨다.
최대 timeoutMS 동안 뒤쳐질 수 있는 값의 지연된 버전을 반환한다.
사용자 입력을 기반으로 즉시 렌더링하거나 데이터 조회를 기다려야 할 때 인터페이스를 반응적으로 유지하는 데에 사용한다.
function App() {
const [text, setText] = useState("hello");
const deferredText = useDeferredValue(text, { timeoutMs: 2000 });
return (
<div className="App">
{/* input에 현재 텍스트를 계속 전달합니다. */}
<input value={text} onChange={handleChange} />
...
{/* 하지만 이 목록은 필요한 경우 "뒤처질" 수 있습니다. */}
<MySlowList text={deferredText} />
</div>
);
}
useDefferedValue Config
const SUSPENSE_CONFIG = { timeoutMs: 2000 };
timeoutMS가 있는 선택적인 Suspense Config를 허용한다.
뒤쳐진 값이 얼마나 지연될 수 있는지 React에 알린다.
React는 네트워크와 장치가 허용할 때 항상 더 짧은 지연을 사용하려 한다.
배칭은 React가 더 나은 성능을 위해 여러 개의 state 업데이트를 하나의 리렌더링으로 묶는 것을 의미한다.
예를 들어 하나의 버튼을 눌렀을 때 2개의 State 업데이트를 가지고 있다면, React는 언제나 이 작업을 배칭하여 하나의 리렌더링으로 만들었다.
function App() {
const [count, setCount] = useState(0);
const [flag, setFlag] = useState(false);
function handleClick() {
setCount((c) => c + 1); // 아직 리렌더링 하지 않음
setFlag((f) => !f); // 아직 리렌더링 하지 않음
// React는 이 함수가 끝나면 리렌더링
}
return (
<div>
<button onClick={handleClick}>Next</button>
<h1 style=>{count}</h1>
</div>
);
}
spring-water-929i6 - CodeSandbox
기존의 React의 배칭은 업데이트를 언제할 것인지에 대해 일관적이지 못했다.
예를 들어, 데이터를 외부 소스로부터 가져와 handleClick 함수 내부에서 state를 업데이트 하고자 한다면 React는 업데이트를 배칭하지 않고 두개의 독립적인 업데이트를 수행하였다.
그 이유는, 클릭과 같은 브라우저의 이벤트 업데이트만 배칭해왔기 때문이고, fetch 콜백에서 이벤트가 핸들링이 완료된 이후에 state를 업데이트하기 때문에 배칭이 적용되지 않았다.
function App() {
const [count, setCount] = useState(0);
const [flag, setFlag] = useState(false);
function handleClick() {
fetchSomething().then(() => {
setCount((c) => c + 1); // 리렌더링을 발생
setFlag((f) => !f); // 리렌더링을 발생
});
}
return (
<div>
<button onClick={handleClick}>Next</button>
<h1 style=>{count}</h1>
</div>
);
}
trusting-khayyam-cn5ct - CodeSandbox
React 18 이전까지, React 이벤트 핸들러 내부에서 발생하는 업데이트만 배칭을 하였다.
Promise, setTimeout, native 이벤트 핸들러, 그리고 여타 모든 이벤트 내부에서 발생하는 업데이트들은 React에서 배칭되지 않았다.
React 18의 createRoot를 통해, 모든 업데이트들은 어디서 왔는가와 무관하게 자동으로 배칭되게 된다.
이 뜻은, timeout, promise, native 이벤트 핸들러와 모든 여타 이벤트는 React에서 제공하는 이벤트와 동일하게 state 업데이트를 배칭할 수 있다.
이를 통해 렌더링을 최소화하고, 나아가 애플리케이션에서 더 나은 성능을 기대할 수 있다.
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>
);
}
morning-sun-lgz88 - CodeSandbox → 렌더가 한번만 찍힘 (React 18)
jolly-benz-hb1zx - CodeSandbox → 렌더가 두번 찍힘 (React 17)
[참고 자료]
React 18을 준비하세요.
요약
medium.com
https://reactjs.org/blog/2022/03/29/react-v18.html
React v18.0 – React Blog
React 18 is now available on npm! In our last post, we shared step-by-step instructions for upgrading your app to React 18. In this post, we’ll give an overview of what’s new in React 18, and what it means for the future. Our latest major version inclu
reactjs.org
[React] SSR(Server Side Rendering), CSR(Client Side Rendering), SSG(Static Site (0) | 2022.04.24 |
---|---|
브라우저 (Browser) (0) | 2022.04.14 |
Selecolor - 프론트엔드 게임 제작 챌린지 (8) | 2022.02.13 |
프론트엔드 개발자 성장 가이드 후기 (0) | 2021.12.25 |
댓글 영역