쉽게 떠먹여주는 React 18 업데이트

Perfume·2022년 6월 29일
25

✨ 들어가기 전에

Reactathon에서 Shruti Kapoor씨의 <Everything you need to know about React 18>라는 발표를 듣고 이 글을 작성하게 되었습니다. 이 포스트는 이번 업데이트로 React가 어떻게 달라졌는지, 이 변화가 어떤 걸 의미하는지 정리하는 글입니다. Suspense의 경우, SLASH 21에서 박서진님의 우아하게 비동기 처리하기 라는 발표 영상을 토대로 정리했고, 해당 영상이 이 글에 많은 도움이 되었습니다.

🤼‍♀️ React 18의 핵심은 "Concurrency"

React팀은 React 18을 발표하면서, 이번 릴리즈는 렌더링 엔진 개선과 사용자 경험 향상에 집중했다고 말했습니다. 어떻게 렌더링 엔진의 성능을 개선시키고, 사용자 경험을 향상시켰을까요? 바로 React에 동시성(Concurrency)을 도입한 것입니다.

동시성(Concurrency)이 뭘까요?

이름 그대로 순서에 상관없이 동시에 수행될 수 있다는 뜻입니다. 좀 더 프로그래밍적인 개념으로 의미를 좁히면, 프로그램을 독립적으로 실행될 수 있는 여러 조각으로 나누어서 구조화하는 방식이라고도 할 수 있겠습니다. 자바스크립트는 여러분도 아시다시피 싱글 스레드 언어입니다. 싱글스레드 언어는 하나의 작업을 수행할 때 다른 작업을 동시에 수행할 수 없습니다. React도 JavaScript를 기반으로 하기 때문에 동일합니다. 여기까지 읽고 "이건 동기와 비동기 이야기 아닌가요?" 라는 의문이 들었을 수도 있습니다. 동시성(Concurrency)과 비동기(Asynchronous)는 비슷한 개념처럼 보이지만 다릅니다.

비동기는 결과를 기다리지 않고 바로 다음 작업을 실행할 수 있게 하는 방식입니다. 보통 메인스레드에서 작업을 다른 스레드로 분산처리시키고 그 작업이 끝나길 기다리지않고 다음 작업을 생성하는 식이죠. 그러나 동시는 싱글 코어(멀티 코어에서도 가능)에서 멀티스레드를 동작시키기 위한 방식으로, 멀티 태스킹을 위해 여러 개의 스레드가 번갈아 가면서 실행되는 방식을 말합니다.

즉, 동기와 비동기가 작업을 보내는 시작점에서 작업을 기다릴지 말지에 관한 개념이라면, 동시성은 여러 작업을 한꺼번에 다루는 것과 관련된 개념입니다.

🔖 concurrent mode

concurrent mode를 사용하면, 이름처럼 여러 작업을 동시에 할 수 있게 됩니다. 자바스크립트는 싱글 스레드 언어인데 그게 어떻게 가능할까요? 바로 앞에서 이야기했던 "동시성"을 도입했기 때문입니다.

동시성은 여러 작업을 작은 단위로 나눈 뒤, 그들 간의 우선순위를 정하고 그에 따라 작업을 번갈아 수행하는 방법이다. 서로 다른 작업들이 실제로 동시에 수행되는 것은 아니지만, 작업 간의 전환이 매우 빠르게 이루어지면서 동시에 수행되는 것처럼 보이는 것이다. -React 사용자 경험 개선기

실제로 스레드는 한 개이지만, 여러 스레드를 번갈아가며 실행하기 때문에 멀티 스레드처럼 동작시킬 수 있습니다.

concurrent mode의 동작 원리는 다음과 같습니다.

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

💃 concurrent mode 사용하기

concurrent mode를 사용하기 위해선 render 대신 createRoot를 사용하기만 하면 됩니다. (업데이트 이후에) CRA로 프로젝트를 시작했다면, index.tsx 파일이 자동으로 render 대신 createRoot로 생성되었을 거에요.

🔖 Automatic Batching

렌더링 최적화를 위한 또 하나의 신규 업데이트는 Automatic Batching, 즉 자동 배칭입니다. 사실 기존에도 React는 자동 배칭을 해왔습니다. 그러나 이벤트 핸들러 밖에서 발생하는 업데이트를 배칭하지 않아서 일관적이지 못했습니다. 자동 배칭의 경우 신규 기능이 아니라 기존에 존재하던 기능의 업데이트로 이해하시면 좋을 것 같습니다.

