React

[React] React v18 - 빠르게 둘러보기

zubetcha 2022. 6. 9. 00:08

 

Concurrency

Concurrency는 새로워진 React v18 의 코어 컨셉이라고도 볼 수 있다. 공식 문서에서 또한 v18에 대해서 소개하면서 Concurrent React가 무엇인지 자세히 설명하고 있다.

 

Coucurrency는 아래와 같은 사전적 의미를 가지고 있다.

 

• 동시성

• 동시 실행

• 병행성

 

그렇다면 리액트에서의 동시성이란 무엇을 의미할까?

 

Concurrency란 어떠한 기능이 아닌, 동시에 여러 가지 버전의 UI를 준비할 수 있게 해주는 메카니즘이다.

 

가장 큰 변화는 리액트의 렌더링 모델에 있다. 이전에는 오직 하나의 트랜잭션만이 직렬로 실행되면서 중간에 엔지니어가 개입할 수 없는 렌더링 구조를 띄고 있었다면, v18부터는 렌더링이 시작되었다 하더라고 임의로 중간에 멈추거나, 렌더링 자체를 지연시킬 수도 있게 되었다.

 

그럼 이제 무엇이, 어떻게 바뀌었는지 살펴보자!

 

✨ Automatic Batching

 

Automatic Batching에 대해 알아보기 전에 우선 batching이 무엇인지 살펴보자.

 

Batching?

Batching이란 렌더링 최적화를 위해 여러 개의 상태 업데이트들에 대해서 한 번의 re-render로 처리하기 위해 그룹화하는 것을 뜻한다. 예를 들어 어떠한 이벤트에 의해 두 개의 상태 업데이트가 발생하는 경우 업데이트마다 리렌더링 하는 것이 아닌 해당 이벤트가 처리된 후 한 번만 리렌더링 하는 것이 batching이다.

 

react v17까지는 리액트 내부의 이벤트에 의해 발생하는 상태 업데이트만 batching되고 Promise, setTimeout 및 native 이벤트에 의해 발생하는 여러 개의 상태 업데이트는 batching되지 않았다.

 

export const Component = () => {
  console.log("batched!")

  const [count, setCount] = useState(0);
  const [flag, setFlag] = useState(false);

  const onClickButton = () => {
    setCount(count => count + 1);
    setFlag(flag => !flag);
  }
  
  return (
    <button style={{width: "100px", height: "40px", background: "#3B5B38", color: "#FFF"}} onClick={onClickButton}>batch!</button>
    <h1 style={{textAlign: "center", color: flag ? "blue" : "black"}}>{count}</h1>
  )
}

 

위의 코드에서 onClick 이벤트는 리액트가 래핑한 합성 이벤트의 마우스 이벤트 중 하나이기 때문에 onClickButton 버튼을 클릭해도 onClickButton 함수 내의 setState마다 리렌더링이 발생하는 것이 아닌, onClickButton의 실행이 종료된 후 setCount와 setFlag에 의해 발생한 상태 업데이트들을 하나로 묶어 처리(batching)하여 batched! 가 한 번만 출력된다.

 

 

그렇다면 아래의 코드에서는 batched! 가 몇 번 출력될까?

 

export const Component = () => {
  console.log("batched!")

  const [count, setCount] = useState(0);
  const [flag, setFlag] = useState(false);

  const onClickButton = () => {
    setTimeout(() => {
      setCount(count => count + 1);
      setFlag(flag => !flag);
    }, 1000);
  };
  
  return (
    <button style={{width: "100px", height: "40px", background: "#3B5B38", color: "#FFF"}} onClick={onClickButton}>batch!</button>
    <h1 style={{textAlign: "center", color: flag ? "blue" : "black"}}>{count}</h1>
  )
}

 

위에서 언급하였듯이 setTimeout에 의해서 발생한 동시 다수의 상태 업데이트들은 batching하지 않기 때문에 각각의 setState마다 리렌더링이 발생하여 batched! 가 두 번 출력되었다.

 

 

이제 동일한 코드를 v18에서 실행시켜서 확인해보자. v18의 업데이트된 새로운 기능들을 사용하는 방법은 간단하다. react의 버전을 18로 업그레이드한 후 루트에서 애플리케이션을 렌더링해주는 index.js 파일에서 DOM render 코드를 기존의 위와 같은 코드에서 아래와 같이 createRoot로 변경해주면 된다.

 

// index.js

// 기존
import ReactDOM from 'react-dom';

ReactDOM.render(
  <React.StrictMode>
    <App />,
  </React.StrictMode>,
  document.getElementById('root')
);

// 변경
import ReactDom from 'react-dom/client';

