Front-End(Web)/React - 프레임워크(React, Next)

[React] React 18 의 달라진 점들

ttaeng_99 2022. 4. 18. 01:15
반응형

올해 3월, React v18.0 이 정식 릴리즈가 되었다. (2020년 10월, v17.0 이 릴리즈된지 약 1년반)

업데이트 소식과 베타버전은 작년부터 리포트 되어왔으며, 최근 정식 릴리즈됨에 따라 이 포스팅을 한 번 정리하면 좋겠다는 생각이 들었다.

 

지난 React v17.0 은 호환성, 안정화 정도의 간단한 업데이트라면, v18.0은 주요 기능들이 많이 추가되어 이례적으로 작년에 알파버전까지 릴리즈하기도 했다.

 

이번 기회를 통해 React를 버전업하는 방법, 그리고 이번에 추가된 주요 기능들에 대해 알아보도록 하겠다!

 

 


 

💙 React 18 시작하기

이번 React 18 버전에서 추가된 주요기능은 크게 아래 3가지로 소개된다.

  1. automatic batching : 여러 개의 상태 업데이트를 한 번에 리렌더링
  2. concurrent features : 동시성 모드(더 나은 사용자 경험을 위한 UI개선) 를 위한 핵심 기능 중 일부가 추가된다. 
  3. SSR support for Suspense : <Suspense>를 통한 새로운 SSR 아키텍쳐 

React 팀은 동시성 모드를 중점적으로 한 앞으로의 개발 로드맵을 공유했으며, React 18 을 다음 메이저 버전으로 가져갈 것이라고 선언했다.

 

 

- 적용하기

먼저, React 기존버전을 v18.0으로 업데이트하는 방법을 알아보겠다.

 

npm install react@18 react-dom@18

먼저, React와 ReactDOM을 최신버전으로 설치해준다.

 

// ~React 17
import ReactDOM from 'react-dom'
import App from './App'

ReactDOM.render(<App />, document.getElementById('root'));
// React 18
import ReactDOM from 'react-dom/client'		// client 추가
import App from './App'

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

다음으로, index의 ReactDOM 문법을 수정해준다.

react-dom의 render() 함수는 deprecated 됬으며, react-dom/client의 createRoot() 함수로 root를 생성한 뒤 이를 render() 한다.

 

React 18 역시 애플리케이션의 수정을 최소화하며 기존 프로젝트에 features들을 적용하게끔 설계되었다.

 

 

 

💙 React 18 주요 특징

 

1. Automatic Batching (자동 배칭)

Batching(배치)란, React가 더 나은 성능을 위해 여러 개의 상태 업데이트를 한 번의 리렌더링으로 묶는 작업이다.

React 18의 즉시 사용 가능한 개선점 중, 기존에 지원되지 않던 배칭들을 지원하고 있다.

 

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

  function handleClick() {
    setCount(c => c + 1); // 아직 리렌더링 되지 않습니다.
    setFlag(f => !f); // 아직 리렌더링 되지 않습니다.
    // 리액트는 오직 마지막에만 리렌더링을 한 번 수행합니다. (배치 적용)
  }

  return (
    <div>
      <button onClick={handleClick}>Next</button>
      <h1 style={{ color: flag ? "blue" : "black" }}>{count}</h1>
    </div>
  );
}

위 예시의 handleClick() 메서드를 참고하자. 이는 count, flag 두 개의 state를 한 번에 업데이트하는 함수이다.

Javascript는 싱글 스레드 언어이기에 위 로직은 순차적으로 일어나며, 그때마다 일일이 리렌더링을 하는 것은 비효율적이다.

이 때, React는 배치를 수행해서 두 번의 setState를 한 번의 리렌더링으로 처리하며 효율성과 안정성을 보장한다.

 

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

  function handleClick() {
    fetchSomething().then(() => {
      // 리액트 17 및 그 이전 버전에서는 배치가 수행되지 않습니다. 왜냐하면
      // 이 코드들은 이벤트 이후의 콜백에서 실행되기 때문입니다.
      setCount(c => c + 1); // 리렌더링 
      setFlag(f => !f); // 리렌더링
    });
  }

  return (
    <div>
      <button onClick={handleClick}>Next</button>
      <h1 style={{ color: flag ? "blue" : "black" }}>{count}</h1>
    </div>
  );
}