일단 배칭이 뭘까요? 배칭은 우리말로 "일괄 처리"정도로 해석할 수 있을 것 같습니다. 개별적으로 어떤 요청이 있을 때마다 실시간으로 통신하는 것이 아닌 한꺼번에 일괄적으로 처리하는 것입니다. React적으로 의미를 좁히면, 여러 개의 state 업데이트를 하나의 리렌더링으로 묶는 것을 의미합니다.

💃 자동 배칭 사용하기

자동 배칭 기능을 사용하기 위해 무언가를 할 필요가 없습니다. React가 알아서 하기 때문입니다. React 18의 createRoot를 통해, 모든 업데이트들은 어디서 왔는가와 무관하게 자동으로 배칭됩니다. ( React 18 이전에는 React 이벤트 핸들러 내부에서 발생하는 업데이트만 배칭을 했습니다.)

자동 배칭이 업데이트 되면서 우리는 아무 것도 하지 않고도 렌더링을 최소화하고, 더 나은 성능을 누릴 수 있게 되었습니다. 🥳❣️

🔖 Transitions

긴급하지 않은 UI 업데이트를 표시할 때 Transition을 사용할 수 있습니다. "긴급하지 않은" 이라는 말이 잘 와닿지 않을 수 있는데요. 구글 검색창에 사용자가 무언가를 입력할 때를 예시로 들어보겠습니다.

사용자가 키보드로 무언가를 입력할 때 UI에는 두 가지 변화가 일어납니다. 첫번째는 입력필드에 무언갈 입력할 때마다 커서가 깜빡이며 무언가가 입력되고 있다는 것을 알려주는 시각적 피드백입니다. 두 번째는 제가 입력한 내용이 검색된 입력창 하단의 백그라운드입니다.

방금 입력한 글자 옆에 커서가 깜빡이는 UI 변화는 "긴급한 것"입니다. 입력이 감지될 때마다 UI가 즉시 업데이트 되어야 하죠. (위 사진에서 T를 입력할 때가 되서야 C옆에 커서가 깜빡인다고 상상해보세요!)

이에 비하면 "검색"은 그렇게 긴급하지 않습니다. 이처럼 비교적 긴급하지 않은 UI 업데이트를 transition이라고 합니다. 우선순위가 낮은 업데이트를 transition으로 표시해주면, React는 렌더링을 더 쉽게 최적화 하고, 업데이트의 우선 순위를 잘 매길 수 있게 됩니다.

💃 Transition 사용하기

간단하게 startTransition를 import함으로써 사용할 수 있습니다.

🔖 Suspense

React 16.6에서 실험적인(experimental) 기능으로 추가됐던 Suspense가 React 18에서 드디어 정식 기능이 되었습니다.

그동안 비동기 처리는 React를 쓰는 개발자들에게 가장 까다로운 문제 중 하나였습니다. 일단 요청이 성공하는 경우에만 집중해 컴포넌트를 구성하기 어렵고, 비동기 로직이 많아질 수록 비즈니스 로직을 파악하기 점점 어려워지기 때문입니다. 그런데 Suspense라는 신기술을 통해 비동기를 보다 우아하게 작성할 수 있게 되었습니다. Suspense는 간단히 말해서, 어떤 컴포넌트가 읽어야 하는 데이터가 아직 준비가 되지 않았다고 리액트에게 알려주는 새로운 매커니즘입니다. 이를 이용해서 컴포넌트 렌더링을 어떤 작업이 끝날 때까지 잠시 중단시키고, 다른 컴포넌트를 먼저 렌더링할 수 있습니다.

어떻게 작동하는지 알아보기 위해 먼저 좋지 않은 비동기 코드 예시를 보겠습니다.

이 코드의 어떤 점이 좋지 않을까요?

  • '성공하는 경우'와 '실패하는 경우'가 fetchAccounts라는 함수 안에서 섞여서 처리되고 있습니다. 중간에 에러를 처리하는 로직이 섞인 바람에 함수가 하는 진짜 역할을 한눈에 보기 어렵습니다.
  • 코드를 작성할 때 매번 에러 유무를 확인해야 합니다.

기존의 비동기 처리 방식은 위의 단점이 고스란히 녹아있습니다. 아래 코드, 무척 익숙하지 않으신가요?

