동시성과 병렬성에 대해서, 그리고 React 가 말하는 동시성 까지도 알아보자.

동시성과 병렬성에 대해서, 그리고 React 가 말하는 동시성 까지도 알아보자.

동시성과 병렬성에 대해 공부하다가 무지의 늪에 빠져 죽을 뻔 했지만 겨우 살아나왔따.

·

13 min read

✋🏻 들어가며

지난번에 동시성과 병렬성에 대해 공부를 하다가, 타고 타고 타고 올라가다보니 하드웨어까지 올라갔었다. 🥸 하핫.. 그래서 이어서 계속 공부를 해봤다.

하드웨어의 구성과 동작 🖥️

사실 동시성과 병렬성을 공부하게 된 계기는 뭐.. 몰라서다 ^^! 특히나 리액트에서도 동시성 모드에 대한 지원에 대해 계속 이야기를 해왔었는데, 이 부분도 명확하게 설명할 수 없었기 때문에 이 참에 동시성과 병렬성 그리고 리액트에서의 동시성은 무엇인지 마저 공부를 해봤다.

내용이 너어ㅓㅓㅓㅓㅓ무 방대해서 줄이고, 줄이고, 줄였는데 여러번 복습하면서 계속 깊이를 늘려가보려고 한다. 🥲

동시성에 알아보기 전, 프로세스에 대해 조금 짚고 넘어가려 한다. 동시성과 병렬성에 대해 이야기를 할 때 Context Switching 에 대한 내용이 나오기 때문에, CS 바보인 나는 일단 이 부분에 대한 이해가 필요했다.


🖥️ 프로세스 (Process) 란?

프로세스란 쉽게 말해서 현재 실행중인 프로그램이다. 메모리에 올라와서 돌아가고 있는 프로그램의 인스턴스(독립 개체)라 할 수 있는데, 프로세스 내부에는 최소 하나의 스레드를 가지고 있고 이 스레드 단위로 스케줄링을 한다.

만약 내가 하드 디스크의 프로그램을 실행하면, 이를 위해 메모리 할당이 이루어지고 할당이 된 메모리 공간에 바니어리 코드가 올라간다. 이 때 부터 프로세스라고 부를 수 있다.

💾 Context

컨텍스트란 프로세스가 현재 어떤 상태에서 수행되고 있는지를 규명하기 위한 정보를 의미한다.

만약 어떤 명령에 의해 기존에 실행되던 프로세스가 다른 프로세스로 넘어갔을 때, 다시 돌아오기 위해서는 이전 프로세스가 어디까지 명령을 수행했는지에 대한 수행 시점과 상태에 대한 정보가 필요했는데 이런 것들을 컨텍스트라고 한다.

하드웨어 컨텍스트CPU 수행상태를 나타내는 것으로 프로그램 카운터(PC)와 각종 레지스터에 저장하고 있는 값
프로세스 주소공간코드, 데이터, 스택으로 구성된 프로세스만의 독자적인 주소 공간
커널상의 컨텍스트프로세스 관리를 위한 자료구조인 PCB(Process Control Block)과 Kernal Stack(커널 내부 주소)를 의미

📦 PCB, Process Control Block (프로세스 제어 블록) 운영체제가 시스템 내의 프로세스들을 관리하기 위해 프로세스마다 유지하는 정보들을 담는 커널 내의 자료구조를 의미

  1. pointer / process state / process number : 운영체제가 관리상 사용하는 정보 - 프로세스 상태, 프로세스 번호 / 스케줄링 정보, 우선순위

  2. program counter / registers : CPU 수행 관련 하드웨어 값 - 프로그램 카운터, 레지스터

  3. memory limits : 메모리 관련 - 코드, 데이터, 스택의 위치 정보

  4. list of open files : 파일관련 - 프로세스가 오픈한 파일 정보를 의미

