logo

Juniverse Dev

thumbnail

React 19 버전 뭐가 달라졌을까?

React 19 Beta 문서로 알아보는 React 19 의 변경

React.js
1028 views

2024년 4월 25일, React 19 베타 버전이 공개됐다.

라이브러리 제작자를 위한 버전으로, 아직 개발용으로는 사용하지 말라는 듯하다.

곧 정식 출시될 예정이니, 어떤 기능이 추가되었는지 살펴보자

 

원문 보기

 

새로 추가된 기능

Actions

이전까지 React 에선 Actions(비동기 전이)의 상태를 관리하고 처리하기 위해선 보류 중인 상태, 오류, 낙관적 업데이트 및 순차적 요청을 수동으로 처리해야 했다. 하지만 React 19버전에선 useTransition을 사용해 해당 흐름을 자동으로 처리할 수 있다. 

const [isPending, startTransition] = useTransition();

두 번째 반환값 startTransition의 인자로 Actions을 전달해 주면 첫번째 반환값 isPending 상태를 true로 설정하고 비동기 요청을 처리한 후에는 isPending를 false로 전환한다. 데이터가 변경되는 동안 현재 UI를 반응적으로 유지할 수 있다.

useTransition의 경우 React18 버전 부터 모습을 보였었다.

React 18 버전의 useTransition은 무거운 렌더링 작업을 뒤로 미뤄, 더 나은 UX를 제공하는 역할을 했다면 19 버전 에선 startTransition의 인자로 비동기 콜백 함수를 받아 Actions의 흐름을 처리하는 역할이 추가되었다.

새로운 Hook

useActionState

useFormStatus(React-dom)

useOptimistic

 

새로운 API

use

 

 

React Server Components

서버 없는 서버 컴포넌트 (정적 컴포넌트)

번들링하기 전에 클라이언트 응용 프로그램이나 SSR 서버와는 별도의 환경(서버)에서 구성 요소를 미리 렌더링할 수 있는 새로운 옵션.

빌드 시 CI 서버에서 한 번 실행할 수도 있고, 웹 서버를 사용하여 각 요청마다 실행할 수도 있다.

 

 

클라이언트 서버에선 useEffect 등을 사용하여 클라이언트에서 정적 데이터를 가져왔지만 이러한 패터에선  정적 콘텐츠를 렌더링하기 위해 추가로 라이브러리를 다운로드하고 구문 분석해야 하며 페이지 수명 동안 변경되지 않을 정적 콘텐츠를 렌더링하기 위해 두 번째 요청을 기다려야 했다.

서버 구성 요소를 사용하면 이러한 구성 요소를 한 번에 빌드할 수 있다:

그렇게 렌더링된 출력은 서버 측 렌더링(SSR)을 통해 HTML로 변환되어 CDN에 업로드 가능하다. 앱이 로드될 때 클라이언트는 사용된 라이브러리를 보지 못하고 렌더링된 출력만 볼 수 있다.

서버 있는 서버 컴포넌트

 페이지 요청 중에 웹 서버에서도 실행될 수 있으며, API를 빌드하지 않고 데이터 레이어에 액세스할 수 있다. 이들은 애플리케이션이 번들되기 전에 렌더링되며 데이터 및 JSX를 Client Components로 props로 전달할 수 있다.

서버 구성 요소가 없으면 클라이언트에서 useEffect 등을 사용해 동적 데이터를 가져오는 것이 일반적 이지만 Server Component를 사용하면 데이터를 읽고 구성 요소에서 렌더링할 수 있다.

번들러는 데이터, 렌더링된 서버 컴포넌트 및 동적 클라이언트 컴포넌트를 번들로 결합한다. 선택적으로 해당 번들은 서버 측 렌더링(SSR)되어 페이지에 대한 초기 HTML을 생성할 수 있다. 페이지가 로드되면 브라우저는 원래의 Note 및 Author 구성요소를 보지 못하고 렌더링된 출력만 클라이언트로 전송된다.

 

