상세 컨텐츠

본문 제목

[React] React 18 성능 개선 정리

WEB

by Yoonsang's Log 2022. 6. 2. 22:34

본문

Concurrent Mode

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가 이전 화면을 계속 표시하도록 지시할 수 있다.

 

동시성(Concurrency)

Concurrent Mode에서 React는 여러 작업을 동시에, 다른 팀원들이 각자 작업을 할 수 있는 브랜치처럼 진행할 수 있다.

  • CPU 바운드 업데이트(DOM 노드 만들기 및 컴포넌트 코드 실행)의 경우 동시성은 더욱 긴급한 업데이트가 이미 시작한 렌더링을 중단할 수 있음을 의미한다.
  • IO 바운드 업데이트(네트워크에서 코드나 데이터를 가져오는 것)의 경우 동시성은 모든 데이터가 도달하기 전에 React가 메모리에서 렌더링을 시작할 수 있으며 로딩을 무시할 수 있다.

 

Concurrent 모드 활성화

createRoot

ReactDOM.createRoot(rootNode).render(<App />);

 

Suspense API

Suspense는 컴포넌트가 렌더링되기 이전에 무언가를 기다리며 그 동안 fallback을 보여준다.

<Suspense fallback={<h1>Loading...</h1>}>
  <ProfilePhoto />
  <ProfileDetails />
</Suspense>

ProfilePhoto, ProfileDetails 컴포넌트에서 비동기 API를 통해 데이터를 받아오고 호출을 기다리고 있는 상황에 Suspense는 Loading...을 보여준다.

 

Suspense List

데이터를 가져올 때, 의도한 순서로 도착하지 않을 수 있다.

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

  • revealOrder(forwards, backwards, together)는 SuspenseList 자식이 표시되는 순서를 정의
    • together은 준비되면 한번에 표시
  • tail(collapsed, hidden)은 SuspenseList에서 로드되지 않은 항목을 표시하는 방법을 나타냄
    • 기본적으로 SuspsnseList는 목록에 있는 모든 폴백을 표시
    • collapsed는 목록에서 다음 폴백만 표시
    • hidden은 로드되지 않은 팡목을 표시

주의사항

  • SuspenseList 아래에 위치하고 가장 인접한 Suspense와 SuspenseList 컴포넌트에만 동작한다.
  • depth가 깊은 경계는 검색하지 않지만 여러 SuspenseList 컴포넌트를 중첩해서 그리드를 형성할 수 는 있다.

 

useTransition

다음 화면으로 transition하기 전에 콘텐츠가 로드될 때까지 대기함으로써 컴포넌트가 바람직하지 않은 로딩 상태를 피할 수 있게 해준다.

또한 컴포넌트가 더 중요한 업데이트를 즉시 렌더링할 수 있도록 후속 렌더링까지 느린 데이터 가져오기를 지연시킬 수 있다.

useTransition Config

timeoutMS가 포함된 선택적인 Suspense Config, 새로운 페이지를 표시하기 전에 기다리는 시간

(주의: 여러 모듈 간에 Suspense Config를 공유하는 것이 좋다)

const SUSPENSE_CONFIG = { timeoutMs: 2000 };​

useTransition hook은 배열에서 두개의 값을 반환

const [startTransition, isPending] = useTransition(SUSPENSE_CONFIG);
  • startTransition은 callback 함수, 지연하고자 하는 상태를 말해주기 위해 이 함수를 사용할 수 있다.
  • isPending은 boolean, transition이 완료되기를 기다리고 있는지 알려주는 React의 방식이다.

사용 예시)

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초동안 지연시킨다.

 

useDeferredValue

최대 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는 네트워크와 장치가 허용할 때 항상 더 짧은 지연을 사용하려 한다.

 

자동 배치(Automatic Batching)

배칭이란

배칭은 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)

 

[참고 자료]

https://medium.com/naver-place-dev/react-18%EC%9D%84-%EC%A4%80%EB%B9%84%ED%95%98%EC%84%B8%EC%9A%94-8603c36ddb25

 

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

 

관련글 더보기

댓글 영역