[React] Concurrent Mode

또이·2024년 12월 10일
1

React 18부터 Concurrent Features가 기본적으로 활성화되었으며, React 16부터 Fiber 아키텍처를 기반으로 Concurrent Mode의 기반 작업이 준비되었습니다.
업데이트, 즉 태스크를 처리할 때 우선순위 조정이 가능해졌다는 특징이 있습니다.
다른 태스크를 처리하다가, 더 먼저 처리할 태스크가 들어오면 더 먼저 처리할 태스크를 처리할 수 있습니다.

오늘은 Concurrent Mode에 대해 알아보도록 하겠습니다.

Concurrent Mode

동시성이란

자바스크립트는 싱글스레드 언어로 하나의 작업씩 처리해야 했습니다.
따라서 렌더링 작업이 일어나고 있을 때 다른 작업을 진행하지 못하고 blocking되는 현상이 존재했습니다.

React가 조정 단계를 시작하면 완료될 때까지 중지할 수 없었고, 브라우저의 기본 UI 스레드는 사용자 입력을 받으면서 다른 작업을 실행할 수 없었습니다.
DOM 트리가 커지면 프레임 드롭이 발생해서 멈추거나 심지어 응답하지 않는 문제가 발생하게 되었습니다.

개발자들이 메모이제이션과 debounce, throttle과 같은 기술을 사용하여 사용자 경험을 더 좋게 만들지만, 단지 주된 문제인 렌더링은 여전히 막혀있다는 문제의 해결을 지연할 뿐이었습니다.

Debounce와 Throttle의 한계

input을 통해 값이 새롭게 들어오고, 입력값을 화면에 출력한다고 가정해보겠습니다!

입력값을 화면에 그리는 렌더링이 동작하는데, input에 계속 입력을 하게 되면 input이 blocking되어 끊기게 됩니다.
이런 버벅임 현상을 잡기 위해 debounce와 throttle을 사용한다면,
정해진 시간 동안의 input의 부담을 덜어주며 이벤트를 처리할 수 있습니다.

하지만 이 방법에는 한계가 있습니다.
throttle의 경우, wait time을 높이게 되면 반응이 느려지기 때문에 wiat time에 따라 성향에 영향을 줍니다.
debounce는 wiat time만큼 딜레이가 발생하기 때문에 가장 합리적인 시간을 정하기가 어렵습니다.

이러한 한계는 Concurrent Mode로 극복 가능합니다.
Concurrent Mode는 빠른 작업 간 전환으로 사용자 입력과 무거운 작업이 버벅대지 않고 동시에 처리되는 경험을 사용자에게 줄 수 있고,
작업 처리 속도는 개발자가 설정한 delay가 아닌, 사용자의 기기 성능에 좌우될 수 있도록 합니다!

이때 동시성은 동시에 두가지 이상의 일을 지원하는 것을 말합니다.
두가지 작업을 처리하는 사람은 한명일지라도, 두가지 작업을 동시에 진행할 수 있는데요,
따라서 단일 스레드 사용의 한계를 깨고 애플리케이션을 더 효율적으로 만들 수 있습니다.

렌더링 프로세스가 더 작은 작업으로 나뉘고 스케줄러를 통해 중요도에 따라 우선순위를 지정(Time slicing)을 하면서
1. 메인 스레드를 차단하지 않고
2. 한 번에 여러 작업을 수행하고 우선순위에 따라 작업 간을 전환하고
3. 결과를 확정하지 않고 부분적으로 렌더링할 수 있게 되었습니다.

이는 React Fiber를 통해 수행이 가능합니다.

Fiber란

Fiber는 UI 업데이트를 작은 작업 단위로 나누고, 이를 스케줄링하여 관리합니다.
작업 단위마다 우선순위를 설정하고,
렌더링을 중단하거나 재개가 가능하기 때문에
중요한 작업이 우선적으로 처리되고, 덜 중요한 작업은 나중에 처리될 수 있습니다.
➡️ 이러한 구조 덕분에 Concurrent Mode가 가능해졌습니다!

React 18의 가장 핵심인 개념

Concurrent Mode는 특정 state가 변경되었을 때 현재 UI를 유지하고 해당 변경에 따른 UI 업데이트를 동시에 준비합니다. 그리고 준비 중인 UI의 렌더링 단계가 특정 조건에 부합하면 실제 DOM에 반영합니다.