React Query 같은 라이브러리가 등장하기 전 비동기 코드는 보통 이런 식으로 작성됐습니다. 내용을 살펴보면 앞서 봤던 코드처럼 성공하는 경우와 실패하는 경우가 분리되지 않고 하나의 컴포넌트 내에 뒤섞여있습니다. 그런데 만약 이런 비동기 작업이 여러 개가 있고 그 여러 개가 동시에 실행된다면 어떨까요? 보통 비동기 작업은 로딩중, 에러, 완료 이렇게 세 가지의 상태를 가지고 있습니다. 2개의 비동기 작업이 있다면, 3의 제곱으로 9가지 상태를 가질 수 있는 거죠. 이렇게 많은 상태를 핸들링하기 위해 코드는 점점 복잡해지고, 가독성이 떨어지게 됩니다. 콜백 지옥처럼, 비동기 코드 지옥이 되죠!

그래서 등장한 것이 바로 ✨Suspense✨입니다.

💃 Suspense 사용하기

Suspense가 목표로 하는 코드는 간단합니다.

  • 성공한 경우에만 집중할 수 있는
  • 로딩 상태와 에러 상태가 분리된
  • 동기처럼 사용할 수 있는

즉, 컴포넌트는 성공한 상태만을 다루고, 로딩 상태와 에러 상태는 외부에 위임함으로써 동기적인 코드와 큰 차이가 없는 코드를 만들겠다는 거죠! 그럼 에러 상태와 로딩 상태는 어떻게 분리될까요?

<ErrorBoundary fallback={<MyErrorPage />}>
  <Suspense fallback={<Loader />}>
    <App />
  </Suspense>
</ErrorBoundary>;

컴포넌트를 '쓰는 쪽'에서 로딩 처리와 에러 처리를 합니다. 컴포넌트를 사용할 때 그 컴포넌트를 위 코드처럼 Suspense로 감싸주면, 컴포넌트의 렌더링을 특정 작업 이후로 미루고, 그 작업이 끝날 때까지는 fallback 속성으로 넘긴 컴포넌트를 대신 보여줄 수 있습니다. 에러 상태는 가장 가까운 ErrorBoundary가 componentDidCatch()로 처리합니다. async await가 try- catch문으로 따로 에러처리를 해주는 것과 유사한 방식이죠? 콜백지옥을 벗어나기 위해 async await가 도입된 것처럼, 비동기 지옥을 벗어나기 위해 suspense가 등장했다고 생각하시면 이해가 쉬울 것 같습니다.

async와 await를 사용할 때 모든 함수에 try ... catch를 감싸지 않는 것처럼, Suspense를 일으키는 모든 컴포넌트에 Suspense나 ErrorBoundary를 붙여주기 보다는 적당한 부분 단위로 에러와 로딩 상태를 한번에 처리하게 됩니다. 예를 들어 아까 봤던 코드를 다시 보면,

<ErrorBoundary fallback={<MyErrorPage />}>
  <Suspense fallback={<Loader />}>
    <App />
  </Suspense>
</ErrorBoundary>;

이 코드의 경우 앱 전체에서 로딩 상태와 에러 상태를 처리해주는 핸들러를 선언한 것입니다.

그럼 이렇게 너무 좋은 Suspense, 어떻게 사용할 수 있을까요?

🥳

사용법도 간단합니다. SWR이나 React Query를 사용하신다면, {suspense: true} 라는 옵션을 사용하기만 하면 자동으로 컴포넌트의 Suspense 상태가 관리됩니다. (Recoil을 사용하실 경우, async selector를 사용하시면 됩니다.) 자, 그럼 이번엔 실제 활용 사례를 봅시다.

이처럼 suspense를 사용하면 복잡한 비동기 처리를 간단하게 바꿀 수 있습니다. 뿐만 아니라 데이터가 준비되는 대로 하나씩 자연스럽게 보여주기 때문에 사용자 경험도 개선할 수 있습니다.

😎 요약;

Suspense를 사용하면 로딩과 에러 처리를 바깥으로 위임하며 비동기 작업을 동기와 똑같이 처리할 수 있다!

🗂 참조

React 18 Quick Guide & Core Concepts Explained
Concurrent Mode
동시성 프로그래밍과 비동기 프로그래밍
동시성(Concurrency)과 동기(Synchronous)의 차이
배치 프로그램이란?
React의 자동배치

profile
공부하는 즐거움

0개의 댓글