🚦 State

  • New

    • 프로세스가 시작되어 그 프로세스를 위한 각종 자료구조는 생성되었지만 아직 메모리 획득을 승인받지 못한 상태
  • Ready

    • CPU만 보유하면 당장 명령을 수행 할 수 있도록 준비되어 CPU를 기다리는 상태
  • Running

    • 프로세스가 CPU 를 잡고 기계어 명령을 수행중인 상태
  • Blocked (waiting, sleep)

    • CPU를 할당받더라도 당장 명령을 실행할 수 없는 상태

    • I/O 등의 event를 (스스로) 기다려야 하거나 디스크에서 file을 읽어와야 하는 경우가 봉쇄상태

    • 예시

      • 디스크에서 file 을 읽어와야 하는 경우
  • Terminated

    • 프로세스가 종료되었으나 운영체제가 그 프로세스와 관련된 자료구조를 완전히 정리하지 못한 상태

    • 수행 (execution) 이 끝난 상태

  • Suspended (stopped)

    • 외부적인 이유로 프로세스의 수행이 정지된 상태이며 프로세스는 통째로 디스크에 swap out 됨

    • 예시

      • 사용자 프로그램을 일시 정지시킨 경우 (break key)

      • 시스템이 여러 이유로 프로세스를 잠시 중단(메모리에 너무 많은 프로세스가 올라와 있을 때)

♻️ Context Switch

시분할 시스템에서 프로세스의 CPU 점유가 끝이나면 다른 프로세스로 CPU를 넘겨주게 되고 이 때 문맥 교환이 발생한다. 즉 Context Switch는 한 프로세스에서 다른 프로세스로 CPU의 제어권을 넘겨주는 과정이다.

문맥교환이 발생하며 CPU가 다른 프로세스에게 넘어갈 때 운영체제는 다음을 수행한다.

  1. CPU를 내어주는 프로세스의 문맥(context)를 그 프로세스의 PCB에 저장

  2. CPU를 새롭게 얻는 프로세스의 문맥(context)을 PCB로부터 읽어 실제 하드웨어로 복원

📆 Process Scheduler

🚌 프로세스를 스케줄링하기 위한 큐

운영체제는 하드웨어와 소프트웨어 리소스를 Queue를 두어 사용한다. 프로세스들은 각 Queue들을 오가며 수행된다.

1️⃣ Job queue

  • 현재 시스템 내에 있는 모든 프로세스의 집합

2️⃣ Ready queue

  • 현재 메모리 내에 있으면서 CPU를 잡아서 실행되기를 기다리는 프로세스의 집합

3️⃣ Device queues

  • I/O device의 처리를 기다리는 프로세스의 집합

📝 스케줄러(Scheduler)

스케줄러란 어떤 프로세스에게 자원을 할당할지를 결정하는 운영체제 커널의 코드를 의미한다. 스케줄러는 그 역할과 목적에 따라 단기, 중기, 장기로 구분된다.

🛺  Short-term scheduler(단기 스케줄러 or CPU scheduler)

단기 스케줄러는 미리 정한 스케줄링 알고리즘에 따라 실행할 프로세스를 선택한다.

  • CPU와 메모리 사이의 스케줄링을 담당하는데 프로세스에 CPU를 할당하는 역할

  • ready queue에 있는 프로세스들 중 다음에 running 시킬 프로세스를 결정

중기 스케줄러와 장기 스케줄러는 한정된 메모리에서 많은 프로세스를 처리해야하는 경우에 사용되는데, 현대의 OS 는 하드웨어의 발전으로 인해 장기 스케줄러는 사용하지 않고 중기 스케줄러를 사용한다.

🚗  Medium-Term Scheduler(중기 스케줄러 or Swapper)

  • 현 시스템에서 메모리에 너무 많은 프로그램이 동시에 올라가는 것을 조절하는 역할

    • 프로세스에게서 메모리를 빼앗음
  • 여유 공간을 위해 프로세스를 통째로 메모리에서 디스크의 swap 영역으로 저장 (swap out)

    • block 상태에 있는 프로세스가 0 순위로 swap out
  • 프로세스 status : ready → suspended