따라서 새로운 화면을 메인 스레드를 blocking하는거 없이 백그라운드에서 준비가 가능해서,
UI 업데이트의 동시성을 구현하면 사용자가 느끼지 못할 정도로 부드러운 전환되기 때문에 사용자 경험이 좋습니다.

주의할 점은 동시성 render를 갖는 것은 아닙니다!
React는 렌더링 중간에 멈췄다가 다시 시작할 수도 있고 진행중인 렌더링을 버릴 수도 있습니다.

어떻게 동작되나요?

우선순위

렌더링 작업에 우선순위를 할당하여, 중요한 데이터의 렌더링을 우선 처리합니다.

이전의 Timer Queue와 Task Queue

React 17 버전 이전에서는 작업의 만료 시간(Expiration Time)을 기준으로 우선순위를 부여하는 매커니즘으로 구현되었습니다.

작업이 생성될 때 "이 작업은 언제까지 완료되어야 하는가?"라는 만료 시간을 부여하고, 가까운 시점에 만료되는 작업일수록 높은 우선순위를 가졌습니다.

export function getCurrentTime() {
  return msToExpirationTime(now());
}
export function msToExpirationTime(ms: number): ExpirationTime {
  return MAGIC_NUMBER_OFFSET - ((ms / UNIT_SIZE) | 0); // ms는 performance.now 또는 Date.now를 호출하기 때문에 오래될수록 숫자가 커짐
}

Fiber 아키텍처는 Timer Queue와 Task Queue를 활용하여작업을 분할하고 스케줄링했는데,
이때 Timer Queue는 작업 만료 시간을 기준으로 큐를 정렬했습니다.

긴급 작업은 Task Queue로 만료 시간과 관계없이 즉시 실행했고,
일반 작업은 Timer Queue로 만료 시간을 기준으로 예약해서 높은 우선순위 작업이 없으면 처리되었습니다.

Timer Queue와 Task Queue

  • Timer Queue
    • 일정 시간이 지나야 실행되는 작업을 처리하는 대기열
    • 작업의 만료 시간(Expiration Time)을 기준으로 작업을 관리
    • 만료 시간이 가까운 작업일수록 높은 우선순위를 갖고 큐의 상단에 배치됨
    • 만료 시간이 지난 작업은 더 이상 지연되지 않고 즉시 처리됨
    • React는 Timer Queue를 사용해 작업을 예약하고 브라우저의 이벤트 루프 및 Idle 시간을 활용해서 작업을 실행함
    • 백그라운드 작업
  • Task Queue
    • 즉시 실행 가능한 작업을 처리하는 대기열
    • 만료 시간과 관계없이, 항상 가장 높은 우선순위를 가짐
    • React의 이벤트 핸들러가 실행된 후 작업을 생성하고, 이 작업을 즉시 실행할 수 있도록 준비함

현재의 Lane 모델

React 18부터 Lane 모델이 도입되면서 이전보다 더 강력하고 추상화된 방식으로 우선순위 기반 스케줄링이 가능해졌습니다.

브라우저의 Idle 시간을 활용하기 위해 requestIdleCallback에서 발전된 scheduler 라이브러리를 사용하게 되었고,
Timer Queue와 Task Queue는 사라지고 Lane 모델과 scheduler로 큐 관리를 추상화하게 되었습니다.

Lane 모델은 작업의 우선순위를 관리하는 React의 내부 메커니즘입니다.
높은 우선순위의 작업은 더 높은 Lane에 할당되는데,
높은 우선순위의 Lane은 사용자 상호작용과 관련된 작업을 처리하고,
낮은 우선순위의 Lane은 데이터 렌더링 관련 작업을 처리했습니다.

우선순위에 따라 우선 실행권을 부여하고 작업 배치 개념을 착안하여 최종 작업 순서를 정하고 진행합니다.

