본문 바로가기

FrontEnd

리액트 Concurrent UI Pattern - Scheduling in React

반응형

원문 : https://philippspiess.com/scheduling-in-react/

 

Scheduling in React

In modern applications, user interfaces often have to juggle multiple tasks at the same time. For example, a search component might need to respond to user…

philippspiess.com

현재는 아래 방법과 다르게 suspense / useTransition을 사용하는 방법으로 변경되었습니다만,

개념을 이해하는데는 아주 좋은 리소스라 생각해서 번역 및 정리하였습니다.

 

아래의 컨셉을 실제로 프로젝트에 적용하는 방식에 대해서는 아래 두 게시물을 참고해 주세요

https://tech.kakaopay.com/post/react-query-2/

 

React Query와 함께 Concurrent UI Pattern을 도입하는 방법 | 카카오페이 기술 블로그

카카오페이에서 React Query를 활용하여 Concurrent UI Pattern을 도입한 사례에 대해 소개합니다. 이 글은 연작 중 2편에 해당합니다. 1편: 카카오페이 프론트엔드 개발자들이 React Query를 선택한 이유, 2

tech.kakaopay.com

https://academind.com/tutorials/react-usetransition-vs-usedeferredvalue

 

React: useTransition() vs useDeferredValue()

React 18 introduced the Concurrency concept and with it two new Hooks: useTransition() and useDeferredValue(). Understanding when to use which can be tricky though.

academind.com

본문 시작

최신 애플리케이션에서 사용자 인터페이스는 종종 동시에 여러 작업을 저글링해야 합니다.
예를 들어 검색 컴포넌트는 자동 완성 결과를 제공하는 동안 사용자 입력에 응답해야 할 수 있으며
대화형 대시보드는 서버에서 데이터를 로드하고 분석 데이터를 백엔드로 보내는 동안 차트를 업데이트해야 할 수 있습니다.
 
이러한 모든 병렬 단계로 인해 인터페이스가 느리고 응답하지 않아 (slow and unresponsive interfaces)
사용자 경험을 망칩니다.
이 문제를 해결하는 방법을 알아보겠습니다.
 

TLDR

개인적으로 읽고 해석을 덧붙인 내용입니다.

  • Immediate : 동기적으로 우선적으로 실행해야 하는 작업입니다.
    • 디폴트인데 스케쥴링을 사용하려면 useTransition을 사용해야겠죠?
  • UserBlocking (250ms timeout) 사용자 상호작용의 결과로 실행되어야 하는 작업(예: 버튼 클릭).
    • 컨커런트 모드의 인터랙션 결과 렌더링
  • Normal (5s timeout) 즉각적으로 느껴질 필요가 없는 업데이트의 경우.
    • 컨커런트 모드의 일반적인 컴포넌트 렌더링
  • Low (10s timeout) 연기할 수 있지만 결국에는 완료해야 하는 작업(예: 분석 알림)
    • 여기서부터 비렌더링 관심사.
  • Idle (no timeout) 전혀 실행할 필요가 없는 작업(예: 숨겨진 오프스크린 콘텐츠).
    • 아예 결과를 렌더링에 반영할 필요도 없음

Scheduling in User Interfaces

사용자는 즉각적인 피드백을 기대합니다.
버튼을 클릭하여 모달을 열거나 입력 필드에 텍스트를 추가할 때 어떤 종류의 확인을 보기 전에 기다리고 싶지 않습니다.
예를 들어, 버튼은 모달을 표시할 수 있고 입력 필드는 입력된 키를 표시할 수 있습니다.
 
그렇지 않을 때 어떤 일이 일어나는지 시각화하기 위해
Dan Abramov가 JSConf 아이슬란드 2018에서 자신의 강연 Beyond React 16(Beyond React 16)에서 발표한 데모 애플리케이션을 살펴보겠습니다.
응용 프로그램은 다음과 같이 작동합니다.
  • 더 많이 입력할수록 아래 차트가 더 자세히 표시됩니다.
  • 두 업데이트(입력 요소 및 차트)가 동시에 실행되기 때문에 브라우저는 너무 많은 계산을 수행하여 일부 프레임을 삭제해야 합니다.
  • 이로 인해 눈에 띄는 지연과 나쁜 사용자 경험이 발생합니다.