const root = ReactDom.createRoot(document.getElementById('root'));
root.render(<App/>);

 

automatic batching 외에도 v18의 새로운 기능이나 훅을 사용하기 위해서는 반드시 위와 같이 createRoot를 사용해서 렌더링 해줘야 한다. 

 

리액트를 v18로 업그레이드한 이후, 상황에 따라 automatic batching을 하지 않아야 하는 유즈케이스도 있을 수 있다. 그런 경우에는 react-dom의 flushSync를 사용해서 각각의 상태 업데이트마다 리렌더링을 강제할 수 있다.

 

import { flushSync } from "react-dom"; // Note: react가 아닌 react-dom이다

function handleClick() {
  flushSync(() => {
    setCounter((c) => c + 1);
  });
  // 이 과정이 끝났을 때 React는 DOM을 업데이트한 상태
  flushSync(() => {
    setFlag((f) => !f);
  });
  // 이 과정이 끝났을 때 React는 DOM을 업데이트한 상태
}

 

✨ Transition

 

트랜지션은 급한 업데이트와 상대적으로 급하지 않은 업데이트를 분리해주는 리액트의 새로운 컨셉이다.

 

• 급한 업데이트 > 타이핑, 클릭, 입력 등 바로 바로 인터랙션을 반영시켜야 하는 업데이트

• 급하지 않은 업데이트 > 단순한 UI 트랜지션

 

급한 업데이트로 처리해야 하는 상태는 기존과 동일하게 상태를 업데이트해주면 된다. 그 외 급하지 않은 업데이트로 처리하기 위해서는 새로운 훅인 useTransition 또는 새로운 메소드인 startTransition을 사용하면 된다.

 

import {startTransition} from 'react';

// 급한 업데이트: 타이핑한 것을 바로 보여줘야 함
setInputValue(input);

// 급하지 않은 업데이트는 트랜지션으로 처리한다.
startTransition(() => {
  // 트랜지션: 입력한 값의 검색 결과를 보여줘야 함
  setSearchQuery(input);
});

startTransition으로 감싼 업데이트는 급하지 않은 업데이트로 처리되고, 만약 업데이트 도중에 마우스 클릭이나 키 입력 등의 더 급한 업데이트가 발생했다 하더라도 업데이트를 중단시키고 더 급한 업데이트를 먼저 처리한다. 

 

• useTransition > pending 중인지를 판별하는 값과 startTransition 메소드를 반환하는 훅

• startTransition > 급하지 않은 업데이트를 트랜지션으로 처리해주는 메소드

 

const [isPending, startTransition] = useTransition()

 

✨ Suspense

 

v17까지는 React.lazy와 함께 code splitting의 용도로만 사용할 수 있었던 Suspense 컴포넌트가 데이터 fetching 까지 사용 범위가 확장되었다. 다만 아직까지 순수 리액트 프로젝트에서는 동작을 하지 않는 것 같고, 리액트 기반의 프레임워크에서만 사용할 수 있는 듯 하다. 공식문서에서 설명하고 있는 리액트 기반의 프레임워크는 Relay, Next.js, Hydrogen, Remix 등이 있다.

 

사용 방법은 간단하다. 

데이터를 fetch하는 api를 호출하는 컴포넌트가 있다면 해당 컴포넌트를 Suspense 컴포넌트로 감싸주면 된다. 즉, Suspense 컴포넌트의 fallback 프로퍼티가 비동기 실행에 대해서도 핸들링할 수 있게 된 것이다.

 

<Suspense fallback={<p>로딩중...</p>}>
  <SuspenseExample /> // data fetch api를 호출하는 컴포넌트
</Suspense>

 

+ react github 레포 보고 구체적으로 Suspense 지원 확장이 어떤 의미가 있는지 추가하기

 

✨ New Hooks

 

v18부터 새로이 추가된 훅이다. 여기서는 간단하게 새로운 훅들이 어떤 역할을 하는지만 알아보자!

 

useId

useId는 클라이언트와 서버 양 쪽에 유니크한 id를 생성하주는 새로운 훅이다. 단, 렌더 트리에서 유니크한 id를 컴포넌트당 1개만 생성할 수 있기 때문에 여러 개의 유니크한 id가 필요한 key에 사용하는 것은 적합하지 않다.

 

useTansition

useTransition과 startTransition은 비교적 급하지 않은 상태 업데이트를 지연시켜주는 새로운 훅이다. useTransition 또는 startTransition으로 감싸지 않은 상태의 업데이트들은 기본적으로 급한 상태 업데이트로 처리되며, 급한 상태 업데이트는 급하지 않은 상태 업데이트의 렌더링이 시작됐다 하더라도 중간에 개입하여 먼저 렌더링될 수 있다.

 