Lane의 종류

  • SyncLane
    • 동기적 작업 처리 Lane
    • 우선순위 1
    • 사용자 버튼 클릭, 입력
  • InputContinuousLane
    • 연속적인 사용자 상호작용에 대한 업데이트
    • 우선순위 2
    • 사용자가 텍스트 입력란에 글자를 입력하는 동안 React는 이러한 입력에 대한 업데이트를 InputContinuousLane에서 처리
  • TransitionLane
    • UI 전환과 관련된 작업처리
    • 우선순위 3
    • 화면 전환, 애니메이션 효과, 모달 다이얼로그 열고/닫기
    • suspense, useTransition, useDefferredValue에 의해 생성된 업데이트
  • DefaultLane
    • 대부분 일반적인 렌더링
    • 우선순위 가장 낮음
    • 컴포넌트 초기렌더링, 상태변경에 따른 렌더링

React Scheduler에서 사용되는 우선순위 큐

실제 코드에서 구현된 우선순위 큐를 통해서 Lane 모델을 어떻게 처리하는지 알아보겠습니다.

  1. 작업 추가 push
  • Fiber에서 발생한 작업은 push를 통해 힙에 추가
  • 사용자 입력, 데이터 요청, 화면 전환 작업 등이 큐에 쌓임
  1. 작업 평가 peek
  • 가장 우선순위가 높은 작업을 확인
  • 긴급한 작업(사용자 입력)이 있다면 먼저 처리
  1. 작업 실행 및 제거 pop
  • 우선순위가 높은 작업이 실행
  • 실행된 작업은 pop을 통해 큐에서 제거되고, 힙은 재정렬
  1. 작업 중단 및 재개
  • 긴급한 작업이 발생하면 현재 작업을 중단하고 긴급 작업을 처리
  • 중단된 작업은 다시 큐에 추가되어 나중에 실행
  1. 렌더링 최적화
  • 브라우저의 Idle 시간을 활용하여 낮은 우선순위 작업을 처리
  • 화면 전환 작업이 데이터 요청 작업보다 낮은 우선순위를 가짐
// 힙 자료구조 정의
type Heap<T: Node> = Array<T>;

// 작업(Node)의 기본 구조
type Node = {
  id: number,        // 작업 고유 ID
  sortIndex: number, // 우선순위 (낮을수록 높은 우선순위)
  ...
};

// 힙에 새로운 작업 추가하기
export function push<T: Node>(heap: Heap<T>, node: T): void {
  const index = heap.length;  // 새 노드가 들어갈 위치 (배열 끝)
  heap.push(node);            // 노드를 배열에 추가
  siftUp(heap, node, index);  // 힙의 우선순위를 유지하도록 정렬
  // React에서는 Fiber 작업이 추가될 때 호출되며,
  // 우선순위 기반으로 작업 큐 정렬
}

// 힙의 최상단(우선순위가 가장 높은) 작업 반환하기
export function peek<T: Node>(heap: Heap<T>): T | null {
  return heap.length === 0 ? null : heap[0];
  // React에서는 현재 처리할 작업을 확인할 때 사용
  // 작업은 제거하지 않고 가장 높은 우선순위의 작업만 반환
}

// 힙의 최상단 작업을 제거하고 반환하기
export function pop<T: Node>(heap: Heap<T>): T | null {
  if (heap.length === 0) {
    return null; // 힙이 비어있으면 null 반환
  }
  const first = heap[0]; // 최상단 작업
  const last = heap.pop(); // 배열의 마지막 작업
  if (last !== first) {
    heap[0] = last;          // 마지막 작업을 최상단으로 이동
    siftDown(heap, last, 0); // 힙 정렬 복구
  }
  return first;
  // React에서는 실행된 작업을 큐에서 제거하며, 나머지 작업들을 다시 정렬
}

// 힙 정렬을 위로 향해 복구 (삽입 시 호출)
function siftUp<T: Node>(heap: Heap<T>, node: T, i: number): void {
  let index = i;
  while (index > 0) {
    const parentIndex = (index - 1) >>> 1; // 부모 노드 인덱스
    const parent = heap[parentIndex];
    if (compare(parent, node) > 0) {
      // 부모 노드가 현재 노드보다 우선순위가 낮으면 교체
      heap[parentIndex] = node;
      heap[index] = parent;
      index = parentIndex;
    } else {
      return; // 부모 노드가 더 높은 우선순위를 가지면 종료
    }
  }
  // React에서는 작업이 추가될 때 큐의 정렬 상태를 유지.
}