단, 이는 기존에 배치가 수행되지 않는 예외코드다. React 17까지는 이벤트 핸들러 함수 내에서만 배칭을 수행했다.

하지만, 내부에 Promise, setTimeout 과 같은 비동기 로직이나, native Event Handler로 상태를 업데이트할 경우배칭이 수행되지 않았다.

 

하지만, React 18의 자동 배칭(automatic batching)이 추가되면서, createRoot를 시작으로 모든 업데이트는 자동적으로 배칭이 이루어진다.

이를 통해, 우리는 React 프로젝트의 렌더링 작업을 줄여 어플리케이션의 성능을 고도화할 수 있다.

function handleClick() {
  setCount(c => c + 1);
  setFlag(f => !f);
  // React will only re-render once at the end (that's batching!)
}


setTimeout(() => {
  setCount(c => c + 1);
  setFlag(f => !f);
  // React will only re-render once at the end (that's batching!)
}, 1000);


fetch(/*...*/).then(() => {
  setCount(c => c + 1);
  setFlag(f => !f);
  // React will only re-render once at the end (that's batching!)
})


elm.addEventListener('click', () => {
  setCount(c => c + 1);
  setFlag(f => !f);
  // React will only re-render once at the end (that's batching!)
});

React 18부터 위 예제의 각 코드들은, 자동 배칭이 적용되어 1번의 리렌더링으로 2개의 상태 업데이트가 반영된다.

 

 

* flushSync

 

만약, 자동 배칭을 원하지 않는다면, flushSync() 로 setter 함수를 감싸 별도의 리렌더링을 유발하면 된다.

import { flushSync } from 'react-dom';

function handleClick() {
  flushSync(() => {
    setCounter(c => c + 1);
  });  // 리액트는 즉시 DOM을 업데이트합니다.

  flushSync(() => {
    setFlag(f => !f);
  });  // 리액트는 즉시 DOM을 업데이트합니다.
}

 

 

 

2. Concurrent Features (동시성 기능)

동시성 기능은 React가 추진하고 있는 차기 핵심 기능이다. (실험버전까지는 동시성 모드로 소개되었으나, 18에서 동시성 기능으로 개정)

 

Javascript는 싱글 스레드(single-threaded) 언어로 하나의 코드가 끝나야 다음 코드가 실행된다.

React 역시 같은 원리로 동작하며, 특히 UI 렌더링 도중에는 다른 작업이 진행되지 않기 때문DOM Tree가 거대한 경우 조작과 표현의 간극이 발생할 수 있다.

 

동시성 기능은 이러한 큰 작업을 작은 여러개의 독립적인 작업으로 나누는 프로그래밍 구조로, 싱글 스레드의 단점을 보완하는 솔루션이다.

이를 통해 유저에게 더 나은 UX(사용자 경험)를 줄 뿐만 아니라, 작업의 우선순위를 조정하거나 일부만 렌더링할 수 있다.

(Server Component, Built-in-cash 등의 Data Fetching 솔루션은 추후 React 18.x 에 적용될 예정이라고 한다.)

* React Concurrent Mode에 대한 더 자세한 설명을 해당 링크를 참고하자!

 

 

* 상태 업데이트 분류

 

동시성 기능을 이해하기 위해, 먼저 2가지 상태 업데이트의 분류를 이해해야 한다.

  • 긴급 업데이트(Urgent) : 직접적인 상호작용 반영 (타이핑, 호버, 스크롤 등). 업데이트가 즉각적으로 일어나는 대상.
  • 전환 업데이트(Transition) : 하나의 뷰에서 다른 뷰로 UI 전환. 상태값 변화에 따른 모든 업데이트가 뷰에 즉시 반영되지 않아도 됨.

위와 같은 검색창이 좋은 예시이다.