🚅  Long-term scheduler(장기 스케줄러 or job scheduler)

  • 시작 프로세스 중 어떤 것들을 ready queue로 보낼지 결정

  • 프로세스에 memory(및 각종 자원)을 주는 문제

  • time sharing system에는 보통 장기 스케줄러가 없음 (무조건 ready)

  • 프로세스 상태도에서 admitted 해주는게 장기스케줄러의 역할


Concurrency & Parallelism

Concurrency(동시성)과 Parallelism(병렬성)을 이해하기 위해 하나씩 파보다보니 결국 하드웨어 까지 올라갔었다. 이제 어느정도 이해를 했으니 이 두 가지 개념으로 돌아와서 다시 살펴보려 한다.

‘동시’와 ‘병렬’이라는 단어가 너무나 비슷한 개념처럼 보여서 헷갈릴 수 있는데, 이 참에 제대로 구분을 하고 가보자.

Group 238.png

1️⃣ Parallelism, 병렬성

병렬성은 ‘물리적으로 구현이 되어있다는 점’에 중심을 두면 이해가 편할 것 같다. 프로세서 하나에 코어가 여러개가 있을 때, 즉 프로세서가 코어에 일을 분배하는데 이 코어들이 여러개가 있다면 순서대로 일을 하는 것이 아니라 동시에 작업 수행이 가능하다. 바로 이것이 Parallelism 이다.

2️⃣ Concurrency, 동시성

여러 작업이 있을 때, 그 작업의 일부만 수행을 하고 다음 작업을 하고.. 또 다시 일부만 수행하고 다음 작업을 수행하고 하는 식으로 진행이 된다. 즉 여러가지 일이 진행되지만 동시에 진행되는 것은 아니고 쫌쫌따리로 나눠서 진행한다. 그럼 어떻게 쫌쫌따리로 진행하냐?

CPU가 작업마다 시간을 분할해서 적절하게 Context Switching을 하게 된다. 프로세스에 대해 공부했을 때, Context Switching 이란 프로세스에서 다른 프로세스로 CPU의 제어권을 넘겨주는 과정이라고 했었다.

동시성의 관점에서 봤을 때, Context Switching 은 하나의 task 가 끝나는걸 기다리는 것이 아니라, 여러 개의 작업을 전환할 수 있도록 한다. 만약 인터럽트가 발생하게 된다면 하나의 프로세스에서 다른 프로세스로 CPU 제어권을 넘겨주는 것이다.

이전 프로세스의 상태를 PCB 에 저장해 보관하고, 새로운 프로세스이 PCB 를 읽어서 보관 상태를 복구하는 작업이 이루어지며, 이 때 CPU 는 아무 일도 하지 않아 잦은 컨텍스트 스위칭은 성능 저하를 일으킬 수 있다.

📝 정리해보자

병렬성은 물리적으로 구현이 되었으며, 코어라는 일꾼이 여러개가 있고 본인이 할당받은 태스크들을 ‘동시’에 수행해나간다.

동시성은 하나의 일꾼이 본인이 수행중이던 태스크를 수행을 하다가 다른 것을 수행하고 하는 식으로 와리가리(..?)를 치며 동시에 일어나는 듯이 보이게끔 하는 것이다.

그럼 리액트에서의 동시성에 대해서도 알아볼까!


⚛️ 리액트에서의 동시성.

리액트 공식문서를 보다보면 “동시성” 에 대해 이야기를 하는데, 그냥 참고만 하고 넘어가다가 이번 기회에 이것이 무엇을 의미하고 어떻게 구현이 되었는지를 찾아봤다.

React 18 이 나오면서 experimental 로만 지원하던 concurrent mode 가 공식적으로 지원되었는데, 이를 사용하기 위해 useTransitionuseDeferredValue 훅도 추가되었다. 전반적으로 같이 알아보자.

