0w0

React v18.0 (2022-05-xx 완료)

2022/03/29 by The React Team

이제 React 18를 npm에서 이용할 수 있습니다!

지난 글에서는 차근차근 React 18로 업그레이드하는 방법을 소개했습니다. 이 글은 React18에는 무엇이 생겼는데 그리고 어떠한 의미를 가지고 있는지 보려합니다.

React 18은 자동 배치처리 같은 것을 바로 사용할 수 있도록 개선, startTransition 같은 새로운 API, Suspense 지원에 의한 streaming server rendering이 포함되어 있습니다.

React 18의 많은 기능은 새로운 concurrent renderer 상에 구축되어 있고, 강력한 기능이 드디어 풀리게 되는 무대 뒤 변경입니다. concurrent React는 opt-in 방식으로 concurrent 기능을 사용하고 싶을 때만 사용할 수 있습니다. 하지만 우리는 이것이 애플리케이션 구축 방법에 큰 영향을 준다 생각하고 있습니다.

우리는 몇 년을 투자해, React의 병렬처리 지원을 연구하고 기존의 유저에게 단계적으로 도입하는 것을 특히 철처히 하고 있습니다. 작년 여름, React 18 워킹그룹을 결성하고 커뮤니티 전문가의 피드백을 모아 React 에코시스템 전체를 부드럽게 업그레이드할 수 있도록 했습니다.

더 궁금하시면, React Conf 2021에서는 이 비전을 공유했습니다.

이하는 Concurrent Rendering를 시작으로 출시로 인해 기대되는 전체상을 소개합니다.

React Native 유저를 위한 주의사항 React 18은 New React Native Architecture를 사용한 React Native에 이식될 예정입니다. 상세는 React Conf 기조연설을 참조해주세요.


React 18의 핵심적인 추가기능은 conccurrent입니다. 애플리케이션 개발자 대부분에게 해당된다 생각하지만 라이브러리 관리자에게는 조금 더 복잡한 이야기가 될 수도 있습니다.

Concurrency는 그 자체로 구동되는 기능이 아닙니다. React가 여러 버전의 UI를 동시에 준비가능캐 하는 무대 뒤에 구현된 사항이라 생각하고, 이로 인해 unlock되는 기능이 있기에 가치가 생깁니다. React는 우선순위 큐, 다중 버퍼링 같이 고도의 기술을 내부구현에 사용하고 있습니다. 하지만 이 컨셉은 우리가 공개하고 있는 그 어떤 API에서도 확인할 수 없습니다.

API를 설계할 때, 구현의 상세사항을 개발자에게 숨기려했습니다. React 개발자는 사용자 경험같은 부분에 집중해, React는 경험을 어떻게 제공할까를 처리합니다. 그러니 React 개발자가 후드 안에 어떻게 concurrency 처리를 하는지 알 수 없었습니다.

그러나 Concurrent React는 전형적이며 상세한 구현보다 중요하고, React의 코어 렌더링 모델에 기반 업데이트라 말할 수도 있습니다. 즉, concurrency 처리가 어떻게 돌아가는지 아는 것이 엄청나게 중요하지는 않지만, 고레벨에서 그게 뭔데 정도는 알 가치가 있을 수 있습니다.

Concurrent React 중요한 특징은 렌더링 중단이 가능하다는 것입니다. React 18으로 업그레이드하려 했을 당시는 concurrent 기능을 추가하기 전에, 갱신은 이전 버전 React처럼 중단되지 않는 트랜잭션 렌더링하려 했습니다. 동기렌더링에는 갱신 렌더링이 시작되면 사용자가 결과를 화면상에 확보하기까지 무엇도 중단할 수 없었습니다.

concurrent 렌더링이 늘 그런건 아니지만, React는 갱신 렌더링을 시작하고 중간에 일시 중시한 다음, 다시 계속할 수 있습니다. 진행중인 렌더링을 포기할 수도 있고요. React는 렌더링이 중단된 경우에도 UI가 일관적으로 게시되는 것을 보증합니다. 그렇기 위해 트리 전체가 평가된 후, 마지막까지 DOM 변이 실행을 기다리고 있습니다. 이 기능으로 React는 메인 스레드를 블록하지 않고, 백그라운드에서 새로운 화면을 준비할 수 있습니다. 즉, 대규모 렌더링을 해야할 대도, UI는 유저 입력에 즉각 반응할 수 있으며, 유동적인 사용자 경험을 실현할 수 있습니다.