입력 업데이트를 우선시하는 버전은 훨씬 더 빠르게 실행되는 것처럼 보여집니다.
이는 동일한 계산 시간이 필요하더라도 사용자가 즉각적인 피드백을 받기 때문입니다.

불행히도 현재 웹 사용자 인터페이스 아키텍처는 우선 순위 지정을 구현하는 것이 쉽지 않습니다.
이 문제를 해결하는 한 가지 방법은 차트 업데이트를 디바운싱하는 것입니다.
이 접근 방식의 문제는 디바운스된 콜백이 실행될 때 차트가 여전히 동기적으로 렌더링되어 사용자 인터페이스가 한동안 응답하지 않게 된다는 것입니다.
 
 

우리는 더 잘할 수 있습니다!


Browser Event Loop

업데이트 우선 순위를 적절하게 지정하는 방법에 대해 자세히 알아보기 전에

브라우저에 이러한 종류의 사용자 상호 작용에 문제가 있는 이유를 더 자세히 살펴보겠습니다.

  • JavaScript 코드는 하나의 스레드에서 실행됩니다. 즉, 주어진 시간에 JavaScript 한 줄만 실행할 수 있습니다.
  • 동일한 스레드는 레이아웃 및 페인트와 같은 다른 문서 수명 주기도 담당합니다.
  • 이는 JavaScript 코드가 실행될 때마다 브라우저가 다른 작업을 수행할 수 없도록 차단됨을 의미합니다.
사용자 인터페이스의 응답성을 유지하기 위해 다음 입력 이벤트를 수신할 수 있는 시간이 매우 짧습니다.
Chrome Dev Summit 2018에서
Shubhie Panicker와 Jason Miller는 A Quest to Guarantee Responsiveness라는 주제로 강연을 했습니다.
대화 중에 그들은 다음 프레임이 그려지고 다음 이벤트가 처리되어야 하기 전의 간격이
최대 16ms(일반적인 60Hz 화면에서 최대 허용하는 처리시간. 1초/60)인
브라우저의 런 루프에 대한 다음 시각화를 보여주었습니다.

대부분의 JavaScript 프레임워크(현재 버전의 React 포함)는 업데이트를 동기식으로 실행합니다.

이 동작을 DOM이 업데이트된 후에만 반환되는 render() 함수로 생각할 수 있습니다.

이 시간 동안 메인 스레드가 차단됩니다.


Problems with Current Solutions

위의 정보를 사용하여 보다 반응성이 뛰어난 사용자 인터페이스에 도달하기 위해 해결해야 하는 두 가지 문제를 공식화할 수 있습니다.
  • 장기 실행 작업은 프레임 드랍을 유발합니다.
    • 각 작업의 크기를 가늠하고,각각 몇 밀리초 내에 완료될 수 있는지 확인해야 한 프레임 내에서 실행할 수 있습니다.
  • 각 작업 별 다른 우선 순위가 있습니다.
    • 위의 예제 애플리케이션에서 사용자 입력의 우선 순위를 지정하면 전반적으로 더 나은 경험을 얻을 수 있음을 확인했습니다.
    • 이렇게 하려면 순서를 정의하고 그에 따라 작업을 예약하는 방법이 필요합니다.

Concurrent React and the Scheduler

⚠️ 경고: 다음 API는 아직 안정적이지 않으며 변경될 예정입니다. 이 게시물을 최신 상태로 유지하기 위해 최선을 다할 것입니다.
(최종 업데이트: 2019년 4월).
React로 적절하게 예약된 사용자 인터페이스를 구현하려면 앞으로 나올 두 가지 React 기능을 살펴봐야 합니다.