<input> 필드의 경우, 키 입력이 올바르게 된 것을 즉각적으로 보여줘야 하는 긴급 업데이트 영역이다.

반면, 하단 자동완성 리스트입력값에 따라 내용이 뒤늦게 바뀌어도 되는 전환 업데이트 영역이다.

 

여기서 중요한 점은, 전환 업데이트가 긴급 업데이트를 방해하면 안된다는 것이다!

React 17 까지는 상태 업데이트를 긴급/전환 으로 분류하는 방법이 없었다. (모두 긴급 업데이트)

그렇기에, 전환 업데이트를 setTimeout, throttle, debounce 등의 테크닉으로 우회하는 차선책을 택할 수 밖에 없었다.

 

React 18 부터는 이를 지원하는 Hooks들을 제공함으로써 전환 업데이트를 명시적으로 구분하여 상태 업데이트를 할 수 있다.

 

 

1) startTransition

 

startTransition() Hooks 는 상태 업데이트를 전환 업데이트로 반영하는 메서드이다.

React는 모든 업데이트를 기본적으로 긴급 업데이트로 취급하며, startTransition() 으로 감싼 업데이트는 전환 업데이트로 설정할 수 있으며, 이를 통해 사용자에게 상대적으로 중요도가 낮은 UI 업데이트의 우선순위를 낮출 수 있다.

  • 전환 업데이트는 느린 렌더링에 많이 사용된다. 작업량이 많아 결과를 표현하는 UI 전환의 시간이 오래 걸리는 경우이다.
  • 전환 업데이트는 느린 네트워크에 많이 사용된다. 데이터 호출량이 많은 UI에 적용되며, Suspense와 연계된다.

 

 

2) useTransition

 

useTransition() Hooks 역시 전환 업데이트를 지원하며, startTransition() Hooks와 더불어 isPending(트랜지션 상태)를 지원한다.

Loading 상태의 추적이나 제어가 필요할 경우 이 Hooks를 사용하면 된다.

 

 

3) useDeferredValue

 

useDefferedValue() Hooks 는 렌더링 우선순위가 낮은 상태값의 업데이트를 지연시키는 메서드다.

이는 사용자 인터페이스를 기반으로 데이터 조회를 기다릴 때 UI를 반응적으로 유지하기 좋은 기능이다.

function App() {
  const [text, setText] = useState("hello");
  const deferredText = useDeferredValue(text, { timeoutMs: 2000 });

  return (
    <div className="App">
      {/* input에 현재 텍스트를 계속 전달합니다. */}
      <input value={text} onChange={handleChange} />
      ...
      {/* 하지만 이 목록은 필요한 경우 "뒤처질" 수 있습니다. */}
      <MySlowList text={deferredText} />
    </div>
  );
 }

 

 

 

3. SSR support for Suspense : 새로운 서버사이드 렌더링 아키텍쳐

이는 우리가 아는 기본적인 서버 사이드 렌더링(Server Side Rendering, SSR) 로직의 연장선이다. 아래는 기본적인 SSR 의 순서이다.

  1.  서버에서 UI를 그리기 위해 필요한 데이터를 Fetching 한다.
  2.  서버가 전체 어플리케이션을 HTML로 렌더링하고, 클라이언트로 Response를 보낸다.
  3.  클라이언트는 어플리케이션에 필요한 Javascript Bundle을 다운로드한다.
  4.  클라이언트Javascript 로직을 HTML에 연결하면서 마무리된다.

위 프로세스의 문제는, 어플리케이션 내에서 각 단계가 끝나야 다음 단계를 시작할 수 있다는 점이다.

그렇기에, 어플리케이션이 거대해질수록 한 부분의 렌더링만 지연되도 전체적인 페이지의 표현이 느려진다.

 

React 18은 <Suspense>를 통해 어플리케이션을 더 작은 독립적인 유닛으로 나누어서 별개의 렌더링을 거치는 것이다.

 

 

* React 17 까지는

<Layout>
  <NavBar />
  <Sidebar />
  <RightPane>
    <Post />
    <Comments />
  </RightPane>
</Layout>

