안녕하세요! 저는 오픈소스컨설팅 Playce Dev 팀에서 Frontend 개발을 하고 있는 강동희 입니다. 22년 3월 29일 WEB Frontend 라이브러리 React 에서 새로운 버전인 18.0.0 버전을 릴리즈 하였고 이번 포스팅을 통해서 해당 버전에 대해 제가 공부한 지식을 여러분들과 함께 나누고자 이 글을 작성하게 되었습니다.

react 18 을 탐구하기에 앞서..

저희 Playce Dev Frontend 파트는 React를 이용해 개발을 진행하고 있습니다. 오픈소스를 누구보다 사랑하고 React 에 대하여 어떤 팀보다 관심 있게 지켜보는 저희 팀에서는 18 버전이 릴리즈 되자마자 “ 이건 못 참지~ “ 하면서 탐구하고 버전을 적용하고자 노력했답니다.

해당 글이 작성된 날을 기준으로 (2022년 5월 3일 기준) React 공식 문서에서 18.0.0 버전은 영문으로만 되어있으며 한국어 문서로는 번역이 되어있지 않은 상태입니다. 하여 혹시라도 영어 알레르기가 있으실 여러분들을 위해 공부한 부분을 React 팀의 Github 와 Official Document를 분석하고 해석하여 공유해 드립니다! 

현재 2022년 4월 27일 기준으로 React 18.1.0 버전이 릴리스 되었는데, 18 버전에서 나온 새로운 기능들이 계속 안정화 단계에 있으므로 꾸준히 기대하고 지켜보면 좋을 것 같습니다.

🤗 아래 포스팅은 React 에 대해서 간단한 지식이 있는 누구나 볼 수 있을 정도의 수준으로 작성한 글입니다.
🤗 2023년 02월 02일 기준으로 문장의 구조를 바꿔 포스팅을 이해하기 쉽게 업데이트 했습니다!

react 팀이 18 버전을 통해 해결하고자 했던 문제들?

3월 29일, React 18 버전이 세간에 등장 했습니다. React 18 버전에는 크게 아래와 같은 변화가 있었습니다.

  • New Root API
  • Automatic Batching
  • New Concept “Transition”
  • SSR support for Suspence

React 팀이 18 버전을 통해서 개선하고자 했던 내용은 무엇 일까요?

여러 가지 개선 사항이 있지만 그중 두 가지만 살펴보자면, 첫 번째는 Batching 작업을 통한 Rendering 퍼포먼스 향상, 두 번째는 새로운 기능인 Transition 을 통해 UI 업데이트에 우선순위를 부여하거나, 느린 네트워크 환경에서의 UX 향상을 살펴볼 수 있을 것 같습니다.

해당 포스팅에서 다루는 것

  1. 새로운 Root API
  2. Automatic Batching
  3. Transition

다루지 않는 것

  1. Server side rendering support for Suspense
저희 팀에서는 별도의 SSR (Server Side Rendering) 기술을 사용하지 않고 있습니다. 그렇기 때문에 이번 개선사항 중 SSR 기술에 관한 내용은 포스팅에서 제외하였습니다.
18 버전 이후로 Suspense 가 상당히 안정적이라고 느껴졌습니다. 기존에 CRA 에서 SSR 을 활용하기 어려워 Next.js 같은 프레임워크를 활용했다면, 이젠 React 에서 제공해주는 API 를 활용해 CRA 에서도 SSR 을 활용한 아키텍처를 설계할 수 있습니다.

New Feature : 새로운 Root API 의 등장

18 버전이 등장하면서 새로운 Root API 가 등장 했습니다. 그렇다면 여기서 말하는 루트란 무엇을 의미하는지 알아봅시다! 아래에서 자주 언급할 루트는 React 가 랜더링을 위해 가상 돔 트리를 추적하는 데 사용하는 최상단에 위치한 데이터 구조 포인터를 의미합니다. 쉽게 말해서 public 폴더 하에 index.html 을 살펴보면 <div id=”root”></div> 라는 요소를 살펴볼 수 있는데, 해당 요소가 루트 요소입니다!