또 다른 예는 재이용가능한 상태입니다. Concurrent React은 화면에서 UI 세션을 제거해서, 이전 상태를 재이용하면서 나중에 되돌릴 수 있습니다.

예를 들면, 유저가 화면에서 탭에서 이탈했을 때, React는 이전 화면을 같은 상태로 돌렸을 것입니다. 이번 마이너 변경으로 이 패턴을 구현한 <Offscreen> 컴포넌트 추가할 예정입니다. Offscreen를 사용해 새로운 UI를 백그라운드에서 준비하고 사용자가 그것을 보기전에 준비를 마칠 수 있습니다.

Concurrent 렌더링은 React의 강력한 도구로 Suspense, 트랜지션, streaming server rendering 같은 대부분 기능은 이걸 이용하기 위해 구축되었습니다. React 18은 이 새로운 기반 위에 구축하려는 목표의 시작에 불과합니다.

기술적으로 Concurrrent 렌더링은 브레이킹 체인지입니다. concurrrent 렌더링은 중단가능하므로 이걸 사용하면, 컴포넌트의 동작이 약간 변합니다.

우리의 테스트에서는 수 천의 컴포넌트를 React 18로 업그레이드 했습니다. 그 결과 거의 모든 컴포넌트가 딱히 변경없이 concurrent 렌더링을 "잘" 동작한다는 걸 알게되었습니다. 하지만 그 중에는 추가 작업을 해야하는 경우도 있었습니다. 크지 않은 변경입니다만, 그리고 본인의 페이스에 맞게 변경할 수있습니다. React 18의 새로운 렌더링 동작은 애플리케이션 신기능을 사용하는 부분에서만 동작합니다.

전체적인 업그레이드 전략은 기존 코드를 건들지 않고, 애플리케이션을 React 18로 동작하는 것입니다. 그래서 자신의 페이스로 점점 concurrent 기능을 추가할 수 있습니다. <StrictMode>를 사용하면 개발중에 concurrent 관련 화면에 버그를 보이게할 수 있습니다. Strict Mode는 실제 동작과는 다르지만, 개발할 때는 경고 및 이중 호출 기능을 기록합니다. 모든 것을 잡아낼 수 없지만 일반적인 실수를 방지하는데 효과적입니다. strict 모드는 본체의 동작에 영향을 주지 않지만 개발중에는 추가 경고를 보여주거나 idempotent인 함수를 2회 호출하거나 합니다. 모든 실수를 잡아주는 것은 아니지만 잦은 실수를 막는데에 효과적입니다.

React 18 업그레이드 후, concurrent 관련을 바로 사용할 수 있습니다. 예를 들면 유저 입력을 블록하지 않고 화면 전환을 하기위한 startTransition를 사용하는 거죠. 아니면 값비싼 재렌더 빈도를 낮추기 위해서 useDefferredValue를 사용할 수도 있습니다.

하지만 장기적으로 당신의 애플리케이션 concurrent를 더하기 위한 정석 방법은 concurrent에 대응한 라이브러리, 프레임워크를 사용하는 것이라 생각합니다. 웬만하면 당신이 concurrent의 API를 직접 다룰리가 없을 것입니다. 예를 들어 새로운 화면전환을 할 때마다 매회 개발자가 startTransition를 호출하는 것이 아니라, 루터 라이브러리가 네이게이션을 startTransition을 자동으로 랩하는 것처럼요.

라이브러리가 업그레이드된 concurrent에 대응되기 까지 다소 시간이 걸릴지도 모릅니다. concurrent를 라이브러리가 활용하기 쉽게하기 위해 새로운 API를 제공합니다. React 에코시스템이 서서히 migration될 때까지 라이브러리 관리자의 작업을 기다려주세요.

상세는 이전 글을 읽어주세요: How to Upgrade to React 18

React 18은 Relay, Next.js, Hydrogen, Remix 같이 사용방법의 규약이 있는(opinionated) 프레임워크에 의해 데이터 fetch을 위한 Suspense를 사용할 수 있습니다. 단발적인 데이터 fetch를 위한 Suspense 사용할 수 있습니다만 현시점에는 일반적 전략이 아니므로 권하지는 않습니다.

장래에는 상기한 프레임워크를 사용하지 않아도 당신이 데이터에 Suspense 사용해 접근하기 쉽도록 하기 위해서 새로운 기본기능을 제공할 수도 있습니다. 허나 Suspense는 라우터, 데이터층, 서버렌더링 환경에 당신의 애플이케이션 아키텍처와 깊게 결합되어 사용하는 경우에 가장 효과적입니다. 장기적으로 라이브러리, 프레임워크가 React의 에코시스템에 의해 중요한 움직임일이 있기를 기대합니다.