📝 리액트가 말하는 Concurrency.

동시성은 여러가지 일이 진행되지만 동시에 진행되는 것은 아니고 쫌쫌따리로 나눠서 진행되는 것이라고 했다. 그럼 리액트가 말하는 동시성도 이와 같을까?

리액트 공식문서가 말하길, 리액트에서는 여러 버전의 UI 를 동시에 보일 수 있도록 도와주는 메커니즘이라고 한다. 그럼 UI 업데이트에 대한 관점에서의 동시성을 의미하는걸까?

🏃🏻‍♂️ Blocking Rendering.

리액트 18이 나오기 전, 리액트는 조금의 어려움을 가지고 있었는데 바로 Blocking Rendering 이다. 말 그대로 렌더링이 블락되고 있는 현상인데, 조금 더 설명을 보태면 많은 화면을 업데이트하는 경우 렌더링 동안 페이지가 그려지는 것에 지연되는 현상이 발생하는 것이다.

특히나 Form 이나 List 를 다루면서 input 에서의 이벤트를 다루다보면 그 문제를 많이들 경험했을 것이다. input 태그에서 state 가 여러번 변경이 되다보면 해당 state 와 연관된 화면들은 state가 변경된 만큼 출력이 일어날 것이기 때문에 성능이 떨어지는 문제가 발생하게 된다.

음 그럼 위와 같은 현상이 발생할 때, 리액트가 말하는 동시성이 구현이 된다면 화면이 출력되는 것에 밀림이 없이 업데이트가 동시에 이뤄지는 것으로 출력이 될까?

🍎 이 부분은 아래 블로그를 주로 보며 공부한 내용이다. 개인적으로 이해한 내용들을 간추린 것으로 자세한 내용은 아래 포스팅을 참고하자. (톺아보기 시리즈 너무너무너무 좋아유..)

React 18 톺아보기 - 03. Transition Lane_2 | Deep Dive Magic Code


♻️ Expiration Time & Lane

리액트에서 렌더링과 관련해서 두 가지 모델에 대해 살펴볼텐데, Expiration Time 과 Lane 이다. Lane 모델은 리액트에서 렌더링의 동시성을 구현하기 위해 새롭게 도입된 모델이다. Lane 도입 전에는 Expiration Time 모델을 사용했다.

⏳ Expiration Time

Expiration TimeSync, Idle, Batched 와 중요도에 따라 계산된 값에 따라 우선순위가 정해진다. 숫자가 클수록 우선순위가 높고, Sync 또는 만료된 Expiration Time 이 가장 우선순위가 높다.

우선순위를 기반으로 Expiration Time 을 계산하는 방법은 렌더링 모드에 따라 아래와 같이 결정된다.

Expiration Time 에서는 렌더링 대상의 우선순위를 기준으로 같거나 큰 업데이트만 렌더링 리스트에 포함되어 배치처리가 된다.

서브 트리의 우선순위(childExpirationTime)가 현재 렌더링 중인 우선순위(renderExpirationTime)보다 낮으면 null 을 반환해 하위 트리로 렌더링 작업이 진행되지 않도록 한다.

📝 Expiration Time 에서의 업데이트 개념 Expiration Time 에서는 두 가지 개념이 하나의 ⏳ 시간 데이터에 존재한다.

1️⃣ 우선 순위 : 업데이트를 발생시킨 이벤트를 기준으로 우선순위를 결정하고 업데이트 간 우선순위는 대소 비교를 통해 판단
2️⃣ 배치 여부 : 업데이트의 배치 여부는 값의 대소비교를 통해 판단

만약 A > B > C의 우선순위가 주어지면 A에 대한 작업 완료 없이는 B에 대한 작업을 수행할 수 없다. 마찬가지로 A와 B를 모두 완료하지 않고 C로 넘어갈 수 없다.

🛣️ Lane - Lane 모델을 통해 해결하고자 하는 것 🤷🏻‍♀️