서버 컴포넌트는 서버로부터 다시 데이터를 가져와 동적으로 만들 수 있다. 서버 중심의 Multi-Page Apps의 간단한 "요청/응답" 메타 모델을 클라이언트 중심의 Single-Page Apps의 원활한 상호 작용과 결합하여 최상의 결과를 제공할 수 있다.

 

서버 컴포넌트에 상호 작용을 추가하는 방법

서버 구성 요소는 브라우저로 보내지지 않으므로 useState와 같은 대화형 API를 사용할 수 없다. 서버 구성 요소에 상호 작용을 추가하려면 "use client" 지시문을 사용하여 클라이언트 구성 요소를 사용할 수 있다.

 

참고: "use server" 로 서버 컴포넌트를 만들 수 있다는 오해가 있지만 서버 컴포넌트에 대한 지시문은 따로 없다. "use server" 지시문은 server action에 사용된다.

Async components with Server Components

서버 구성 요소는 async/await를 사용하여 컴포넌트를 작성하는 새로운 방법을 제공한다.

async 구성 요소에서 await를 사용하면 React는 프로미스가 해결될 때까지 렌더링을 일시 중지하고 대기한. 이 작업은 Suspense에 대한 스트리밍 지원으로 서버/클라이언트 경계를 넘어 작동 가능하다.

심지어 서버에서 프로미스를 생성하고 클라이언트에서 await할 수 있다.

React Server Actions

서버 액션은 클라이언트 컴포넌트가 서버에서 비동기 함수를 호출할 수 있게 한다.

"use server" 지시문과 함께 서버 액션이 정의되면, 프레임워크는 자동으로 서버 함수에 대한 참조를 생성하고 해당 참조를 클라이언트 컴포넌트에 전달한다. 클라이언트에서 해당 함수를 호출하면, React는 서버에 함수를 실행하기 위한 요청을 보내고 결과를 반환한다.

서버 액션은 서버 컴포넌트에서 생성되어 클라이언트 컴포넌트에 props로 전달되거나, 가져와서 클라이언트 컴포넌트에서 사용할 수 있다.

 

서버 컴포넌트에서 서버 액션 생성하기

서버 컴포넌트는 "use server" 지시어를 사용하여 서버 액션을 정의할 수 있다.

// Server Component
import Button from './Button';

function EmptyNote () {
  async function createNoteAction() {
    // Server Action
    'use server';
    
    await db.notes.create();
  }

  return <Button onClick={createNoteAction}/>;
}

React가 EmptyNote 서버 컴포넌트를 렌더링할 때, createNoteAction 함수에 대한 참조를 생성하고 그 참조를 Button 클라이언트 컴포넌트에 전달할 것이다. 버튼을 클릭하면, React는 해당 참조를 사용하여 서버에 createNoteAction 함수를 실행하는 요청을 보낼 것이다.

"use client";

export default function Button({onClick}) { 
  console.log(onClick); 
  // {$$typeof: Symbol.for("react.server.reference"), $$id: 'createNoteAction'}
  return <button onClick={onClick}>Create Empty Note</button>
}

클라이언트 컴포넌트에서 서버 액션 가져오기

클라이언트 컴포넌트에서 서버 액션 가져오기 클라이언트 컴포넌트는 "use server" 지시어를 사용한 파일에서 서버 액션을 가져올 수 있다.

"use server";

export async function createNoteAction() {
  await db.notes.create();
}

EmptyNote 클라이언트 컴포넌트가 번들러에 의해 빌드될 때, 번들에 createNoteAction 함수에 대한 참조가 생성된다. 버튼이 클릭되면, React는 해당 참조를 사용하여 서버에 createNoteAction 함수를 실행하는 요청을 보낼 것이다.

"use client";
import {createNoteAction} from './actions';

