그동안 React의 내부 동작을 깊이 있게 다룬 리액트 톺아보기 시리즈도 여러 번 읽고 React 소스코드도 살펴보았지만, 세부적인 구현을 완벽히 이해하기란 쉽지 않았습니다. 그래서 이번 포스팅에서는 너무 깊은 구현 상세보다는, React 18에서 동시성의 큰 흐름을 정리해보고자 합니다.
동시성의 핵심은 더 나은 사용자 경험을 위한 화면 전환과 높은 응답성 유지에 있습니다.
이를 가능하게 하는 핵심 메커니즘인 Lane 모델을 중심으로 알아보겠습니다.
업데이트의 이해
React에서 발생하는 업데이트는 종류와 중요도가 다양합니다. 일상적으로 접하는 몇 가지 상황을 통해 이해해보겠습니다.
-
검색어 자동완성
- 검색어 입력 - 사용자가 입력하는 텍스트는 즉시 화면에 표시되어야 함
- 추천 검색어 목록 - 입력된 텍스트를 기반으로 서버에서 데이터를 받아와 표시
- 일정 시간 내에 추천 검색어가 표시되지 않으면 사용자 경험이 저하됨
-
SNS 게시글 작성
- 텍스트 입력 - 사용자가 입력하는 내용이 즉시 화면에 표시되어야 함
- 해시태그 추천 - 입력한 '#' 다음의 텍스트를 기반으로 추천 목록 표시
- 이미지 미리보기 - 첨부한 이미지의 썸네일 생성과 표시
-
쇼핑몰 상품 필터링
- 필터 선택 UI - 체크박스나 라디오 버튼의 상태는 즉시 변경되어야 함
- 상품 목록 업데이트 - 수천 개의 상품 중 조건에 맞는 상품을 필터링하고 정렬
- 가격 범위 설정 - 슬라이더 움직임은 부드럽게, 실제 상품 필터링은 적절한 타이밍에
위의 사례에서 볼 수 있듯이, 업데이트는 크게 두 가지 특성으로 분류할 수 있습니다.
-
즉각적인 반응이 필요한 업데이트
- 사용자의 직접적인 조작에 대한 반응
- 지연되면 "버벅임"을 느끼게 되는 업데이트
- 예시:
- 검색창에 타이핑하는 텍스트 표시
- 게시글 작성 시 텍스트 입력
- 체크박스 선택/해제 상태 변경
- 슬라이더 핸들 이동
-
지연 가능한 전환 업데이트
- 현재 화면에서 다른 상태로의 전환
- 계산이나 데이터 처리가 필요한 무거운 업데이트
- 예시:
- 검색어 추천 목록 표시
- 해시태그 추천 목록 업데이트
- 쇼핑몰 필터링된 상품 목록 표시
- 이미지 썸네일 생성과 미리보기
업데이트 처리의 문제점
기존의 방식(Expiration Time)에서는 이러한 업데이트들이 모두 동일한 우선순위로 처리되었습니다. 모든 작업이 동시에 처리되려다 보니 입력이 버벅거리는 현상이 발생했습니다.
Expiration Time 모델의 한계
React 18 이전에는 Expiration Time 모델을 사용했습니다. 이 모델은 다음과 같은 한계를 가지고 있었습니다.
-
단일 시간 데이터의 한계
- 우선 순위와 배치 여부가 하나의 시간 데이터에 함께 존재
- 단순한 대소 비교만으로 우선순위와 배치 여부를 결정
-
Suspense와의 호환성 문제
- Suspense 등장 이전에 설계되어, 우선순위 외의 이유로 작업 순서를 결정하는 유연성 부족
-
IO-Bound와 CPU-Bound 작업의 충돌
- IO-Bound 작업(예: 데이터 페칭)은 외부 리소스 대기 시간이 있어 완료까지 시간이 더 걸림
- IO-Bound의 경우 CPU-Bound가 느릴 수 밖에 없는데 짧은 간격으로 IO-Bound 이후 CPU-Bound가 발생할 경우 IO - CPU 시간 범위를 지정하고 구분할 수 있어야하는데 불가능
Lane 모델
이러한 한계를 해결하기 위해 React 18에서는 Lane 모델을 도입했습니다.
업데이트 우선순위
Lane은 비트 필드로 구현되어 있으며, 다음과 같은 우선순위를 가집니다.
-
SyncLane
- 가장 높은 우선순위
- 개별적 물리 이벤트 (click, input, submit)
- 긴급한 에러 처리
-
InputContinuousLane
- 연속적 물리 이벤트 (drag, scroll)
-
DefaultLane
- 일반적인 비동기 업데이트
- 외부 이벤트 (setTimeout, Promise)
-
TransitionLane
- 개발자가 정의한 전환 이벤트
- startTransition, useTransition 사용
-
기타 Lanes
- RetryLane: 에러 복구
- SelectiveLane: 선택적 하이드레이션
- IdleLane: 유휴 시간 작업
Lane의 동작 방식
-
인터럽트 처리
- 현재 렌더링 중인 Lane보다 높은 우선순위의 업데이트가 발생하면 인터럽트 발생
- wipLanes를 통해 현재 진행 중인 렌더링 추적 (인터럽트가 필요한지 결정을 위해 필요)
-
배치 처리
- 동일한 우선순위(같은 Lane)의 업데이트는 자동으로 배치 처리
-
Time Slicing
- 우선순위에 따라 작업을 나누어 비동기적으로 처리
- 브라우저의 메인 스레드를 주기적으로 양보하여 다른 작업(사용자 입력 등) 처리 가능
Suspense와 Transition의 조합
-
Fallback 처리 최적화
- Suspense만 사용할 경우 데이터 로딩 시 즉시 fallback이 표시됨
- Transition과 함께 사용하면 불필요한 로딩 상태 깜빡임 방지
-
사용자 경험 개선
- 기존 컨텐츠를 유지하면서 새로운 데이터를 준비
- 준비가 완료된 후에만 새로운 컨텐츠를 표시
- 전환 중에도 UI의 응답성 유지
동시성을 위한 핵심 요구사항
-
렌더링 제어
-
렌더링 독립성
- 렌더링 간 의존성 없음
- 멱등성 보장
- current와 workInProgress 더블 버퍼링
-
브라우저 차단 방지
- 렌더 페이즈: 비동기 처리
- 커밋 페이즈: 동기 처리
- 효율적인 작업 스위칭
마무리
Lane이 어떻게 우선순위 기반의 렌더링을 가능하게 하는지, 정리하며 어느정도 큰 그림은 감을 잡은 듯 하네요. 세부 구현을 파악하기에는 아직 갈 길이 멀지만요 ㅠㅠ
항상 소스 코드를 분석하다가 방대한 양에 지레 겁먹고 흐지부지 넘어간 것 같는데 곧 React 19의 안정된 버전이 나오면 다시 한 번 소스 코드 분석 도전!!!
참고
https://goidle.github.io/
https://ko.react.dev/reference/react/Suspense#preventing-already-revealed-content-from-hiding