Expiration Time은 사실상 큰 문제는 없었다. 다만 Suspense 가 도입이 되면서 작업순서 결정에 있어 새로운 기준점이 생기며 고려해야하는 사항들이 생겨버렸다. 바로 렌더링의 IO-Bound 라는 개념이다.

🍡 CPU Bound & IO Bound

1️⃣ CPU Bound : 일반적인 모든 렌더링 (CPU에 의존적)
2️⃣ IO Bound : 네트워크와 전환이 결합된 경우의 렌더링

만약 CPU - IO - CPU 순서로 렌더링 작업이 진행될 경우, 우선순위가 높은 IO-Bound 렌더링이 상대적으로 우선순위가 낮은 두 번째 CPU-Bound 렌더링 작업을 차단하는 경우가 발생하게 되었다.

상대적으로 IO-Bound는 CPU-Bound보다 느릴 수 밖에 없기에 우선순위를 가진 전환 렌더링이라도 IO-Bound 보다는 CPU-Bound를 먼저 처리하는 게 유리하다.

Expiration Time 은 이벤트 우선순위와 업데이트 발생 시점을 기준으로 시간 데이터를 계산하여 사용한다. 짧은 간격으로 B - A의 버튼이 클릭 된 경우, 같은 전환 우선순위를 기반으로 Expiration Time이 계산된다.

여기서 IO - CPU의 시간 범위를 지정하고 구분할 수 있어야 하는데, 다음과 같이 범위를 나타낼 수는 있지만, 해당 범위에서 특정 범위(일련의 범위에서 IO-Bound에 해당하는 Expiration Time)를 제거할 수는 없다.

리액트 18의 렌더링은 여러 요소(업데이트 종류, Suspense, Hydrate)에 따라서 렌더링 방식을 다르게 가져간다. 여기에 맞춰서 업데이트를 보다 세부적으로 관리할 수 있어야 한다.

이를 위해 Lane은 두 가지 개념(업데이트 간의 우선순위, 업데이트 배치 여부)을 분리하여 관리할 수 있도록 구상되었다.

🏃🏻‍♀️ 우선 순위가 더 높은 업데이트 우선 처리.

사용자 액션의 응답이 가장 중요하므로 렌더링 중이라도 우선순위가 더 높은 업데이트가 발생한다면 현재의 렌더링을 중단하고 우선순위가 더 높은 업데이트를 기준으로 다시 렌더링을 진행할 수 있어야 한다. 이 과정에서 여러 업데이트 중에 발생 시점과 상관없이 적절히 우선순위를 판별하고 처리할 수도 있어야한다.

Lane은 시간에 의존적인 친구가 아니다. 렌더링 순서가 변경되어도 여러 업데이트를 기준에 따라 정확하게 구분할 수 있다.

🛣️ Lane - 불필요한 렌더링 건너뛰기 🏇🏻

리액트 18에서는 Lane을 통해 오래된 렌더링을 폐기, 중간 상태를 건너뛰고 최신 상태의 렌더링을 다시 수행할 수 있다.

📝 정리해보면,

| Expiration Time | - 이벤트 우선순위와 업데이트 발생 시점을 기준으로 시간 데이터를 계산하여 사용
- 만약 우선순위가 주어지면 이전 작업에 대한 완료 없이는 이후의 것들은 블락
| | --- | --- | | Lane | - 업데이트 간의 우선순위, 업데이트 배치여부를 분리해 관리하며 시간에 비의존적
- 불필요한 렌더링을 건너뛸 수 있음 |

키워드는 시간에 의존적인가.. 이려나..! Lane 에 대해 조금 더 알아보자.


🛣️ Lane

Lane 모델은 리액트에서 렌더링의 동시성을 구현하기 위해 새롭게 도입된 모델이다. Lane 도입 전에는 Expiration Time 모델을 사용했다. Lane은 리액트 내부에서도 단어 그대로 차선의 개념으로 이해할 수 있는데 우리 고속도로에 여러 개의 차선이 있듯 Lane 또한 여러 종류가 있다. Lane의 종류는 다음과 같다.