function EmptyNote() {
  console.log(createNoteAction);
  // {$$typeof: Symbol.for("react.server.reference"), $$id: 'createNoteAction'}
  <button onClick={createNoteAction} />
}

서버 액션과 액션 조합하기

서버 액션은 클라이언트의 액션과 함께 구성될 수 있다.

"use server";

export async function updateName(name) {
  if (!name) {
    return {error: 'Name is required'};
  }
  await db.users.updateName(name);
}
"use client";

import {updateName} from './actions';

function UpdateName() {
  const [name, setName] = useState('');
  const [error, setError] = useState(null);

  const [isPending, startTransition] = useTransition();

  const submitAction = async () => {
    startTransition(async () => {
      const {error} = await updateName(name);
      if (!error) {
        setError(error);
      } else {
        setName('');
      }
    })
  }
  
  return (
    <form action={submitAction}>
      <input type="text" name="name" disabled={isPending}/>
      {state.error && <span>Failed: {state.error}</span>}
    </form>
  )
}

이렇게 하면 클라이언트의 액션으로 감싸서 서버 액션의 isPending 상태에 접근할 수 있다.

폼 액션과 서버 액션

서버 액션은 React 19의 새로운 폼 기능과 함께 작동한다.

폼에 서버 액션을 전달하면 폼이 서버로 자동으로 제출된다.

"use client";

import {updateName} from './actions';

function UpdateName() {
  return (
    <form action={updateName}>
      <input type="text" name="name" />
    </form>
  )
}

폼 제출이 성공하면, React는 자동으로 폼을 재설정한다. useActionState를 추가하여 pending 상태, 마지막 응답에 접근하거나 progressive enhancement을 향상시킬 수 있다.

서버 액션과 useActionState 액션

pending 상태와 마지막으로 반환된 응답에만 접근해야 하는 일반적인 경우에 서버 액션을 useActionState와 함께 구성할 수 있다

"use client";

import {updateName} from './actions';

function UpdateName() {
  const [submitAction, state, isPending] = useActionState(updateName, {error: null});

  return (
    <form action={submitAction}>
      <input type="text" name="name" disabled={isPending}/>
      {state.error && <span>Failed: {state.error}</span>}
    </form>
  );
}

useActionState를 서버 액션과 함께 사용하면, React는 또한 hydration이 완료되기 전에 입력된 폼 제출을 자동으로 재생한다. 이는 사용자가 앱이 hydrated 되기 전에도 앱과 상호작용할 수 있음을 의미한다.

useActionState와 progressive enhancement

서버 액션은 또한 useActionState의 세 번째 인수로  progressive enhancement 를 지원한다.

"use client";

import {updateName} from './actions';

function UpdateName() {
  const [, submitAction] = useActionState(updateName, null, `/name/update`);

  return (
    <form action={submitAction}>
      ...
    </form>
  );
}

permalink가 useActionState에 제공되면, JavaScript 번들이 로드되기 전에 폼이 제출되면 React는 제공된 URL로 리다이렉트한다.

개선된 기능

Ref를 props로 접근할 수 있다(드디어)

React 19에서는 이제 함수형 컴포넌트에서 ref를 prop으로 접근할 수 있다. React 18 까지는 구조적인 문제 때문에 forwardRef API를 사용해야만 했지만 앞으론  컴포넌트가 새로운 ref prop을 사용하도록 자동으로 업데이트하는 코드가 배포될 예정이다. 이후 버전에서는 forwardRef 는 아예 삭제 될 수 도 있다.

hydration 에러 로그 개선

이전엔 hydration 오류 발생시 일치하지 않는 정보 없이 여러 오류를 로그로 남겼다

React 19 부터는 불일치의 차이점을 보여주는 단일 메시지를 로그로 한꺼번에 보여준다.

 

Context 자체를 Provider 로써 사용할 수 있게 되었다

이제 createContext로 생성한 Context를 사용할 때  Compound Components 패턴처럼 사용하지 않고 바로 사용할 수 있다.