react-index.html
위 코드의 31행을 보시면 작고 소중한 Root 를 확인할 수 있답니다.

기존의 Legacy Root API (ReactDOM.render)는 18 버전 등장 이전의 Legacy 모드 (React 17 혹은 그 이전 버전에서 작동하는) 에서 돌아가는 루트를 생성했습니다.

18 버전 이후의 새로운 Root API 는 ReactDOM.createRoot 으로 React 18 버전에서 사용하는 루트를 생성할 수 있는데, 새로운 Root API 를 통해서 React 18 버전의 새로운 기능 및 API 를 사용할 수 있습니다.

// 18v 이전의 루트
ReactDOM.render(, document.getElementById(‘root’));

// 18v 이후의 루트 API
import * as ReactDOMClient from ‘react-dom/client’;

const container = document.getElementById(‘app’);
const root = ReactDOMClient.createRoot(container);
root.render();

React Team은 왜 새로운 Root API 를 적용하게 되었을까?

18 버전 이전의 React 에서는 루트가 되는 컨테이너에 변화가 없더라도 render 하기 위해서, 루트를 반드시 체크하고, 루트를 통과 했어야만 했습니다. 이 과정은 React 가 Virtual DOM을 사용하기 때문에 반드시 거쳐야 하는 작업입니다. React 팀에서는 루트가 되는 컨테이너를 랜더링 과정을 거칠때마다, 매번 해당 루트를 통과할 필요가 없다고 생각했고, 이런 무의미한 반복되는 과정을 개선하기 위해서 새로운 Root API 를 18 버전에 적용하게 되었습니다.

New API “ ReactDOMClient “

새로운 API의 createRoot() 함수를 사용하면 루트를 반환합니다. 새로운 루트를 통해서 React Node 를 DOM 에 Render 할 수 있습니다. 또한 원한다면 Unmount 도 할 수 있답니다.

import * as ReactDOMClient from "react-dom/cleint";

const root = ReactDOMClient.createRoot(container);
root.render();

root.unmount();

“ Root API ” 에서의 콜백 함수의 삭제

기존의 루트 (ReactDOM.render) 에서는 컴포넌트가 랜더링 되거나 업데이트될 때, 주로 콜백 함수를 render 함수의 매개변수로 사용하지 않았지만 (생략 했지만), 만약 콜백 함수가 필요한 상황이라면 render 함수의 매개변수로 넣어서 사용할 수 있었습니다. 하지만 새로운 Root API 에서는 매개변수로 사용할 수 있는 콜백 함수를 삭제 했답니다. (함수의 형식 매개 변수를 삭제함 으로써 매개변수로 콜백 함수를 넣어주지 못한다는 의미입니다.)

콜백 함수를 삭제한 이유는, hydration, SSR 과 함께 콜백 함수를 사용한다면 해당 콜백 함수를 호출하는 타이밍이 개발자의 예상과는 다르게 작동할 수 있었기에, 이런 의도하지 못한 상황을 피하기 위해 매개변수 콜백 함수를 삭제한 것 입니다. 대신에 이전 버전의 콜백 함수 기능을 활용하고자 한다면 requestIdleCallback, setTimeout 혹은 루트에서 ref 콜백을 통해 사용할 수 있게 되었습니다.

// 18v 이전 render() 에서 콜백 함수의 사용
import * as ReactDOM from "react-dom";
import App from "App";

const root = document.getElementById('root');

ReactDOM.render(, root, () => {
  console.log('rendered')
});

// 18v 에서 콜백 함수를 사용하고자 한다면
import * as ReactDOMClient from "react-dom/client";

const container = document.getElementById('root');
const root = ReactDOMClient.createRoot(root);

root.render(console.log("rendered"));

New Feature : Automatic Batching

React 에서 Batching 이란?

React 에서 언급한 배칭 (Batching) 이란 무엇 일까요?