React의 이전 버전과 같이 Suspense는 클라이언트에서 React.lazy를 사용해 코드를 분할하는 경우에도 이용할 수 있습니다. 다만 우리는 Suspense를 사용해 실현하고 싶은 구상은 코드를 읽는 것보다 더 많은 것입니다. 목표는 Suspense의 지원을 확장하면서 언젠가 Suspense에 의한 하나의 선언적인 콜백이 다양한 비동기 조작(코드, 데이터, 이미지 로드 등)를 다루는 것입니다.

서버 컴포넌트는 구현 예정 기능입니다. 클라인언트 사이드 애플리케이션에 의한 풍부한 인터렉티브와 전통적인 서버렌더링에 의한 퍼포먼스 개선을 겸한 클라이언트/서버 양방에 걸쳐있는 애플리케이션 개발을 가능하도록 하는 것입니다. 서버 컴포넌트는 React의 Concurrent와 본질적으로는 결합되어 있지 않지만, Suspense나 Streaming 서버 렌더링 같이 Concurrent와 병용한 경우에 가장 잘 돌아가도록 설계되었습니다.

서버 컴포너틑는 아직 실험적 기능이지만, 18,x의 마이너 출시 초기 버전를 출시할 것이라 봅니다. 그 때까지는 proposal를 통해서 넓게 선택되도록 준비를 다지며, Next.js, Hydrongen, Remix 같은 프레임워크와 협력할 것입니다.

배치는 React가 퍼포먼스를 위해 여러 상태 갱신를 그룹화해서 한 번의 재렌더링으로 모으는 것을 지칭합니다. 자동 배치 이전에는 React 이벤트 핸들러 안에서 갱신만 배치처리했습니다.

Promise, SetTimeout, 네이티브의 이벤트 핸들러나 다른 여러 이벤트 내에 일어나는 갱신은 기본적으로 배치 처리되어 있지 않았습니다. 자동 배치에 의해 이러한 갱신도 자동적으로 배치 처리되었습니다.

// 이전: React 이벤트만 배치됨setTimeout(() => {  setCount((count) => count + 1);  setFlag((flag) => !flag);  // React는 각각 한 번씩 2번 업데이트했다(no batching)}, 1000);// React 18: promise, setTimeout, native event handlers 혹은 어떤 이벤트든 업데이트는 다 배치됨setTimeout(() => {  setCount((count) => count + 1);  setFlag((flag) => !flag);  // React는 딱 한 번의 리렌더링으로 끝(this's batching!)}, 1000);

상세는 Automatic batching for fewer renders in React 18

트랜지션(단계적추이)는 React에 들어온 새로운 개념이며, 긴급성 높은 갱신(urgent update)와 그렇지 않은 갱신(non-urgent update)를 구혈하는 것입니다.

타이핑, 클릭, 프레스 같이 긴급성 높은 갱신은 물리적인 물체의 어떤 행동이 우리의 직관에 반하지 않도록 즉각 반응할 필요가 있으며, 그렇지 않으면 "이상하다" 느낍니다. 한 편, 트랜지션 안에서는 유저는 화면상으로 어떠한 중간 값을 보여줄지 기대하지 않습니다.

예를 들어 드롭다운 내에 필터를 선택하는 경우, 필터 버튼은 클릭한 순간 반응한다 기대할 것 입니다. 하지만 필터 결과는 버튼 반응과 다르게 서서히 보여도 괜찮습니다. 작은 지연은 인식 못하며, 있어도 별 탈없습니다. 또 이번 렌더가 끝나지 않은 단계에서 다시 필터 변경할 때, 최종 결과 외에는 신경쓰지도 않겠지요.

전형적으로 좋은 UX를 위해서는 어떠한 유저 입력은 긴근성 높은 갱신과 그렇지 않은 갱신 양방을 취해야할 것입니다. input 이벤트 내에서 startTransition API를 준비하고, React에 어떤 갱신의 긴급성이 높고, 어떤 것이 "트랜지션"인가 정할 수 있습니다.

import { startTransition } from 'react';// 긴급: 무엇인가 타이핑setInputValue(input);// 트랜지션할 상태 업데이트를 안에 넣는다.startTransition(() => {  // 트랜지션: 결과 보임  setSearchQuery(input);});

startTransition으로 랩한 긴급성 낮은 갱신으로 인식되어 https://ja.reactjs.org/blog/2022/03/29/react-v18.html#suspense-in-data-frameworks