*아래 코드는 리액트 reconciler 의 ReactFiberLane 의 한 부분이다.

각 레인을 그림으로 표현하자면 아래와 같다.

👮🏻‍♂️ 업데이트가 발생했을 때 Lane 배정

리액트에서 업데이트가 발생하면 업데이트의 종류에 따라 Lane 이 할당된다.

Reconciler(렌더링 모듈)는 현재 렌더링 대상인 renderLanes를 들고 있고 해당 Lane 위에 올라가 있는 업데이트들이 배치처리 된다.


📊 이벤트와 Lane 의 우선순위.

업데이트에는 종류가 있는데, 이것을 분류하는 기준은 업데이트의 시작점이고 이것들은 어떠한 이벤트들이다. 이벤트에 따른 Lane 들의 분류와 우선순위를 살펴보자.

LaneEventPriority
SyncLane사용자의 물리적 행위 중 개별적으로 처리해야 하는 이벤트 (DiscreteEvent)

- click, input, mouse down, submit 등 | 1 | | InputContinuousLane | 사용자의 물리적 행위 중 연속적으로 발생하는 이벤트 (ContinuousEvent)
- drag, scroll, mouse move, wheel 등 | 2 | | DefaultLane | 기타 모든 이벤트, 리액트 외부에서 발생한 업데이트
- setTimeout, Promise 등 | 3 | | TransitionLane | 개발자가 정의한 전환 이벤트
- startTransition(), useTransition()를 통해 생성된 업데이트 | 4 |

각 이벤트에 대응하는 Lane 에 대한 소스코드를 살펴보자.

분류 표에서 봤듯이 각 이벤트들은 우선순위가 있고 이것들은 각각의 Lane 마다 대응이 된다. DOM 이벤트들에 대한 priority 가 부여가 되는 것도 코드를 보며 알아보자.

input 이벤트는 개별적으로 처리되는 이벤트로 DiscreteEvent 이며, 그렇기 때문에 getEventPriority 메서드는 DiscreteEventPrioroty 를 반환한다. wheel 이벤트는 연속적으로 처리되는 이벤트이기 때문에 ContinuosEventPriority 가 반환된다.

👮🏻‍♂️ 업데이트에 Lane 할당하기

업데이트에 Lane을 할당하는 시점은 useState() 훅의 setter를 사용해 상태를 변경할 때이다.

위 소스 코드를 보면 requestUpdateLane() 이 업데이트의 우선순위를 찾아주는데, 아래의 환경들을 순차적으로 탐색하는 과정에서 우선순위를 찾는다.

각각의 이벤트에서 어떻게 우선순위를 찾아가는지 살펴볼까?

1️⃣ DOM 이벤트

DOM 이벤트의 분류에 따라 리액트 내부적으로 구축한 이벤트 시스템이 있는데, 이 시스템 위에서 아래와 같이 핸들러가 구성되어 있다.

requestUpdateLane() 는 위 핸들러에서 setCurrentUpdatePriority() 가 설정해둔 우선순위를 참조한다.

2️⃣ 리액트 외부 이벤트

만약 리액트 시스템의 외부에서 이벤트가 발생한다면, 리액트의 내부 핸들러는 우선순위를 설정할 수 없다. 이 케이스에서 리액트는 호스트 시스템으로 이벤트를 확인하고, 만약 해당하지 않는다면 DefaultEventPriority 를 할당하게 된다.

3️⃣ 전환 이벤트

Transition Lane을 할당할 업데이트는 useTransition(),startTransition() API를 통해서 개발자가 결정한다. 해당 API를 통해서 생성된 업데이트는 내부적으로 특별한 플래그를 세우게 된다.