// 힙 정렬을 아래로 향해 복구 (삭제 시 호출)
function siftDown<T: Node>(heap: Heap<T>, node: T, i: number): void {
  let index = i;
  const length = heap.length;
  const halfLength = length >>> 1; // 리프 노드 전까지 탐색
  while (index < halfLength) {
    const leftIndex = (index + 1) * 2 - 1; // 왼쪽 자식 노드
    const left = heap[leftIndex];
    const rightIndex = leftIndex + 1;     // 오른쪽 자식 노드
    const right = heap[rightIndex];

    if (compare(left, node) < 0) {
      if (rightIndex < length && compare(right, left) < 0) {
        heap[index] = right;      // 오른쪽 자식이 더 우선순위 높으면 교체
        heap[rightIndex] = node;
        index = rightIndex;
      } else {
        heap[index] = left;       // 왼쪽 자식과 교체
        heap[leftIndex] = node;
        index = leftIndex;
      }
    } else if (rightIndex < length && compare(right, node) < 0) {
      heap[index] = right;        // 오른쪽 자식과 교체
      heap[rightIndex] = node;
      index = rightIndex;
    } else {
      return; // 자식 노드들보다 우선순위가 높으면 종료
    }
  }
  // React에서는 작업이 제거된 후 큐의 정렬 상태를 복구
}

// 작업의 우선순위를 비교하기
function compare(a: Node, b: Node) {
  const diff = a.sortIndex - b.sortIndex; // sortIndex를 기준으로 비교
  return diff !== 0 ? diff : a.id - b.id; // 같으면 id를 기준으로 비교
  // React에서는 작업 간 우선순위를 비교하여 작업 큐를 관리
}

작업 분할

Concurrent Mode는 성능 향상을 위해 이러한 우선순위를 나누고 조절하며 수행하는 작업을 "분할"하여 처리합니다.
컴포넌트를 먼저 부분적으로 렌더링하고, 브라우저에서 동시에 처리할 수 있는 다른 작업을 처리합니다.
작은 단위로 작업을 분할하면 여러 작업을 동시에 수행할 수 있어 메인 스레드가 blocking되지 않고 렌더링이 진행됩니다.

동시성 다루기

startTransition

startTransition은 React 18에서 도입된 함수로, 상태 업데이트를 전환 작업으로 처리하여 우선순위를 낮추는 역할을 합니다.
import { startTransition } from 'react';로 import해서 단독으로 사용이 가능하며, 상태 관리 기능은 따로 제공하지 않습니다.

startTransition(() => {
      setState((prev) => prev +1); // 우선순위가 낮은 작업으로 처리
});

useTransition

💡 2021년 7월 기준 experimental 버전에서 useTransition의 return 형태가 [startTransition, isPending]에서 [isPending, startTransition]으로 바꼈습니다!

useTransitionstartTransition을 기반으로 한 훅으로, 추가적으로 로딩 상태 관리(isPending) 기능을 제공합니다.
import { useTransition } from 'react';로 import해서
const [isPending, startTransition] = useTransition();으로 사용할 수 있습니다.
isPending으로 전환 작업(상태 업데이트)이 진행 중인지 표시하여 로딩 상태를 관리할 수 있습니다.

아래의 예시 코드를 살펴보면

function mountTransition() {
  const [isPending, setPending] = mountState(false); // 전환 상태를 false로 초기화
  const start = startTransition.bind(null, setPending); // setPending을 전달
  return [isPending, start];
}

function startTransition(setPending, callback) {
  setPending(true); // 전환 시작 (isPending = true)

  const prevTransition = ReactCurrentBatchConfig.transition;
  ReactCurrentBatchConfig.transition = {}; // Transition 플래그 설정

  try {
    callback(); // 상태 업데이트 실행
  } finally {
    setPending(false); // 전환 종료 (isPending = false)
    ReactCurrentBatchConfig.transition = prevTransition; // 플래그 복원
  }
}
  1. isPendingtrue로 설정해서 전환 작업이 시작됨을 알리고, 이 상태는 즉시 렌더링되어 UI에 반영됩니다.
  2. 전환 작업(콜백 함수)가 낮은 우선순위로 실행됩니다.
  3. 콜백 함수가 완료되면 isPendingfalse로 설정하고, 다시 렌더링되어 최종 상태에 반영됩니다.

이때 useTransition은 추가적인 렌더링 단계가 있기 때문에
startTransition보다 시간이 더 걸릴 수 있습니다.
따라서 useTransition은 사용자에게 로딩 상태를 제공해야 하는 경우에 적합하고, startTransition은 상태관리가 필요하지 않은 간단한 상태 전환 작업에 적합합니다.