const ThemeContext = createContext('');

function App({children}) {
  return (
    <ThemeContext value="dark">
      {children}
    </ThemeContext>
  );  
}

Ref 자동 Clean up

이제 ref 콜백에서 정리 함수를 반환하는 것을 지원한다.

<input
  ref={(ref) => {
    // ref 생성됨

    // 새로운 기능: 엘리먼트가 DOM에서 제거될 때
    // ref를 재설정하기 위한 정리 함수를 반환합니다.
    return () => {
      // ref 정리
    };
  }}
/>

컴포넌트가 언마운트될 때, 기존에는 useEffect 에서 정리해줘야 했지만 이제 ref를 생성할 때 cleanup 함수를 작성할 수 있다.

React는 ref 콜백에서 반환된 cleanup 함수를 호출한다. 이는 DOM ref, 클래스 컴포넌트의 ref, 그리고 useImperativeHandle에 대해서도 작동한다.

이전에는 React가 컴포넌트를 언마운트할 때 ref 함수에 null을 전달했지만.  ref가 정리 함수를 반환한다면 이 단계는 필요 없어진다. 때문에 앞으로의 버전에서는 컴포넌트를 언마운트할 때 ref에 null을 전달하는 것을 폐기할 예정이라고 한다.

useDeferredValue에 초기값을 설정 할 수 있게 되었다

useDeferredValue에 initialValue 옵션이 추가됐다.

function Search({ deferredValue }) {
  // 초기 렌더링 시 값은 ''입니다.
  // 그런 다음 deferredValue로 다시 렌더링이 예약됩니다.
  const value = useDeferredValue(deferredValue, '');
  
  return (
    <Results query={value} />
  );
}

useDeferredValue는 UI 업데이트를 연기할 수 있는 Hook으로 비동기적으로 업데이트되는 값을 처리하는 데 사용된다. 현재 렌더링 사이클에서는 이전 값을 사용하고, 다음 렌더링 사이클에서 새 값을 사용해인터랙션에 반응하면서도 렌더링 작업을 중단시키지 않고 부드럽게 업데이트할 수 있게 한다.

React 19 버전부터 initialValue를 제공하면, useDeferredValue는 컴포넌트의 초기 렌더링에 해당 값을 반환하고, 이후 백그라운드에서 deferredValue로 다시 렌더링을 예약한다.

Metadata지원

React 19에서는 컴포넌트에서 문서 메타데이터 태그를 네이티브로 렌더링하는 기능을 추가하고 있다.

function BlogPost({ post }) {
  return (
    <article>
      <h1>{post.title}</h1>
      <title>{post.title}</title>
      <meta name="author" content="Josh" />
      <link rel="author" href="https://twitter.com/joshcstory/" />
      <meta name="keywords" content={post.keywords} />
      <p>
        Eee equals em-see-squared...
      </p>
    </article>
  );
}

React가 이 컴포넌트를 렌더링할 때, <title>, <link>, <meta> 태그를 보고 자동으로 문서의 <head> 섹션으로 이동시킨다.

이러한 메타데이터 태그를 네이티브로 지원함으로써, 클라이언트 전용 앱, 스트리밍 SSR, 그리고 서버 컴포넌트와 같은 기능과 함께 작동하도록 보장할 수 있다.

물론 react-hemet 등의 외부 라이브러리도 여전히 사용할 수 있다.

StyleSheet 지원

React 19 에서는 클라이언트에서의 Concurrent Rendering 및 서버에서의 Streaming Rendering에 대해 더 깊은 통합을 제공하기 위해 내장된 스타일시트 지원을 제공한다.  React에게 스타일시트의 우선순위를 알려주면, React가 DOM에 스타일시트의 삽입 순서를 관리하고, 스타일 규칙에 의존하는 콘텐츠가 표시되기 전에 (외부일 경우) 스타일시트를 로드한다.