Concurrent React (also known as Time Slicing). 

  • React 16과 함께 출시된 새로운 Fiber architecture 재작성 덕분에 React는 이제 렌더링 중에 일시 중지되고 메인 스레드에 양보2할 수 있습니다.
  • 앞으로 Concurrent React에 대해 더 많이 듣게 될 것입니다.
  • 현재로서는 이 모드가 활성화되면 React가 React 컴포넌트의 동기 렌더링을 여러 프레임에서 실행되는 조각으로 분할한다는 것을 이해하는 것이 중요합니다.
  • ➡️ 이 기능을 사용하면 장기 실행 렌더링 작업을 작은 청크로 분할할 수 있습니다.
    • (주 : 더 알아보기 : Suspense, useTransition)

스케줄러

범용 협력 메인 스레드 스케줄러는 React Core 팀에서 개발했으며 브라우저에서 다른 우선 순위 수준으로 콜백을 등록할 수 있습니다.

이 문서를 작성하는 시점에서 우선 순위 수준은 다음과 같습니다.

  • Immediate : 동기적으로 우선적으로 실행해야 하는 작업입니다.
  • UserBlocking (250ms timeout) 사용자 상호작용의 결과로 실행되어야 하는 작업(예: 버튼 클릭).
  • Normal (5s timeout) 즉각적으로 느껴질 필요가 없는 업데이트의 경우.
  • Low (10s timeout) 연기할 수 있지만 결국에는 완료해야 하는 작업(예: 분석 알림).
  • Idle (no timeout) 전혀 실행할 필요가 없는 작업(예: 숨겨진 오프스크린 콘텐츠).
우선 순위가 높은 작업이 계속 실행될 수 있도록 수행할 우선 순위가 훨씬 높은 작업이 있더라도
우선 순위가 낮은 작업이 계속 실행되도록 하려면 각 우선 순위 수준에 대한 타임아웃이 필요합니다.
스케줄링 알고리즘에서 이 문제를 starvation이라고 합니다.
 
타임아웃은 예약된 모든 작업이 결국 실행된다는 보장을 제공합니다.
예를 들어, 앱에 지속적인 애니메이션이 있더라도 단일 분석 알림을 놓치지 않습니다.
 
내부적으로 스케줄러는 등록된 모든 콜백을 만료 시간(콜백이 등록된 시간에 우선 순위 수준의 타임아웃)으로 정렬된 목록에 저장합니다.
그런 다음 스케줄러는 브라우저에서 다음 프레임을 그린 후 실행되는 콜백을 자체적으로 등록합니다.
  • 현재 구현(current implementation)에서는 requestAnimationFrame() 콜백 내에서 postMessage()를 사용하여 이 작업을 수행합니다. 그러면 프레임이 렌더링된 직후에 호출됩니다.
이 콜백 내에서 스케줄러는 다음 프레임을 렌더링할 시간이 될 때까지 등록된 콜백을 최대한 많이 실행합니다.
➡️ 이 기능을 사용하면 우선 순위가 다양한 작업을 예약할 수 있습니다.

Scheduling in Action

이러한 기능을 사용하여 앱이 훨씬 더 반응적으로 느껴지도록 하는 방법을 살펴보겠습니다.
이를 위해 사용자가 이름 목록에서 검색어를 강조 표시할 수 있는 앱인 ScheduleTron 3000(ScheduleTron 3000)을 살펴보겠습니다. 먼저 초기 구현을 살펴보겠습니다.

 

// The app shows a search box and a list of names. The list is
// controlled by the searchValue state variable, which is updated
// by the search box.
function App() {
  const [searchValue, setSearchValue] = React.useState();

  function handleChange(value) {
    setSearchValue(value);
  }

  return (
    <div>
      <SearchBox onChange={handleChange} />
      <NameList searchValue={searchValue} />
    </div>
  );
}
// 검색 상자는 기본 HTML 입력 요소를 렌더링하고
// inputValue 변수를 사용하여 제어합니다.
//  새 키가 눌리면 먼저 로컬 inputValue를 업데이트한 다음 
// App 컴포넌트의 searchValue를 업데이트한 다음 
// 서버에 대한 분석 알림을 시뮬레이션합니다.
//