ReactCurrentBatchConfig는 리액트 내부에서 전역적으로 사용되는 객체인데, 해당 객체를 업데이트 생성 시점에 참조해 업데이트가 전환 업데이트인지 판단한다.

Transition Lane(+ Retry Lane)은 다른 Lane들과는 다르게 Lane이 여러 개인데, 그 때문에 전환 업데이트는 생성 시점에 따라 같은 전환 업데이트라도 다른 Transition Lane에 할당될 수 있다.

🚦 렌더링을 진행할 Lane 선택하기

리액트 18 부터는 우선순위가 낮은 업데이트와 높은 업데이트를 따로 렌더링하는데, 🔍 getNextLanes() 를 활용해서 렌더링을 대기하고 있는 Lanes 중 우선순위가 가장 높은 아이를 찾아낸다.

이 과정에서 렌더링 중이던 Lane보다 우선순위가 더 높은 Lane이 뽑힐 수도 있다(Interrupt). 그렇게 된다면 이전 렌더링 작업은 폐기 처리되고 신규 Lane을 기준으로 렌더링을 다시 시작한다.

그럼 렌더링 작업을 진행할 Lanes를 어떻게 뽑아낼까?

우선 순위 체크를 위해서 해당 시점에서 렌더링이 밀려있는 아이들(렌더링이 보류된 상태인 Lanes)을 먼저 찾아야겠지. 이것들은 root(VDOM에서의 전역 컨텍스트)를 통해 참조한다. (root.pendingLanes)

suspendedLanespingedLanes도 확인 하는데, 이것들은 Suspense 와 연관있다.

| suspendedLanes | - IO-Bound인 렌더링에서 보류 처리된 Lanes
*네트워크 요청으로부터 데이터가 아직 도착하지 않은 상태 | | --- | --- | | pingedLanes | - 보류된 상태가 해결되었지만 아직 렌더링을 진행하지 못한 Lanes
*네트워크 요청이 완료되어 데이터가 준비된 상태 |

렌더링이 보류된 친구들에 대한 확인이 다 되었다면, 이 중에서 렌더링 작업에 포함하지 않을 Lane들을 제거한다. 기본적으로 suspendedLanes 는 제외된다. (데이터가 아직 도착 안했으니..!?)

getHighestPriorityLanes()는 현재 보류 중인 Lanes에서 우선순위가 가장 높은 Lane 을 반환하는데, 이 때 해당 Lane 그룹에 속하는 보류 중인 Lanes도 배치처리하기 위해 함께 반환한다.

getHighestPriorityLanes() 도 한 번 살펴볼까?

만약 lanesTransitionLane1, TransitionLane2, RetryLane1이 포함되어 있다면 가장 우선순위가 높은 TransitionLane1이 선택되고 반환될 때는 같은 그룹인 Transition Lane2도 포함하여 반환된다.

여기서 nextLanesNoLanes라면 보류 중인 Lanes 중 현재 렌더링을 진행해야 할 Lane이 없다는 의미다. NoLanes를 반환하여 렌더링 작업을 진행하지 않도록 한다.

wipLanes는 동시성 렌더링에서 현재 렌더링 중인 Lanes을 저장하는 변수인데, 해당 변수가 있다는 건 렌더링이 진행 중이었다는 의미이다.

그렇기 때문에 ‘어, 지금 렌더링이 진행 중인데 이전 렌더링보다 우선순위가 높은 업데이트가 있는지 찾아볼까?’ 하며 확인해보고 Interrupt (우선순위가 더 높은 Lane이 뽑히는 것)가 필요한지 결정하게 된다.

이전 렌더링을 계속 이어갈 것이라면 wipLanes를 반환한다.(계속 진행중이라는 뜻을 담아야하니!) 하지만 만약 렌더링 중이었고 nextLanewipLane 보다 우선순위가 더 높다면 이전 렌더링을 Interrupt 한다.