useDeferredValue

useDeferredValue는 렌더 트리에서 급하지 않은 부분의 리렌더링을 지연시켜주는 새로운 훅이다. 디바운싱과 비슷하지만 디바운싱과 다르게 지연시킬 고정된 시간을 미리 설정할 필요가 없기 때문에 첫 렌더링이 끝나면 바로 지연시킨 렌더링 화면을 보여줄 수 있다. 또한 지연된 렌더링도 렌더링이 시작된 후에도 중간에 개입할 수 있으며 유저의 인터랙션을 막지 않는 장점을 가지고 있다.

 

useSyncExternalStore

useSyncExternalStore는 외부의 스토어가 스토어 업데이트를 동기적으로 강제하여 동시에 상태를 구독할 수 있도록 해주는 새로운 훅으로, 외부에 있는 데이터를 구독하려고 할 때 useEffect의 사용 필요성을 없애준다. 외부의 상태를 리액트와 통합시켜주는 어떠한 라이브러리와도 호환 가능하도록 설계되었다.

✅ 상태 관련 라이브러리와 함께 사용할 것을 권장함

 

useInsertionEffect

useInsertionEffect는 css-in-js 라이브러리가 렌더링 중 스타일을 입힐 때 발생하는 성능 문제를 해결할 수 있도록 해주는 새로운 훅이다. 이 훅은 DOM이 변하기 시작한 후와 레이아웃 이펙트가 새로운 레이아웃을 읽기 전 사이에 실행되며, 동시에 다수의 렌더링이 발생하는 동안 브라우저가 레이아웃을 다시 계산할 수 있도록 해준다.

✅ css-in-js 라이브러리와 함께 사용할 것을 권장함

 

✨ New Client & Server Rendering APIs

 

v18의 새로운 클라이언트 및 서버 렌더링 API는 react-dom/client에서 import하여 사용할 수 있다.

 

Client

createRoot

: 렌더링하거나 언마운트할 루트를 생성해주는 새로운 메소드로, v17까지 사용했던 ReactDOM.render 대신 이 createRoot를 사용하면 된다. v18로 버전을 업그레이드 했다 하더라도 createRoot 메소드를 사용해 DOM을 렌더링하지 않으면 v18의 새로운 기능들은 사용할 수 없다. 

hydrateRoot

: 서버 사이드 렌더링으로 동작하는 애플리케이션을 수화시켜주는 새로운 메소드이다. v17까지 사용했던 ReactDOM.hydrate 대신 사용하면 되며, createRoot와 마찬가지로 hydrateRoot 또한 v18에서 이 메소드를 사용하지 않으면 새로운 기능들은 사용할 수 없다.

 

Server

renderToPipeableStream

: 노드 환경에서의 스트리밍을 위한 메소드이다.

renderToReadableStream

: Deno나 Cloudflare 같은 모던 엣지 런타임 환경을 위한 메소드이다.

 

기존의 renderToString 메소드도 계속 사용할 수는 있지만 권장되지는 않는다.

 

✨ New Strict Mode

 

v18부터 개발 환경에만 적용되는 strict mode가 새롭게 변경됐다.

 

리액트가 그리고 있는 그림 중 하나는 이전 상태를 보존하면서 UI 중 일부분을 추가하거나 삭제할 수 있도록 하는 것인데 이걸 가능하게 하기 위해서는 컴포넌트가 여러번 마운트되고, 언마운트되고, 사라지더라도 다시 마운트될 때 이전과 동일한 상태를 가지고 있어야 한다. 새로운 strict mode는 개발 환경에서 컴포넌트가 최초로 마운트될 때마다 자동으로 두 번째 마운트로 이전 상태를 저장하면서 언마운트하거나 리마운트되는지 확인해준다.

 

~v17

* 컴포넌트를 마운트한다.
  * 레이아웃 리펙트가 생성된다.
  * 이펙트가 생성된다.

 

v18~

* 컴포넌트를 마운트한다.
  * 레이아웃 이펙트가 생성된다.
  * 이페트가 생성된다.
* 컴포넌트 언마운트를 가장해 본다.
  * 레이아웃 이펙트가 사라진다.
  * 이펙트가 사라진다.
* 이전의 상태를 가지고 컴포넌트를 다시 마운트하는 것을 가장해 본다.
  * 레이아웃 이펙트가 생성된다.
  * 이펙트가 생성된다.

 

✨ Server Component

 

Server Component는 개발자가 서버와 클라이언트를 넘나들며 애플리케이션을 만들 수 있도록 해주는 차기 신기능으로, 아직 개발 중에 있다.