function SearchBox(props) {
  const [inputValue, setInputValue] = React.useState();

  function handleChange(event) {
    const value = event.target.value;

    setInputValue(value);
    props.onChange(value);
    sendAnalyticsNotification(value); // 매우 느림
  };

  return (
    <input
      type="text"
      value={inputValue}
      onChange={handleChange}
    />
  );
}

ReactDOM.render(<App />, container);

아래 검색 상자에 이름(예: "Ada Stewart")을 입력하고 작동 방식을 확인하세요.


인터페이스가 반응이 좋지 않다는 것을 알 수 있습니다.
문제를 증폭시키기 위해 이름 목록의 렌더링 시간을 인위적으로 늦췄습니다.
그리고 이 목록은 방대하기 때문에 애플리케이션의 성능에 상당한 영향을 미칩니다. 좋지 않아요 😰 .
 
사용자는 즉각적인 피드백을 기대하지만 키 입력 후 앱이 한동안 응답하지 않습니다.
무슨 일이 일어나고 있는지 이해하기 위해 DevTools의 성능 탭을 살펴보겠습니다.
다음은 검색 상자에 "Ada"라는 이름을 입력하는 동안 녹음한 스크린샷입니다.
우리는 일반적으로 좋은 징조가 아닌 많은 빨간색 삼각형이 있음을 알 수 있습니다.
모든 키 입력에 대해 키 누르기 이벤트가 발생하는 것을 볼 수 있습니다.
세 가지 이벤트는 모두 하나의 프레임 내에서 실행되며,
  • 첫 번째 keypress 이벤트를 처리한 후 브라우저는 대기열에서 보류 중인 이벤트를 보고 프레임을 렌더링하기 전에 이벤트 리스너를 실행하기로 결정합니다.
이로 인해 프레임의 지속 시간이 733ms로 확장됩니다. 이는 평균 프레임 예산인 16ms보다 훨씬 높습니다.
이 키 누르기 이벤트 내에서 React 코드가 호출되어 입력 값과 검색 값이 업데이트된 다음 분석 알림을 보냅니다.
결과적으로 업데이트된 상태 값으로 인해 앱이 모든 개별 이름으로 다시 렌더링됩니다.
그것은 우리가 해야 할 많은 작업이며, 순진한 접근 방식으로 메인 스레드를 차단할 것입니다!
현상 유지를 개선하기 위한 첫 번째 단계는 불안정한 동시 모드를 활성화하는 것입니다.
다음과 같이 새로운 ReactDOM.createRoot API로 React 루트를 생성하면 됩니다.
- ReactDOM.render(<App />, container);
+ const root = ReactDOM.unstable_createRoot(rootElement);
+ root.render(<App />);
그러나 동시 모드만 활성화해도 우리의 경우 환경이 변경되지 않습니다.
React는 여전히 두 상태 업데이트를 동시에 수신하므로 어느 것이 덜 중요한지 알 방법이 없습니다.
 
대신 처음에 검색 상자를 업데이트하기만 하면 되도록 입력 값을 먼저 설정하려고 합니다.
검색 값 및 분석 알림에 대한 업데이트는 나중에 발생합니다.
이를 위해 우리는 스케줄러 패키지(npm i 스케줄러와 함께 설치할 수 있음)에 의해 노출된 API를 사용하여
우선순위가 낮은 콜백을 대기열에 추가합니다.
import { unstable_next } from "scheduler";