useDeferredValue

useDeferredValue는 현재 상태 값의 변경을 지연시켜 렌더링 우선순위를 조정합니다.

useDeferredValue는 상태 값을 받아서 지연된 값을 반환합니다.
값이 지연됨에 따라 UI는 이전 값을 유지하고, 업데이트가 완료되면 새 값을 렌더링합니다.

const [inputValue, setInputValue] = useState('');
const deferredValue = useDeferredValue(inputValue); // 백그라운드에서 렌더링 작업 처리 (낮은 우선순위 렌더링)

return (
  <div>
    <input
      value={inputValue}
      onChange={(e) => setInputValue(e.target.value)}
    />
    <ul>
      {deferredValue.split('').map((char, index) => (
        <li key={index}>{char}</li>
      ))}
    </ul>
  </div>
);

이를 통해 사용자는 즉각적인 입력값을 확인할 수 있고,
렌더링은 지연됩니다.

useTransition은 상태 업데이트(setState)를 감싸는 방식으로 상태 업데이티 우선순위를 낮추며 작동하고, useDeferredValue는 값(state)을 받아 지연된 값을 반환하여 상태 값을 지연시키며 이전 값을 유지는데요,
모두 렌더링 우선순위를 조정하여 사용자 경험을 부드럽게 만드는 Concurrent Mode의 우선순위 조정의 핵심 도구로 복잡한 UI에서도 반응성을 유지하며 최적화된 성능을 제공한다는 장점이 있습니다.

Suspense, 그리고 Concurrent Mode

JND와 Concurrent Mode

Suspense로만 로딩을 구현하면 이전 페이지를 사용자로부터 block하고 다음 페이지의 전체 로딩 화면으로 대체하기 때문에 사용자는 답답함을 느낄 수 있습니다.

비동기 호출에서 로딩이 발생하는 경우라면 로딩 화면을 보여주는 UI는 자연스럽지만, 비동기 처리가 매우 빠르게 처리된다면 깜빡임이 발생하여 사용자 경험을 저하시킬 수 있습니다.

JND(Just Noticeable Difference)는 인지 가능한 최소 차이를 뜻하는데,
"전환이 JND 아래에 머물 수 있다"는 말은 "전환이 항상 사용자가 알아채지 못할 정도로 지연 시간이 작다"는 뜻입니다.
이 경우, 사용자 경험을 위해 Fallback을 생략하고 이전 상태의 화면을 유지하는 것이 더 나을 수 있습니다.

이때 Concurrent Mode는 일정 시간동안 현재 페이지와 기능들을 유지하고 다음 페이지에 대한 렌더링을 동시에 진행함으로써 해결이 가능한데, useTransition을 사용해서 해결이 가능합니다.

JND보다 큰 지연이면 Suspense를 사용하나요?

JND보다 큰 지연에도 원래 상태의 UI를 유지하는게 더 좋은 경우도 있습니다.
이때는 Fallback과 마찬가지로 지연상태에 대한 피드백을 사용자에게 전달해야 하며 isPending을 활용할 수 있습니다.

Transition과 Concurreunt

React는 상태가 변경되면 일반적으로 화면의 즉각적인 변화를 기대합니다.
하지만 UI 반응성을 유지하기 위해 화면 변화를 지연하고 싶은 경우도 있습니다.

예를 들어 한 페이지에서 다른 페이지로 전환할 때,
다음 화면에 필요한 코드나 데이터가 준비되지 않았다면 깜빡임이 발생할 수 있습니다.
useTransition으로 이전 화면을 길게 보여줌으로써 부드러운 전환이 가능합니다.

import { useTransition, Suspense } from "react";

function App() {
  const [resource, setResource] = useState(initialResource);
  const [isPending, startTransition] = useTransition({ timeoutMs: 3000 });

  return (
    <div>
      <button
        disabled={isPending} // 로딩 중에는 버튼 비활성화
        onClick={() => {
          startTransition(() => { // 데이터 로드를 낮은 우선순위로 처리, 버튼 클릭은 즉시 반응
            const nextUserId = getNextId(resource.userId);
            setResource(fetchProfileData(nextUserId)); // 다음 데이터 로드
          });
        }}
      >
        {isPending ? "Loading..." : "Next"}
      </button>

      <Suspense fallback={<Loading />}>
        <ProfilePage resource={resource} />
      </Suspense>
    </div>
  );
}