먼저, 지금까지 React에서 위 레이아웃을 SSR로 그리는 과정이다.

 

1) 서버에서 Data Fetching

일반적인 SPA(Single Page Application) 에서는 Javascript 로딩 전까지 화면이 표현되지 않는다.

그래서, SPA 프레임워크들은 SSR을 지원해서, HTML을 미리 Response로 보내줘서 미리 화면을 표현하게 된다. (상호작용 불가)

 

 

2) 서버에서 HTML Response

회색 영역은 상호작용이 불가능한 HTML

서버에서 Response로 보낸 HTML을 클라이언트가 표현한 모습이다.

단, Javascript가 아직 연동되지 않아 사용자 간 상호작용이 불가하다. (link, form, input 등 내장기능만 가능)

 

 

3) 클라이언트에서 JS번들 로드

 

 

4) 클라이언트에서 HTML과 JS로직 연결(Hydration)

녹색 영역은 Hydration이 끝나 상호작용이 가능한 React 컴포넌트 영역

JS 로딩이 끝나면, React는 메모리 단에서 컴포넌트 트리를 렌더링한다.

그 후, 모든 JS로직을 HTML에 연결한다. 이 작업을 Hydration(수화) 라고 한다.

 

여기서 알 수 있듯이 SSR은 페이지의 로딩이 빠른게 아닌, 로딩 중에만 임시화면을 표출해서 느리지 않게끔 인지만 시켜주는 것이다.

하지만, 여기서도 아래와 같은 문제가 생길 수 있다.

  1. 특정 컴포넌트가 Data Fetching 이 오래 걸린다면, 전체적인 렌더링이 지연된다. (병목현상)
  2. 특정 컴포넌트의 코드량이 크다면, 여기의 JS번들이 로드되기 전에 Hydration에 들어갈 수 없다.
  3. 특정 컴포넌트의 로직이 복잡하다면 JS번들 로딩과 Hydration이 그만큼 느려질 수 있다.

 

 

* React 18 부터는

 

React 18 부터는 <Suspense> 를 통한 Selective Hydration이 가능하다. 렌더링 비용이 큰 부분을 <Suspense>로 감싸 별도의 Hydration이 가능한 것이다.

<Layout>
  <NavBar />
  <Sidebar />
  <RightPane>
    <Post />
    <Suspense fallback={<Spinner />}>
      <Comments />
    </Suspense>
  </RightPane>
</Layout>

여기서, 우측 하단의 렌더링이 상대적으로 느린 <Comments>를 <Suspense>로 감싸서 선택적 Hydration을 적용했다.

또한, fallback 옵션에 대신 노출할 컴포넌트인 <Spinner> 를 설정해서 해당 컴포넌트가 렌더링되기 전에 fallback 컴포넌트를 노출한다.

 

 

1) <Suspense> - Data Fetching 지연 해결

이렇게, <Comments> 컴포넌트의 Data Fetching 전에는 fallback을, 이후에는 컴포넌트를 정상으로 노출한다.

여기선, Data Fetching이 지연되는 문제를 해결했다.

 

 

2) React.lazy - JS코드가 거대하고 복잡해서 로드 지연 해결

 

React.lazy는 동적 import를 통해 컴포넌트를 렌더링하며, 이를 Lazy Component 라고 한다.

이는, Code Splitting을 위해 v16.6에 추가된 기능이며, <Suspense> 하위에서 렌더링되어야 한다. 

 

import { lazy } from 'react';

// import Comments from '@/views/components/Comments.js' - 기존
const Comments = lazy(() => import('./Comments.js'));

<Suspense fallback={<Spinner />}>
  <Comments />
</Suspense>

React 18부터는 <Suspense> 는 내부의 Lazy Component가 업로드되기 전에 앱을 선택적으로 Hydrate 해준다.

만약, JS번들이 HTML 코드보다 일찍 로드된다면, 해당 컴포넌트를 기다리지 않고 나머지 영역을 먼저 hydrate 할 것이다.

이렇게 어플리케이션은 좀 더 빠른 UI/UX를 사용자에게 제공할 수 있게 된다.

 