function SearchBox(props) {
  const [inputValue, setInputValue] = React.useState();

  function handleChange(event) {
    const value = event.target.value;

    setInputValue(value);
    unstable_next(function() {
      props.onChange(value);
      sendAnalyticsNotification(value);
    });
  }

  return <input type="text" value={inputValue} onChange={handleChange} />;
}​
우리가 사용하는 API, unstable_next() 내의
모든 React 업데이트는 onChange 리스너 내부 로직의 기본 우선순위보다 낮은 Normal 우선순위로 예약됩니다.

 

실제로 이 변경으로 인해 입력 상자가 이미 훨씬 더 반응적으로 느껴지고 입력하는 동안 프레임이 더 이상 떨어지지 않습니다.

성능 탭을 함께 다시 살펴보겠습니다.

장기 실행 작업은 이제 단일 프레임 내에서 완료할 수 있는 더 작은 작업으로 분할됩니다.

프레임 드랍을 나타내는 빨간색 삼각형도 사라졌습니다.

그러나 여전히 이상적이지 않은 한 가지는 분석 알림(위 스크린샷에서 강조 표시됨)이 렌더링 작업과 함께 계속 실행된다는 것입니다.
우리 앱의 사용자는 이 작업을 볼 수 없기 때문에 더 낮은 우선순위로 콜백을 예약할 수 있습니다.
(눈에 보이지 않으면 매우 낮은 우선순위를 먹인다 - 비렌더링 관심사) 
import {
  unstable_LowPriority,
  unstable_scheduleCallback
} from "scheduler";

function sendDeferredAnalyticsNotification(value) {
  unstable_scheduleCallback(unstable_LowPriority, function() {
    sendAnalyticsNotification(value);
  });
}
이제 검색 상자 컴포넌트에서 sendDeferredAnalyticsNotification()을 사용하면
성능 탭에서 모든 렌더링 작업이 완료된 후 분석이 전송되는 것을 볼 수 있습니다.
또한 모든 우리 앱의 작업은 완벽하게 스케줄링되어 있습니다.

Try it out!


스케줄러의 한계

스케줄러를 사용하면 콜백이 실행되는 순서를 제어할 수 있습니다.
최신 React 구현에 깊숙이 구축되어 있으며 Concurrent 모드에서 즉시 사용할 수 있습니다.
즉, 스케줄러에는 두 가지 제한 사항이 있습니다.
  • 리소스 파이팅
    • 스케줄러는 사용 가능한 모든 리소스를 사용하려고 합니다.
    • 스케줄러의 여러 인스턴스가 동일한 스레드에서 실행되고 리소스를 놓고 경쟁하는 경우 문제가 발생합니다.
    • 애플리케이션의 모든 부분이 동일한 스케쥴러 인스턴스를 사용하도록 해야 합니다.
  • 사용자 정의 작업과 브라우저 작업의 균형.
    • 스케줄러는 브라우저에서 실행되기 때문에 브라우저가 노출하는 API에만 액세스할 수 있습니다.
    • 렌더링 또는 가비지 수집과 같은 도큐먼트 생명 주기는 제어할 수 없는 방식으로 작업을 방해할 수 있습니다.
 
이러한 제한을 제거하기 위해
Google Chrome 팀은 React, Polymer, Ember, Google Maps 및 Web Standards Community와 협력하여 브라우저에서 Scheduling API in the browser를 만들고 있습니다.
얼마나 신나는 일인가요?

결론

  • Concurrent React와 Scheduler를 사용하면 애플리케이션에서 작업 스케줄링을 구현할 수 있어 응답성이 뛰어난 사용자 인터페이스를 만들 수 있습니다.
  • 이러한 기능의 공식 릴리스는 2019년 2분기에 있을 것입니다. 그때까지는 불안정한 API를 가지고 놀 수 있지만 변경될 것이라는 점에 유의하십시오.
  • 이러한 API가 언제 변경되거나 새로운 기능에 대한 문서가 언제 작성되는지 가장 먼저 알고 싶다면 This Week in React ⚛️⚛️를 구독하세요.

 

 




 

 

 
 

 

반응형