Transition으로 보는 렌더링

렌더링은 Transition ➡️ Loading ➡️ Done의 과정으로 이루어집니다.

  • Transition
    • state 변경 직후에 일어날 수 있는 UI 렌더링 단계
    • Pending
      • useTransition 훅을 사용하면 state 변경 직후에도 UI를 업데이트하지 않고 현재 UI를 잠시 유지할 수 있는데, 이를 Pending 상태라고 함
    • Receded
      • useTransition 훅을 사용하지 않은 기본 상태
        • state 변경 직후 UI가 변경됨
        • Pending 상태의 시간이 useTransition 옵션으로 지정된 timeoutMs을 넘으면 강제로 Receded 상태로 넘어감
  • Loading
    • Skeleton 상태
    • 페이지의 일부만 로딩하는 상태로,
      전체 화면을 모두 로딩으로 대체하는 Receded와 다름
  • Done
    • Complete 상태
    • 로딩 UI 없이 모든 정보가 사용자에게 보이는
      최종적으로 목표하는 단계

렌더링을 유발하는 state 관점으로, 상태 변화에 따라 UI가 재렌더링되고, 이때 React는 UI를 부드럽게 하기 위해서 백그라운드에서 새로운 state에 대한 데이터를 준비합니다.

Concurrent Mode에서는 새로운 상태로의 전환 과정에서 우선순위가 중요한 역할을 하는데, 더 최신 상태를 기준으로 렌더링이 결정됩니다.

Pending ➡️ Skeleton ➡️ Complete를 거친 후, 새로운 화면을 들어간다면 다시 Pending이 될까요?

만약 새로운 화면의 데이터가 기존 화면의 데이터와 동일하다면
(예를 들어 목록 페이지랑 상세 페이지랑 같은 데이터라면),
바로 Complete 상태로 넘어가게 됩니다.

이미 현재 UI는 Complete 상태이기 때문에,
Pending이나 Skeleton 상태보다 더 최신 상태이므로
Pending이나 Skeleton 상태로 전환하지 않고
바로 Complete로 넘어간 것입니다.

더 자세하게 흐름을 따라가보면,
새로운 상태가 준비되는 동안 React는 백그라운드에서 Virtual DOM 비교를 수행합니다.
새로운 상태가 기존 상태와 동일하다고 판단되면,
Pending이나 Skeleton은 이전 상태의 Complete보다 덜 최신 상태이기 때문에 필요하지 않은 상태로 간주되며
결과적으로 React는 렌더링을 건너뛰고 기존 Complete 상태, 현재 UI를 계속 보여줍니다.

이후 새로운 상태가 Complete 상태가 되면 새로운 Complete 상태가 기존 상태보다 최신이라고 판단하여
새로운 Complete 상태를 DOM에 반영하기 때문에
결과적으로 사용자에게는 새로운 Complete 상태가 즉시 나타나는 것처럼 보이며, 렌더링 작업을 최적화한 결과입니다.

결론

Concurrent Mode는 렌더링에서 최적화를 실현하는 핵심 기술입니다.
Fiber 아키텍처를 기반으로 렌더링 작업의 우선순위를 조정하고, 렌더링 작업을 분할하고 스케줄링하면서
기존 UI를 유지하기 때문에 작업 중단없이 자연스럽게 전환이 되어 사용자 경험을 개선할 수 있습니다.

앞으로 Concurrent Mode를 제대로 이해하고 활용해서 더 나은 프로덕트를 개발하고 싶습니다!
또 다양한 방법들이 있다고 하는데 공부해보도록 하겠습니다!!

기능주요 역할사용 방식
useTransition상태 업데이트 우선순위 낮춤startTransition으로 상태 업데이트 감싸기
useDeferredValue상태 값을 지연시키며 이전 값 유지값을 전달받아 지연된 값을 반환
Streaming SSR점진적으로 HTML을 제공서버-클라이언트 렌더링 최적화
Selective Hydration특정 부분만 클라이언트 하이드레이션필요 영역만 하이드레이션