이는 더 나은 랜더링 퍼포먼스를 위하여 여러 state 업데이트를 (setState) 하나의 업데이트로 그룹화하는 것을 의미합니다.

React 팀이 18 버전을 출시하기 이전의 React에서는 React Event Handler만이 state 업데이트를 Batching 처리했습니다. 하지만 18 버전 이후로는 React Event Handler뿐만 아니라 promise, setTimeout, native event handler 등 다양한 로직에서도 Batching 작업이 가능하게 되었습니다.

18 버전 이후 Batching 되는 방법

18 버전 이후부터는 새로운 Root API인 createRoot 를 통해서, 브라우저 이벤트 뿐만 아니라 어떤 이벤트에서 발생하는 state 업데이트를 어디서 발생했는지를 신경 쓰지 않고 자동으로 업데이트들이 배칭 시킵니다. setTimeout, promise, native event handler 등에서 발생하는 모든 state 업데이트는, React 에서 발생하는 이벤트 내부의 업데이트와 동일한 방식으로 state 업데이트 들을 Batching 하여 여러 번 수행 했어야 했던 랜더링 과정을 단 한 번만 처리할 수 있게 해줬고, 이는 리랜더링 횟수를 최소화하여, 애플리케이션의 성능 향상을 기대할 수 있게 되었습니다.

// 위 두 업데이트 모두 배칭되어서 단 한번 리랜더링 된다!
function handleClick() {
  setCount((prev) => prev + 1);
  setFlag((f) => !f);
}

// setTimeout 내에서 업데이트도 배칭되어 한번의 리렌더링을 하게된다.
setTimeout(() => {
  setCount((prev) => prev + 1);
  setFlag((f) => !f);
}, 1000);

// fetch api에서 또한 배칭되어 한번의 리렌더링을 하게된다.
fetch("api").then(() => {
  setCount((prev) => prev + 1);
  setFlag((f) => !f);
});

Batching 을 원하지 않는 경우..

만약 Batching 을 원하지 않는 코드 (state 변경 후 즉시 DOM 으로 부터 값을 가져와야 하는 경우 등..) 일 경우엔 ReactDOM.flusySync() 를 사용하면 state 업데이트를 진행할 때 Batching 하지 않고 업데이트를 진행할 수 있습니다.

import {flushSync} from 'react-dom';

function handleClick() {
  flushSync(() => {
    setCount(prev => prev + 1);
  });
  flushSync(() => {
    setFlag(f => !f);
  });
}

New Feature : Transition

Transition 은 React 18 버전이 출시하며 새롭게 등장한 컨셉 입니다. 개발자는 Transition 기능을 활용한다면 보다 빠르게 업데이트되어야 하는 상황과 그렇지 않은 업데이트 상황을 구별하면서 개발을 할 수 있게 됩니다.

18 버전 이전의 버전에서는 state를 업데이트하는데 있어서 우선순위를 두는 게 어려웠습니다. 예를 들어서 Throttling, Debounce 기법을 활용하여 업데이트의 우선순위를 설정할 수 있었지만, 두 방법 모두 원하지 않는 작업 시간이 발생 한다는 문제점이 발생했습니다. 또한 위 기법을 활용하는 동안엔 어떤 컴포넌트는 업데이트에 반응을 (리랜더링) 하지 않는 문제도 발생하고는 하였습니다.

이런 상황에서 React 팀은 18 버전에서 Transition 이라는 기능을 출시 했습니다. 이 새로운 기능은 특수한 업데이트 들을 직접 마킹 하면서, 의도된 방식으로 작업을 지연시켜 사용자와의 상호작용 및 UX 를 지속적으로 향상시켜 줍니다.

Transition 활용법

Transition API를 활용하기 위해서는 먼저 새로운 Root API 를 사용해야 합니다.

useTransition Hook

useTransition Hook 을 사용하면 Transition 기능을 쉽게 사용할 수 있습니다. useTransition 은 배열을 반환하는데 이는 각각 isPending, startTransition입니다.