최종적으로 nextLanes를 반환하며, Reconciler 는 반환받은 Lanes를 기반으로 VDOM을 탐색하며 렌더링을 진행한다. 이 과정은 root에 pendingLanes가 더 이상 없을 때까지 반복된다.

아주 간단하게 정리해볼까?

  1. 렌더링 과정의 진행

  2. 렌더링 중간에 이벤트가 발생하면 각각의 작업을 레인에 배정

  3. 낮은 순위 렌더링을 멈추고 높은 순위 렌더링과 페인팅을 수행

  4. 펜딩 상태의 낮은 순위 렌더링을 리베이스

오… 이제야 조금 리액트가 동시성 모드를 어떤식으로 구현했는지 감이 오는 것 같다. 그럼 이 동시성 모드를 활용할 수 있도록 제공한 두 가지 훅의 간단한 Usage 를 알아보고 마무리해보자.


🪝 Hooks

1️⃣ useTransition

상태 변화의 우선순위를 지정하기 위한 훅으로, isPendingstartTransition 을 반환한다.

  • isPending : 상태 변화가 지연됨을 확인할 수 있는 boolean

  • startTransition : 상태변화를 유발하는 콜백함수를 내부에 전달하는데, 이렇게 전달되고나면 해당 콜백함수 내부의 상태 변화는 그 우선순위가 낮아진다.

훅에 대한 사용 예시는 아래 코드를 참고하자.

2️⃣ useDeferredValue

useDeferredValue 또한 상태 변화의 우선순위를 지정하기 위한 훅이다. 그럼 useTransition 과는 어떤 차이가 있을까? 사용 예시 코드를 보면 그 차이를 확인할 수 있다.

useTransition 은 setter 함수가 포함된 함수를 콜백함수로 넘겨서 우선순위를 낮춘 반면, useDefferedValue 는 우선순위를 낮추고 싶은 “값” 을 훅의 인자로 넘겨준다.

Dan Abramov 에 따르면 useDeferredValue는 상태를 props로 받는 등 제어할 수 없을 때 사용하는 것을 권장하고 있다.

훅에 대한 사용 예시는 아래 코드를 참고하자.


👩🏻‍🌾 마무리

뭔가..ㅋㅋㅋ 브라우저 렌더링 이후로 정말 오래 붙잡고 들여다본 것 같다. 휴.. 그래도 덕분에 전반적으로 병렬성과 동시성에 대해서도 이해할 수 있었고, 리액트에서의 동시성 모드에 대해서도 알 수 있었다.

이번에 이 부분을 공부하면서 내가 사용하던 것들이 타고타고 올라가면 결국은 기본적인 CS 지식이 있으면 더 이해가 쉬워진다는 걸 느낄 수 있었다. 휴.. CS 지식이 조금 더 탄탄했으면 좋겠다는 욕심도 들었었고, 내가 사용하는 것들에 대해서도 그냥 지나치지말고 욕심있게 깊게 들여다봐야겠다는 생각도 했다.

이번에 공부한 것들이 100% 머리에 다 들어오고 다 이해했으면 좋겠건만 생각보다 내용이 정말 방대하고 내가 모르는 부분들이 정말 많아서, 수시로 복습하면서 내가 이번에 못봤던 부분들에 대해서도 공부해보고 쌓아나가야겠다.


🗓️ 참고.

https://nesoy.github.io/articles/2018-09/OS-Concurrency-Parallelism

https://cerulean85.tistory.com/558

https://hongong.hanbit.co.kr/컴퓨터의-4가지-핵심-부품cpu-메모리-보조기억장/

https://codingwanee.tistory.com/entry/전산기초-주기억장치-보조기억장치

https://goidle.github.io/react/in-depth-react18-transition_1/

https://deview.kr/data/deview/session/attach/1_Inside React (동시성을 구현하는 기술).pdf

https://tecoble.techcourse.co.kr/post/2023-07-09-concurrent_rendering/

https://goidle.github.io/react/in-depth-react18-transition_2/#1-업데이트의-개념-분리