function ComponentOne() {
  return (
    <Suspense fallback="loading...">
      <link rel="stylesheet" href="foo" precedence="default" />
      <link rel="stylesheet" href="bar" precedence="high" />
      <article class="foo-class bar-class">
        {...}
      </article>
    </Suspense>
  )
}

function ComponentTwo() {
  return (
    <div>
      <p>{...}</p>
      <link rel="stylesheet" href="baz" precedence="default" />  <-- foo & bar 사이에 삽입됩니다
    </div>
  )
}

스타일 라이브러리 및 번들러와의 스타일 통합도 이 새로운 기능을 채택할 수 있으므로, 직접적으로 스타일시트를 렌더링하지 않더라도 이 기능을 사용하도록 도구를 업그레이드할 경우 이점을 얻을 수 있다.

Async Script 지원

React 19에서는 비동기 스크립트를 더 잘 지원하기 위해 실제로 스크립트에 의존하는 컴포넌트 내부에서 스크립트를 어디에서든 렌더링할 수 있도록 하여, 스크립트 인스턴스의 이동 및 중복 제거를 관리하지 않아도 된다.

function MyComponent() {
  return (
    <div>
      <script async={true} src="..." />
      Hello World
    </div>
  )
}

function App() {
  <html>
    <body>
      <MyComponent>
      ...
      <MyComponent> // 문서에 중복 스크립트가 생성되지 않습니다
    </body>
  </html>
}

resources 사전 로딩 지원

React 19에는 브라우저 리소스를 로드하고 사전 로드하는 새로운 API가 포함되어 있어, 비효율적인 리소스 로딩을 최소화 할 수 있다.

import { prefetchDNS, preconnect, preload, preinit } from 'react-dom'
function MyComponent() {
  preinit('https://.../path/to/some/script.js', {as: 'script' }) // 이 스크립트를 미리 로드하고 즉시 실행합니다
  preload('https://.../path/to/font.woff', { as: 'font' }) // 이 폰트를 사전 로드합니다
  preload('https://.../path/to/stylesheet.css', { as: 'style' }) // 이 스타일시트를 사전 로드합니다
  prefetchDNS('https://...') // 이 호스트로 실제로 요청하지 않을 때
  preconnect('https://...') // 요청할 것이지만 무엇인지 확실하지 않을 때
}

위 API를 사용하면 다음과 같은 이점을 얻을 수 있다.

third-party 스크립트 및 익스텐션 호환성

hydration 중에 클라이언트에서 렌더링되는 요소가 서버에서 찾은 HTML 요소와 일치하지 않는 경우, React는 컨텐츠를 수정하기 위해 클라이언트 재렌더링을 강제한다. 이전에는 제 3자 스크립트나 브라우저 확장 기능이 삽입된 요소가 있으면 불일치 오류와 클라이언트 렌더링이 발생했다.

React 19에서는 <head>와 <body>에 예상치 못한 태그가 발견되면 불일치 오류를 피하고 건너뛴다.  React가 관련 없는 하이드레이션 불일치로 전체 문서를 다시 렌더링해야 할 경우, 제 3자 스크립트나 브라우저 확장 기능이 삽입한 스타일시트는 그대로 유지된다.

에러 로그 개선

hydration 에러 로그 개선에서 기술된 내용과 유사하게 React 19에서는 다른 오류 처리도 개선하여 중복을 제거하고 catch되거나 catch되지 않은 오류를 처리하는 옵션을 제공하다.

Custom Elements 지원

React19에서는 사용자 정의 요소에 대한 완전한 지원을 추가했다.

 이전까지 React는 인식되지 않는 props를 속성(attribute)이 아닌 속성(properties)으로 처리했기 때문에 Custom Element를 사용하기 어려웠다.

React 19에서는 다음 전략으로 CSR와 SSR에서 작동하는 속성을 지원하도록 추가했다.