사용자가 사용하는 UI는 여러 부분으로 구성되어 있다. 일반적으로 폼 입력 필드는 상호 작용에 즉각적으로 응답하지만, 많은 양의 필터링 목록 같은 부분은 응답하는데 오랜 시간이 소요될 수 있다. React는 기본적으로 동기적인 방식으로 렌더링하는데, UI의 느린 작업(필터링 목록 등)이 다른 빠른 작업(입력 폼 등)을 차단하여 응답이 느려지는 현상이 발생한다.

<aside> <img src="/icons/search_gray.svg" alt="/icons/search_gray.svg" width="40px" /> Render-blocking

Concurrent 기능을 사용하지 않을 때 React는 컴포넌트를 동기적으로 렌더링 하며, 렌더링을 시작하면 예외가 아닌 이상 렌더링을 중단할 수 없다. 즉, 렌더링이 끝나야만 다른 작업을 수행할 수 있다.

</aside>

React 18 버전에 공개한 Concurrent 기능은 이런 문제를 해결하기 위해 개발됐다. Concurrent는 여러 작업에 우선순위를 부여하여 중요한 부분을 먼저 처리하는 방식으로, 빠른 작업과 느린 작업을 실질적으로 분리하여 각각 자신의 속도로 작업을 진행할 수 있도록 한다. 즉, 다음 View를 렌더링하는 동안 현재 View의 반응성을 유지하도록 렌더링 프로세스를 재작업한다. 이러한 방식은 사용자 경험을 끊김 없이 유지하면서 어플리케이션의 반응성을 향상시키는 효과를 가져온다.

동기적 렌더링의 문제점


아래는 ①제어(controlled) 컴포넌트 <input />, ②렌더링하는데 2초가 소요되는 <Slow /> 컴포넌트, ③강제로 <Slow /> 컴포넌트를 리렌더링하는 버튼으로 구성되어 있다.

<Slow /> 컴포넌트는 props를 받지 않고 메모이징돼서 input 값이 변경돼도 리렌더링하지 않는다. 하지만 버튼을 클릭할 때마다 key 값이 변경되므로 <Slow /> 컴포넌트는 리렌더링 된다.

export default function App() {
  const [value, setValue] = useState("");
  const [key, setKey] = useState(Math.random());

  return (
    <div className="container">
      <input
        value={value}
        onChange={(e) => {
          console.log(
            `%c Input changed! -> "${e.target.value}"`,
            "color: yellow;"
          );
          setValue(e.target.value);
        }}
      />
      <button onClick={() => setKey(Math.random())}>Render Slow</button>
      <Slow key={key} />
    </div>
  );
}

const Slow = memo(() => {
  sleep(2000);
  console.log("%c Slow rendered!", "color: teal;");
  return <></>;
});

const sleep = (ms) => {
  const start = performance.now();
  while (performance.now() - start < ms);
};

버튼을 클릭해서 <Slow /> 컴포넌트를 리렌더링하는 동안 input 필드에 값을 입력해도 화면에 아무런 반응이 없다가, 리렌더링이 끝나야만 입력한 값이 반영된다. 느린 컴포넌트의 렌더링을 마친 후에야 다른 UI의 업데이트를 처리할 수 있기 때문이다. 느린 작업이 빠른 작업을 차단하는 현상이 발생한 것이다.

  1. a 입력 → 화면에 바로 반영
  2. 버튼 클릭 → <Slow /> 컴포넌트 리렌더링 시작
  3. b 입력 → 반응 없음
  4. (리렌더링 종료) 입력한 내용 반영

Untitled

렌더링 우선순위


Concurrent 렌더링 맥락에서 말하는 업데이트는 리렌더링이 발생하는 상황을 말한다. 업데이트는 두 가지 우선순위로 나뉘며, 우선순위가 낮은 업데이트는 우선순위가 높은 렌더링을 완료해야만 실행한다. 또한, 우선순위가 낮은 업데이트는 우선순위가 높은 업데이트에 의해 중단될 수 있다. 우선순위가 낮은 리렌더링이 중단되면, 우선순위가 높은 리렌더링을 완료할 때까지 기다린 후 처음부터 다시 시작한다.

  1. 우선순위가 높은 업데이트