또 React에서는 Concurrent Mode 때문에 Strict Mode를 활성화하는 것을 권장한다고 합니다!
렌더링 작업을 중단하고 재개하고, 우선순위를 조정할 수 있기 때문에 기존의 비동기 로직이 올바르게 동작하지 않을 가능성이 있어서 조기에 문제를 발견해서 안정적인 코드를 작성하기 위함이라고 합니다!!
거슬린다고 절대 지우지 말도록 합시다!!!!!!!!

StrictMode가 검사하는 항목들

  • 안전하지 않은 생명주기를 사용하는 컴포넌트 발견
  • 레거시 문자열 ref 사용에 대한 경고
  • 권장되지 않은 findDOMNode 사용에 대한 경고
  • 예상치 못한 side effect 검사
  • 레거시 context API 검사
  • 재사용 가능한 상태를 보장

문서들을 찾다보니까 옛날 글들이 많아서 정확하지 않을 수도 있습니다..🥲
혹시 틀린 부분이 있다면 댓글로 알려주세요!!🙇🏻🙇🏻

참고

https://github.com/facebook/react/blob/v19.0.0/packages/scheduler/src/SchedulerMinHeap.js
https://tecoble.techcourse.co.kr/post/2021-07-24-concurrent-mode/
https://velog.io/@tnghgks/React-Concurrent-mode
https://seungjoon-lee.oopy.io/6fc869e2-4983-4f0a-92ee-6b768a03499b
https://deview.kr/data/deview/session/attach/1_Inside%20React%20(%E1%84%83%E1%85%A9%E1%86%BC%E1%84%89%E1%85%B5%E1%84%89%E1%85%A5%E1%86%BC%E1%84%8B%E1%85%B3%E1%86%AF%20%E1%84%80%E1%85%AE%E1%84%92%E1%85%A7%E1%86%AB%E1%84%92%E1%85%A1%E1%84%82%E1%85%B3%E1%86%AB%20%E1%84%80%E1%85%B5%E1%84%89%E1%85%AE%E1%86%AF).pdf

profile
또이의 개발새발 개발일기

2개의 댓글

comment-user-thumbnail
2024년 12월 11일

아티클 잘 읽었습니다!
저는 전반적인 동시성 개념과 활용 방법 위주로 많이 다뤘는데, 어떻게 동작하는지 React 내부적으로도 공부를 하셔서 작성을 해주셨다니..! 반성을 많이 하게 되네요.

특히 이전 주제였던 Fiber 아키텍쳐가 이 Concurrent와 연관 되어 있다는 것을 설명을 해주시니 훨씬 이해가 잘 되었던 것 같아요! Fiber 기능의 목표였던 중단 가능한 작업을 나누고, 진행 중인 작업의 우선 순위를 정한다는 것이 결국 Concurrent Feature의 작업 스케쥴링을 위한 것이 아닐까 생각이 듭니다!! 아직 내부적인 코드를 봐도 완벽하게 이해가 가지는 않지만 이 부분은 먼저 구현된 기능을 잘 쓰는 방법을 배우고 궁금해지는 부분을 하나하나 뜯어가도 괜찮은 접근이 될 것 같습니다!

그리고 JND라는 개념을 처음 봤는데 이 관점으로 Suspense와 Concurrent를 설명해주신 것이 인상 깊었습니다. 확실히 예시를 보니 두 개를 같이 잘 쓰면 훨씬 좋은 UX를 끌어낼 수 있지 않을까 생각이 드는 것 같습니다! 수고하셨습니다 :)

답글 달기
comment-user-thumbnail
2024년 12월 11일

아티클 너무 잘 읽었습니다!
Transition으로 보는 렌더링 파트에서 과정을 이해하기 쉽게 설명해주셔서 좋았습니다! 덕분에 제가 그동안 pending 상태와 loading 상태를 명확히 구분하지 않았다는 사실을 깨닫게 되었습니다...😥 또한, 새로운 상태가 기존 상태와 동일하다고 판단되면, 렌더링을 건너뛰고 Complete 상태가 된다는 사실도 흥미롭네요.
또, JND라는 용어도 처음 알게 되었는데요. 물론 JND보다 큰 지연이 있더라도 Suspense를 사용하지 않는 경우도 있다고 하지만, 저 나름대로는 기준으로 삼을 수 있을 것 같습니다.
수고하셨어요!

답글 달기