또한 useTransition 의 매개변수로 객체를 넘겨줄 수 있는데 이를 활용한다면 Transition 을 활용한 결과를 불러올 때까지 설정한 시간까지는 이전 화면을 보여주고, 그 이후에는 isPending 을 활용해 다른 UI 를 보이게 끔 설정할 수 있습니다.

공식문서 확인 >

const [isPending, startTransition] = useTransition({ timeoutMs: 3000 });

isPending

isPending 은 boolean 값이며, Transition이 활용중 인지 알 수 있는 정보를 제공합니다.

isPending 을 활용한다면 백그라운드에서 컴포넌트가 Rendering 되는 동안 유저가 페이지와 상호작용할 수 있도록 UI를 쉽게 설정할 수 있으며 이는 사용자들에게 지금 화면이 로딩 중이거나 업데이트 되고 있다는 정보를 쉽게 제공할 수 있습니다.

{isPending && }

startTransition

startTransition 함수는 React 에서 Transition 기능을 구현할 때 사용합니다. startTransition 함수 내부에서 state 업데이트를 하면 자동으로 Transition 처리가 되며, state 업데이트될 때 우선순위 큐 내부에서 뒤로 밀려나 업데이트가 이뤄집니다.

startTransition 은 React가 UI 업데이트를 위해 복잡한 로직을 수행하면서 대기 시간이 발생하거나, 느린 네트워크 환경에서 데이터를 받아오기 위해 사용자가 기다려야 하는 상황에서 사용할 수 있습니다.

startTransition 에 넘겨지는 콜백 함수는 동기적으로 호출되며,콜백 함수 안에서 내부에서 일어나는 state 업데이트는 Transition 으로 마킹되어 React 가 업데이트를 진행할 때 후 순위로 밀려나게 됩니다.

Transition 예제

간단한 예제를 활용해 Transition 개념을 살펴봤습니다.

해당 예제에서 사용자는 검색 기능을 활용하여 무엇인가 검색하려고 합니다. 사용자가 입력하게 될 키보드 이벤트의 결과물(input 값) 은 즉각적으로 화면에 업데이트되어야 하며, 그에 비해서 보편적으로 사용자는 검색 결과가 즉시 업데이트되는 것을 기대하지 않습니다.

해당 컴포넌트에서 관리될 state는 두 가지입니다. “input value”, “search result”입니다.

useTransition 훅을 활용해 isPending 과 startTransition 을 반환받았고, isPending 을 활용해 결과 화면에 대한 UI를 활용할 수 있었으며, startTransition 을 활용해 state 업데이트의 우선순위를 구현해 Component의 효율적인 렌더링을 기대할 수 있게 됩니다.

CodeSandbox 를 통해서 살펴보기

느꼈던 점(회고)

18 버전을 통해서 React 팀이 React 를 얼마나 사랑하며 문제점을 개선하고자 하는지 느껴볼 수 있게 되었습니다. 16.8 버전에 출시된 hook 만큼 놀랄 만한 개선사항 (breaking change) 은 아니지만 다양한 부분에서 세심하게 신경 쓰고 있다는 점을 느끼게 되었습니다.

이렇게 다양한 팀 혹은 개인이 신경 쓰고 사랑하는 라이브러리 라면 이미 성숙기에 접어든 상태지만 꾸준히 안정적으로 롱런 하는 라이브러리로 남지 않을까 생각해 봅니다.

읽어봐주셔서 감사합니다. React Forever!!

참고자료

React 18 v Official Document

강동희
Developer

오픈소스컨설팅 Playce Dev Team 소속으로 오늘을 바탕으로 내일이 더 기대되는 프론트엔드 개발자 강동희입니다. 빠르게 변화하는 기술들에 관심이 많으며 지식 공유에 큰 가치를 두고 있습니다.

Leave a Reply

Your email address will not be published. Required fields are marked *

3 replies on “React 18 버전의 실상을 파헤치다.”