무엇보다, React 18에서의 가장 큰 변화는 기존에 불가능했던 Suspense, Lazy Component의 SSR 에서의 사용이 가능해졌다는 것이다! (위, Code Splitting 링크를 참조하면 기존에는 SSR이 불가해 Loadable Component로 대체해왔다.)

 

서버단에서 HTML 스트리밍을 담당하는 기존의 renderToString() 대신, pipeToNodeWritable API가 React 18에 새롭게 추가된다.

기존의 renderToNodeStream이 Data Fetching을 기다릴 수 없었던 부분을 개선하여 위 기능들이 SSR도 가능도록 한 것이다.

 

 

* 사용자 친화적 Hydration

 

또한, 복수의 Lazy Component가 Hydration 되는 경우 우선순위를 지정할 수 있는 기능이 추가되었다.

<Layout>
  <NavBar />
  <Suspense fallback={<Spinner />}>
    <Sidebar />
  </Suspense>
  <RightPane>
    <Post />
    <Suspense fallback={<Spinner />}>
      <Comments />
    </Suspense>
  </RightPane>
</Layout>

위 예제는, 다른 부분은 Hydration이 끝난 중에도 2개의 Lazy Component(<Sidebar>, <Comments>)를 렌더링하는 경우다.

 

통상은, 컴포넌트 트리의 상단에 있는 <Sidebar> 먼저 Hydration이 진행된다.

 

만약, 이 때 <Comments> 컴포넌트에서 유저 상호작용이 발생한다고 가정해보자. (클릭 이벤트)

그러면, React는 해당 컴포넌트의 우선순위가 높다고 판단하여 기존의 Hydration을 중단, <Comments> 의 Hydration을 시작한다.

또한, 이 때 클릭 이벤트를 기록해 두었다가, Hydration이 마무리되면 이를 다시 실행하여 컴포넌트가 반응하게끔 하는 유용한 기능이다!


언제나 그렇듯, 간단히 정리하려고 시작한 글은 정말 두서없이 길어진다.. 😂😂

 

보통 2~3개의 포스팅을 메인으로, 내가 정리하고 싶은 내용들을 블로그에 기록해 나가지만,

중간중간 모르는 개념이나 의문이 생기는 부분들에 대해 검색해보고 내용을 추가하다보니 글 하나하나가 비대해지기 마련이다.

 

이번 포스팅 역시, React 18의 주요 기능인 3가지에 대해 훑는 정도로 마무리하려고 했으나,

Concurrent Features, SSR 의 경우엔 이전 버전(약 v16~)부터 거론되어온 개념들을 좀 더 안정적이고 사용자 친화적으로 제공하기 위한 기능들이 추가 및 보완된 것이다.

그렇다보니, 동시성이나 서버사이드 렌더링 등의 개념을 복기해가면서 글을 적어가다보니 작성기간도 길어졌다.

 

그래도, 항상 얘기하듯 내 정리와 그 중간중간의 의문들이 이 글을 읽는 분들께 적잖은 도움이 됬으면 하는 바램에 오늘도 글을 마무리한다!

이제 남은 토이 프로젝트를 마무리하고, 계획했던 공부들을 다시 시작해야겠다!

 

 

📎 출처

- [React v18.0] React 공식문서(블로그) : https://reactjs.org/blog/2022/03/29/react-v18.html  

- [React 18 Update] Academind 유튜브 영상 : https://www.youtube.com/watch?v=N0DhCV_-Qbg 

- [React 18 번역] 최철헌 님의 블로그 : https://medium.com/naver-place-dev/react-18%EC%9D%84-%EC%A4%80%EB%B9%84%ED%95%98%EC%84%B8%EC%9A%94-8603c36ddb25  

- [React v8 번역] kimsangyeon 님의 블로그 : https://kimsangyeon-github-io.vercel.app/blog/2022-01-23-introducing-react-18  

- [React 18 달라진 점] : DevOwen 님의 블로그 : https://devowen.com/410

- [Concurrent Features API] Progress Telerik : https://www.telerik.com/blogs/concurrent-rendering-react-18

반응형