React 19

정식 릴리즈 된 리액트19에 대해 알아봅시다!

2024-12-30

387

22분

soaple


안녕하세요, 소플입니다.

얼마 전인 2024년 12월 6일에 드디어 리액트 버전19가 정식으로 릴리즈 되었습니다 🎉

리액트 18이 출시된 이후 약 2년 반만의 메이저 버전 업데이트라서 더욱 더 기대가 큰 것 같습니다.

이번 매거진에서는 리액트 19에서 새롭게 등장한 기능들과 업데이트 되는 내용들에 대해,

아래와 같이 총 세 개의 파트로 나눠서 한 번 살펴보도록 하겠습니다.


Part 1. 리액트 19의 새로운 기능

1.1 Actions

리액트 19에서는 Actions라는 새로운 기능과 Actions를 위한 훅들이 함께 추가되었습니다.

1.1.1 Actions가 등장한 배경

먼저 리액트에서 비동기 요청을 보내는 경우를 한 번 생각해볼까요?

비동기 요청은 일반적으로 아래와 같은 세 가지 상태를 포함합니다.

  • isPending: 요청이 진행 중인지 여부를 담기 위한 상태
  • data: 응답으로 반환받은 데이터를 담기위한 상태
  • error: 에러가 발생할 경우 에러를 담기 위한 상태

그래서 기존에 리액트 컴포넌트에서 useState() 훅을 사용해서 비동기 요청을 처리하려면 아래와 같이 코드를 작성해야 했습니다.

function UpdateName() {
    const [name, setName] = useState("");
    const [isPending, setIsPending] = useState(false);
    const [error, setError] = useState(null);

    const handleSubmit = async () => {
        setIsPending(true); // 요청 대기 중으로 변경
        const error = await updateName(name);
        setIsPending(false); // 요청 완료로 변경
        if (error) {
            setError(error); // 에러 발생 시 에러 상태 업데이트
            return;
        }
        redirect("/path");
    };

    return (
        <div>
            <input value={name} onChange={(event) => setName(event.target.value)} />
            <button onClick={handleSubmit} disabled={isPending}>
                Update
            </button>
            {error && <p>{error}</p>}
        </div>
    );
}

하지만 일반적으로 컴포넌트에서 요청을 보내는 경우가 한 두번이 아니라 굉장히 많기 때문에,

이러한 모든 비동기 요청에 대해 state를 선언하고 관리하는 것이 굉장히 불편했었죠.

그래서 상태 관리 라이브러리를 사용해서 비동기 요청과 관련된 모든 상태를 한 곳에서 관리하는 방법으로 구현하기도 하고,

별도로 서버 상태를 관리하기 위한 SWR이나 TanStack Query 같은 라이브러리를 사용해서 처리하기도 했습니다.

그래서 리액트 19에서는 Actions라는 새로운 기능과 관련된 훅들을 통해 비동기 요청을 편리하게 다룰 수 있게 한 것입니다.

1.1.2 Actions란?

그렇다면 정확히 리액트에서 말하는 Actions란 무엇일까요?

Actions은 async transitions을 사용하는 함수를 의미하며, 데이터를 제출(submit)하는 과정을 아래와 같이 관리합니다.

  • 대기 상태 (Pending state)
    • Actions는 요청이 시작될 때 Pending 상태를 제공하며, 최종 상태 업데이트가 커밋될 때 자동으로 초기화됨
  • 낙관적 업데이트 (Optimistic updates)
    • Actions는 새로운 useOptimistic() 훅을 통해 요청이 제출되는 동안 사용자에게 즉각적인 피드백을 제공할 수 있음
  • 오류 처리 (Error handling)
    • Actions는 오류 처리를 제공하여 요청이 실패할 경우 Error Boundaries를 표시할 수 있으며, 낙관적 업데이트를 원래 값으로 자동 복구함
  • 폼 (Forms)
    • <form> 요소는 이제 actionformAction 속성에 함수를 전달하는 것을 지원함
    • action 속성에 함수를 전달하면 기본적으로 Actions를 사용하며, 제출 후 폼을 자동으로 초기화함

그리고 Actions를 지원하기 위한 기능들이 아래와 같이 추가되었습니다.

  • React에 추가된 기능
    • useOptimistic(): Optimistic updates를 처리하기 위한 훅
    • useActionState(): 일반적인 Actions(비동기 요청)를 편리하게 다루기 위한 훅
  • React DOM에 추가된 기능
    • <form> Actions: form submit 과정을 자동으로 관리
    • useFormStatus(): form에서 사용되는 일반적인 Actions를 편리하게 다루기 위한 훅

아래 코드는 <form> Actions와 useActionState() 훅을 사용하여 form submit을 처리하는 예시 코드입니다.

function ChangeName({ name, setName }) {
    const [error, submitAction, isPending] = useActionState(
        async (previousState, formData) => {
            const error = await updateName(formData.get("name"));
            if (error) {
                return error;
            }
            redirect("/path");
            return null;
        },
        null,
    );

    return (
        <form action={submitAction}>
            <input type="text" name="name" />
            <button type="submit" disabled={isPending}>Update</button>
            {error && <p>{error}</p>}
        </form>
    );
}

이처럼 Actions를 활용하면 비동기 요청을 간결한 코드로 깔끔하게 처리할 수 있습니다.

1.2 Actions를 위해 react 패키지에 추가된 기능

지금부터는 Actions를 위해 react 패키지에 추가된 기능을 하나씩 살펴보겠습니다.

1.2.1 useOptimistic()

useOptimistic() 훅은 우리말로 낙관적 업데이트라고 부르는 Optimistic Update를 위한 훅입니다.

Optimistic Update란 어떤 데이터를 업데이트 할 때 서버로부터 응답이 오기 전에 먼저 최종 결과를 보여주는 것을 의미합니다.

예를 들면, 어떤 게시물에 Like 버튼을 눌렀을 때 서버로부터 응답이 오기 전에 Like 버튼이 눌린 상태로 업데이트 하는 것이죠.

기존에는 이러한 Optimistic Update를 개발자가 직접 구현하려면 꽤 귀찮은 작업들을 해줘야 했습니다.

그래서 리액트 19에서는 useOptimistic() 훅을 통해 Optimistic Update를 쉽게 구현할 수 있게 만든 것입니다.

아래 코드는 useOptimistic() 훅을 사용한 예시 코드입니다.

function ChangeName({ currentName, onUpdateName }) {
    const [optimisticName, setOptimisticName] = useOptimistic(currentName);

    const submitAction = async formData => {
        const newName = formData.get("name");
        setOptimisticName(newName); // 새로운 데이터를 즉시 업데이트
        const updatedName = await updateName(newName); // 서버로 요청 전송
        onUpdateName(updatedName);
    };

    return (
        <form action={submitAction}>
            <p>Your name is: {optimisticName}</p>
            <p>
                <label>Change Name:</label>
                <input
                    type="text"
                    name="name"
                    disabled={currentName !== optimisticName}
                />
            </p>
        </form>
    );
}

useOptimistic() 훅에서 제공하는 set함수를 통해 서버에 요청을 보내기 전에 즉시 UI를 업데이트 합니다.

그리고 만약 요청이 실패했을 경우 이전 값으로 자동으로 되돌려 줍니다.

🔗 useOptimistic() 훅에 대한 자세한 내용은 아래 공식문서 링크를 참고하시기 바랍니다.
https://react.dev/reference/react/useOptimistic

1.2.2 useActionState()

useActionState() 훅은 일반적인 Actions(비동기 요청)를 편리하게 다루기 위한 훅입니다.

아래 코드는 useActionState() 훅을 사용한 예시 코드입니다.

'use client';

import { useActionState } from 'react';

async function increment(previousState, formData) {
    return await new Promise((resolve, reject) => {
        setTimeout(() => {
            resolve(previousState + 1);
        }, 1000);
    });
}

function Page() {
    const [state, formAction, isPending] = useActionState(increment, 0);

    return (
        <form>
            <div>
                {state}
                <button formAction={formAction}>Increment</button>
            </div>
            {isPending && 'Loading...'}
        </form>
    );
}

export default Page;

useActionState() 훅은 Action이라고 부르는 함수와 초기값을 파라미터로 받아서 아래와 같은 값이 들어있는 배열을 리턴합니다.

  • state (현재 상태 값)
    • 첫 번째 렌더링 시, 초기값과 동일
    • Action이 호출된 이후에는 Action에서 리턴한 값이 됨
  • formAction (wrapped Action이라고도 부름)
    • <form>action prop 또는 <button>formAction prop으로 전달할 수 있는 함수
  • isPending
    • 요청이 진행 중인지를 나타내는 boolean

이를 통해 Actions(비동기 요청)를 편리하게 처리할 수 있습니다.

🔗 useActionState() 훅에 대한 자세한 내용은 아래 공식문서 링크를 참고하시기 바랍니다.
https://react.dev/reference/react/useActionState

1.3 Actions를 위해 react-dom 패키지에 추가된 기능

지금부터는 Actions를 위해 react-dom 패키지에 추가된 기능을 하나씩 살펴보겠습니다.

1.3.1 <form> Actions

리액트 19에서는 Actions를 지원하기 위해 react-dom에도 변화가 생겼습니다.

아래와 같이 <form>, <input>, 그리고 <button> element에,

action 또는 formAction이라는 이름의 prop으로 action 함수를 넘길 수 있게 되었습니다.

<form action={actionFunction}>
<input formAction={actionFunction}>
<button formAction={actionFunction}>

만약 <form> Action이 성공할 경우, 리액트는 Uncontrolled components에 한해 입력 필드를 초기화 시킵니다.

<form>을 수동으로 초기화 시키고 싶을 경우, React DOM에서 제공하는 requestFormReset() 함수를 호출하면 됩니다.

🔗 <form>, <input> 태그에 대한 자세한 내용은 아래 공식문서 링크를 참고하시기 바랍니다.
https://react.dev/reference/react-dom/components/form
https://react.dev/reference/react-dom/components/input

1.3.2 useFormStatus()

컴포넌트 디자인 관점에서 컴포넌트는 자신이 속한 <form>의 상태에 접근하고 싶은 경우가 꽤 있습니다.

기존에는 props를 통해 넘겨주거나 Context를 활용할 수 있었지만,

이 과정을 조금 더 편리하게 만들기 위해서 useFormStatus() 훅이 추가되었습니다.

useFormStatus() 훅은 마지막 form submission에 대한 상태 정보를 제공하는 훅입니다.

부모 <form>의 상태를 읽어서 마치 Context provider처럼 하위 컴포넌트에 제공해주는 것이죠.

const { pending, data, method, action } = useFormStatus();

아래 코드는 useFormStatus() 훅을 사용한 예시 코드입니다.

import { useFormStatus } from 'react-dom';

function DesignButton() {
    const { pending } = useFormStatus();
    return <button type="submit" disabled={pending} />
}

🔗 useFormStatus() 훅에 대한 자세한 내용은 아래 공식문서 링크를 참고하시기 바랍니다.
https://react.dev/reference/react-dom/hooks/useFormStatus

1.4 기타 새로운 API

지금부터는 리액트 19에 새롭게 추가된 기타 API들에 대해 살펴보도록 하겠습니다.

1.4.1 React의 새로운 API

use()

use()는 렌더링 과정에서 값을 읽을 수 있게 해주는 새로운 API입니다.

예를 들면, 아래와 같이 use()와 Promise를 함께 사용해서 Promise가 resolve될 때까지 Suspensefallback을 표시할 수 있습니다.

import { use } from 'react';

function Comments({ commentsPromise }) {
    // `use`는 promise resolve 될 때까지 중단시킴
    const comments = use(commentsPromise);
    return comments.map((comment) => {
        return <p key={comment.id}>{comment}</p>
    });
}

function Page({ commentsPromise }) {
    // `use`가 Comments를 중단시키면, 아래 Suspense boundary가 표시됨
    return (
        <Suspense fallback={<div>Loading...</div>}>
            <Comments commentsPromise={commentsPromise} />
        </Suspense>
    );
}

NOTE. use를 사용할 때 주의할 점
use 훅은 렌더링 과정에서 생성된 Promises는 지원하지 않습니다.

그리고 use()를 사용하면 Context의 값을 조건부로 읽어올 수도 있습니다.

아래 코드는 use() 사용해서 Context의 값을 읽어오는 예시 코드입니다.

import { use } from 'react';
import ThemeContext from './ThemeContext'

function Heading({ children }) {
    // 'children'이 null일 경우 early return
    if (children == null) {
        return null;
    }

    // 조건부로 호출될 수 있음
    const theme = use(ThemeContext);
    return (
        <h1 style={{ color: theme.color }}>
            {children}
        </h1>
    );
}

use()는 이름 때문에 일반적인 훅과 동일하다고 생각할 수도 있습니다.

렌더링 과정에서 호출된다는 점은 동일하지만, 훅와 달리 use()는 조건부로 호출할 수 있습니다.

🔗 use()에 대한 자세한 내용은 아래 공식문서 링크를 참고하시기 바랍니다.
https://react.dev/reference/react/use

1.4.2 React DOM의 새로운 Static API

SSG(Static Site Generation)를 지원하기 위해 아래와 같은 두 개의 Static API가 새롭게 추가되었습니다.

  • prerender()
  • prerenderToNodeStream()

이 새로운 API들은 정적 HTML 생성을 위해 필요한 데이터를 로드할 때까지 기다리는 기능을 제공합니다.

이 API들은 Node.js 스트림 및 Web Streams와 같은 스트리밍 환경에서 작동하도록 설계되었습니다.

예를 들어, 웹 스트림 환경에서는 아래와 같이 prerender()를 사용하여 리액트 트리를 정적 HTML로 미리 렌더링할 수 있습니다.

import { prerender } from 'react-dom/static';

async function handler(request) {
    const { prelude } = await prerender(<App />, {
        bootstrapScripts: ['/main.js'],
    });
    return new Response(prelude, {
        headers: { 'content-type': 'text/html' },
    });
}

이렇게 하면 Prerender API는 모든 데이터가 로드될 때까지 기다린 후 정적 HTML 스트림을 반환합니다.

반환된 스트림은 문자열로 변환하거나 스트리밍 응답으로 전송할 수 있습니다.

참고로 Prerender API는 데이터 로드 중 스트리밍 콘텐츠를 지원하지 않으며, 이는 기존 React DOM 서버 렌더링 API에서 지원됩니다.

🔗 React DOM의 새로운 Static API에 대한 자세한 내용은 아래 공식문서 링크를 참고하시기 바랍니다.
https://react.dev/reference/react-dom/static


Part 2. React Server Components

이번 리액트 19에서 가장 큰 변화 중에 하나는 바로 React Server Components가 아닐까 싶습니다.

그동안 실험적인 기능으로 사용되어 왔던 Server Components가 정식으로 릴리즈 되었기 때문이죠.

지금부터는 React Server Components에 대해 간단하게 살펴보도록 하겠습니다.

2.1 Server Components

서버 컴포넌트는 이름 그대로 서버 환경에서 실행되는 컴포넌트를 의미합니다.

그렇다면 서버 컴포넌트에서 말하는 서버란 무엇일까요?

서버 컴포넌트는 클라이언트 애플리케이션 또는 SSR 서버와는 별도의 환경에서 실행되는데,

이 별도의 환경이 바로 서버 컴포넌트의 서버를 의미합니다.

서버 컴포넌트는 빌드 시 CI 서버에서 한 번 실행되거나, 웹 서버를 사용하여 요청마다 실행될 수 있습니다.

🔗 Server Components에 대한 자세한 내용은 아래 공식문서 링크를 참고하시기 바랍니다.
https://react.dev/reference/rsc/server-components

2.2 Server Actions (a.k.a Server Functions)

Server Actions는 클라이언트 컴포넌트가 서버에서 실행되는 비동기 함수를 호출할 수 있게 해주는 기능입니다.

그리고 "use server" 지시어를 사용해서 Server Actions를 정의할 수 있습니다.

Server Actions가 정의되면 프레임워크는 서버 함수에 대한 참조를 자동으로 생성하고 이를 클라이언트 컴포넌트에 전달합니다.

이후 클라이언트에서 해당 함수가 호출되면 리액트는 서버에 요청을 보내서 해당되는 서버 함수를 실행하고 결과를 반환하게 됩니다.

Server Actions는 서버 컴포넌트에서 생성하여 클라이언트 컴포넌트에 props로 전달하거나, 클라이언트 컴포넌트에서 import해서 사용할 수 있습니다.

NOTE. 서버 컴포넌트의 지시어
"use server" 지시어를 사용하면 서버 컴포넌트가 된다는 오해가 있지만, 서버 컴포넌트로 지정하기 위한 지시어는 없습니다.
"use server" 지시어는 Server Actions에 사용됩니다.

추가로 Server Actions의 이름 때문에 앞에서 설명한 Actions와 헷갈릴 수 있는데,

Server Functions가 action prop에 전달되거나 action 내부에서 호출되는 경우에만 Server Actions가 됩니다.

즉, 모든 Server Functions가 Server Actions는 아니라는 것이죠.

그래서 현재 공식 문서에서도 'Server Functions'라고 언급하고 있으며 향후 업데이트 될 예정이라고 합니다.

🔗 Server Actions에 대한 자세한 내용은 아래 공식문서 링크를 참고하시기 바랍니다.
https://react.dev/reference/rsc/server-functions


Part 3. React 19에서 개선된 점들

지금부터는 리액트 19에서 개선된 점들에 대해서 살펴보도록 하겠습니다.

3.1 ref as a prop

리액트 19에서는 함수 컴포넌트에 refprops로 제공됩니다.

즉, 함수 컴포넌트에서 ref를 전달하기 위해 더 이상 forwardRef()를 사용할 필요가 없다는 것이죠.

참고로 forwardRef()는 향후 deprecate되고 제거될 예정이라고 합니다.

아래 코드는 props를 통해 ref에 접근하는 예시 코드입니다.

function MyInput({ ref, placeholder }) {
    return (
        <input
            ref={ref}
            placeholder={placeholder}
        />
    );
}

// 'ref' 사용 예시
<MyInput ref={ref} />;

NOTE. 클래스 컴포넌트에서의 ref
클래스 컴포넌트는 React.Component의 인스턴스이기 때문에 refprops로 전달되지 않습니다.

3.2 Hydration errors에 대한 에러 표시 개선

react-dom에서 hydration error에 대한 에러 표시가 개선되었습니다.

기존에는 아래 그림과 같이 서버와 클라이언트의 불일치에 대한 정보 없이 DEV에서 여러 오류가 출력되었습니다.

Hydration errors before

하지만 리액트 19에서는 아래 그림과 같이 서버와 클라이언트의 불일치하는 부분을 단일 메시지와 함께 표시해줍니다.

Hydration errors after

이를 통해 더 빠르게 hydration error가 발생하는 부분을 확인하고 수정할 수 있겠네요.

3.3 <Context> as a provider

리액트 19에서는 <Context>를 Provider로 사용할 수 있습니다.

즉, 기존처럼 <Context.Provider>를 사용하지 않아도 된다는 것이죠.

참고로 <Context.Provider>는 향후 deprecate될 예정이라고 합니다.

아래 코드는 <Context>를 Provider로 사용하는 예시 코드입니다.

const ThemeContext = createContext('');

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

3.4 refs를 위한 cleanup functions

이제 ref callback에서 함수를 리턴함으로써 cleanup을 할 수 있도록 제공한다고 합니다.

아래 코드는 cleanup 함수를 사용한 예시 코드입니다.

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

        // NEW: cleanup 함수를 리턴함으로써,
        // element가 DOM에서 제거될 때 ref를 reset할 수 있음
        return () => {
            // ref cleanup
        };
    }}
/>;

이렇게 하면 컴포넌트가 unmount될 때 리액트가 cleanup 함수를 호출하게 됩니다.

그리고 이는 DOM refs, 클래스 컴포넌트에 대한 refs, useImperativeHandle() 에 적용됩니다.

NOTE. 참고사항
기존에는 컴포넌트를 unmount할 때 리액트가 nullref 함수를 호출했습니다.
이제 cleanup 함수가 제공되는 경우 이 단계를 건너뛰게 됩니다.
그리고 향후 버전에서는 컴포넌트를 언마운트할 때 nullref 함수를 호출하는 것이 deprecate될 예정입니다.

그리고 ref cleanup 함수가 도입되었기 때문에, ref 함수에서 다른 것을 반환하는 것은 이제 TypeScript에서 허용되지 않습니다.

그래서 아래와 같이 암묵적 반환(implicit returns)을 사용하지 않는 형태로 코드를 수정해야 합니다.

- <div ref={current => (instance = current)} />
+ <div ref={current => {instance = current}} />

기존 코드의 경우 HTMLDivElement의 인스턴스를 반환하는데,

이렇게 하면 TypeScript가 cleanup 함수를 반환하는지 판단할 수 없습니다.

그래서 암묵적 반환을 사용하지 않는 형태로 코드를 수정해야 하는 것입니다.

3.5 useDeferredValue() 훅에 초기값 설정 제공

이제 useDeferredValue() 훅에 초기값 설정을 제공합니다.

아래 코드는 initialValue를 사용한 예시 코드입니다.

function Search({ deferredValue }) {
    // 초기 렌더링 시 value는 ''가 되고,
    // 그런 다음 deferredValue로 다시 렌더링이 예약됨
    const value = useDeferredValue(deferredValue, '');

    return (
        <Results query={value} />
    );
}

initialValue가 제공되면, useDeferredValue() 훅은 초기 렌더링 시 값으로 초기값을 리턴하고,

그런 다음 백그라운드에서 deferredValue로 다시 렌더링을 예약합니다.

🔗 useDeferredValue() 훅에 대한 자세한 내용은 아래 공식문서 링크를 참고하시기 바랍니다.
https://react.dev/reference/react/useDeferredValue

3.6 Document Metadata 지원

HTML에서 <title>, <link>, <meta>와 같은 문서 메타데이터 태그는 문서의 <head> 섹션에 배치하기 위해 예약되어 있습니다.

리액트에서는 앱에 적합한 메타데이터를 결정하는 컴포넌트가 <head>를 렌더링하는 위치에서 매우 멀리 떨어져 있거나,

리액트가 <head>를 전혀 렌더링하지 않을 수 있습니다.

과거에는 이러한 elements를 effects에서 수동으로 삽입하거나 react-helmet과 같은 라이브러리를 통해 삽입해야 했고,

서버에서 리액트 애플리케이션을 렌더링할 때 세심한 처리가 필요했습니다.

이러한 점을 개선하기 위해 리액트 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>
    );
}

리액트가 이 컴포넌트를 렌더링할 때 <title>, <link><meta> 태그를 만나게 되면 문서의 <head> 섹션에 자동으로 올라라게 됩니다.

이러한 메타데이터 태그를 지원함으로써 클라이언트 전용 앱, 스트리밍 SSR 및 서버 컴포넌트에서 작동하도록 보장합니다.

NOTE. 여전히 Metadata 라이브러리가 필요할까?
간단한 사용 사례의 경우 문서 메타데이터를 태그로 렌더링하는 것이 적합할 수 있지만, 라이브러리는 현재 경로에 따라 일반 메타데이터를 특정 메타데이터로 재정의하는 등 더 강력한 기능을 제공할 수 있습니다.
이러한 기능을 사용하면 프레임워크와 react-helmet과 같은 라이브러리가 메타데이터 태그를 대체하는 것이 아니라 더 쉽게 지원할 수 있습니다.

🔗 <title>, <link><meta> 태그에 대한 자세한 내용은 아래 공식문서 링크를 참고하시기 바랍니다.
https://react.dev/reference/react-dom/components/title
https://react.dev/reference/react-dom/components/link
https://react.dev/reference/react-dom/components/meta

3.7 stylesheets를 위한 지원 개선

외부 링크(<link rel="stylesheet" href="...">)와 인라인(<style>...</style>) Stylesheets는 모두 스타일 우선순위 규칙(style precedence rules)으로 인해 DOM에 신중하게 배치해야 합니다.

컴포넌트 내에서 Stylesheets를 조합하는 기능을 구현하는 것은 어렵기 때문에,

종종 모든 스타일을 해당 스타일에 의존할 수 있는 컴포넌트에서 멀리 떨어진 곳에 로드하거나, 복잡성을 캡슐화하는 스타일 라이브러리를 사용합니다.

리액트 19에서는 이러한 복잡성을 해결하고 Stylesheets에 대한 기본 지원을 통해,

클라이언트에서 동시 렌더링(Concurrent Rendering)과 서버에서 스트리밍 렌더링(Streaming Rendering)을 더욱 심층적으로 통합할 수 있도록 했습니다.

아래와 같이 리액트에 Stylesheets의 우선순위(precedence)를 지정하면 DOM에서 Stylesheets의 삽입 순서를 관리하고,

해당 스타일 규칙에 의존하는 콘텐츠를 표시하기 전에 Stylesheets(외부 링크인 경우)가 로드되는지 확인합니다.

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>
            {/* ⬇︎ foo와 bar 사이에 삽입됨 */}
            <link rel="stylesheet" href="baz" precedence="default" />
        </div>
    )
}

서버 측 렌더링 중에 리액트는 <head>에 Stylesheets를 포함시켜 브라우저가 로드될 때까지 페인팅하지 않도록 합니다.

이미 스트리밍을 시작한 후 Stylesheets가 늦게 발견되는 경우,

리액트는 Stylesheets가 클라이언트의 <head>에 삽입되었는지 확인한 후 해당 Stylesheets에 의존하는 Suspense 경계의 콘텐츠를 표시합니다.

클라이언트 측 렌더링 중 리액트는 새로 렌더링된 Stylesheets가 로드될 때까지 기다렸다가 렌더링을 커밋합니다.

그리고 애플리케이션 내 여러 위치에서 이 컴포넌트를 렌더링하는 경우 리액트는 Stylesheets를 문서에 한 번만 포함합니다.

function App() {
    return <>
        <ComponentOne />
        ...
        <ComponentOne /> // DOM에서 중복 Stylesheets 링크로 연결되지 않음
    </>
}

Stylesheets를 수동으로 로드하는 데 익숙한 사용자에게는 Stylesheets에 의존하는 컴포넌트와 함께 해당 Stylesheets를 찾을 수 있어 로컬 추론이 향상되고 실제로 의존하는 Stylesheets만 로드할 수 있습니다.

또한 스타일 라이브러리 및 번들러와의 스타일 통합도 이 새로운 기능을 채택할 수 있으므로,

직접 Stylesheets를 렌더링하지 않더라도 이 기능을 사용하도록 도구가 업그레이드되므로 이점을 누릴 수 있습니다.

🔗 <link><style> 태그에 대한 자세한 내용은 아래 공식문서 링크를 참고하시기 바랍니다.
https://react.dev/reference/react-dom/components/link
https://react.dev/reference/react-dom/components/style

3.8 async scripts를 위한 지원 개선

HTML에서는 일반 스크립트(<script src="...">)와 deferred 스크립트(<script defer="" src="...">)가 문서 순서대로 로드되므로 컴포넌트 트리 깊숙한 곳에 이러한 종류의 스크립트를 렌더링하는 것이 어렵습니다.

그러나 비동기 스크립트(<script async="" src="...">)는 임의의 순서로 로드됩니다.

리액트 19에서는 스크립트에 실제로 의존하는 컴포넌트 내의 컴포넌트 트리 어디서든지 렌더링할 수 있도록 비동기 스크립트에 대한 지원이 개선되었습니다.

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

function App() {
    <html>
        <body>
            <MyComponent>
            ...
            <MyComponent> // DOM에 중복 script가 생기지 않음
        </body>
    </html>
}

모든 렌더링 환경에서 비동기 스크립트는 중복이 제거되어 여러 컴포넌트에 의해 렌더링되더라도 리액트가 스크립트를 한 번만 로드하고 실행합니다.

서버 측 렌더링에서 비동기 스크립트는 <head>에 포함되며 stylesheets, fonts, 및 image preloads와 같이 페인트를 차단하는 더 중요한 리소스 뒤에 우선순위가 지정됩니다.

🔗 <script> 태그에 대한 자세한 내용은 아래 공식문서 링크를 참고하시기 바랍니다.
https://react.dev/reference/react-dom/components/script

3.9 preloading resources를 위한 지원 개선

초기 Document 로드 시와 클라이언트 측 업데이트 시에 브라우저에 가능한 한 빨리 로드해야 할 리소스를 알려주면 페이지 성능에 큰 영향을 미칠 수 있습니다.

리액트 19에는 브라우저 리소스 로드 및 사전 로딩(preloading)을 위한 새로운 API가 다수 포함되어 있어,

비효율적인 리소스 로드에 방해받지 않는 훌륭한 경험을 최대한 쉽게 구축할 수 있습니다.

아래 코드는 리소스 로드와 관련된 API를 사용한 예시 코드입니다.

import { prefetchDNS, preconnect, preload, preinit } from 'react-dom';

function MyComponent() {
    preinit('https://.../path/to/some/script.js', { as: 'script' });  // loads and executes this script eagerly
    preload('https://.../path/to/font.woff', { as: 'font' });  // preloads this font
    preload('https://.../path/to/stylesheet.css', { as: 'style' });  // preloads this stylesheet
    prefetchDNS('https://...');  // when you may not actually request anything from this host
    preconnect('https://...');  // when you will request something but aren't sure what
}

그리고 위 코드를 통해 아래와 같은 DOM/HTML이 생성됩니다.

<html>
    <head>
        <!-- links/scripts are prioritized by their utility to early loading, not call order -->
        <link rel="prefetch-dns" href="https://...">
        <link rel="preconnect" href="https://...">
        <link rel="preload" as="font" href="https://.../path/to/font.woff">
        <link rel="preload" as="style" href="https://.../path/to/stylesheet.css">
        <script async="" src="https://.../path/to/some/script.js"></script>
    </head>
    <body>
        ...
    </body>
</html>

이러한 API를 사용하면 글꼴과 같은 추가 리소스의 검색을 Stylesheets 로딩에서 제외하여 초기 페이지 로드를 최적화할 수 있습니다.

또한 예상 탐색에 사용되는 리소스 목록을 미리 가져온 다음, 클릭 또는 마우스오버 시 해당 리소스를 미리 로드하여 클라이언트 업데이트 속도를 높일 수 있습니다.

🔗 Resource Preloading APIs에 대한 자세한 내용은 아래 공식문서 링크를 참고하시기 바랍니다.
https://react.dev/reference/react-dom#resource-preloading-apis

3.10 third-party scripts와 extensions를 위한 호환성 개선

타사 스크립트 및 브라우저 확장 프로그램을 고려하도록 hydration을 개선했습니다.

hydration 시 클라이언트에서 렌더링되는 요소가 서버의 HTML에서 찾은 요소와 일치하지 않으면,

React는 클라이언트에서 강제로 다시 렌더링하여 콘텐츠를 수정합니다.

이전에는 타사 스크립트나 브라우저 확장 프로그램에 의해 요소가 삽입된 경우 불일치 오류가 발생하고 클라이언트에서 렌더링되었습니다.

리액트 19에서는 <head><body>의 예기치 않은 태그를 건너뛰어 불일치 오류를 피할 수 있습니다.

관련 없는 hydration 불일치로 인해 리액트가 전체 문서를 다시 렌더링해야 하는 경우,

타사 스크립트 및 브라우저 확장 프로그램에 의해 삽입된 Stylesheets를 그대로 유지합니다.

3.11 더 나은 error reporting

리액트 19에서는 중복을 제거하고 catch된 에러와 catch되지 않은 에러를 처리하는 옵션을 제공하기 위해 에러 리포팅을 개선했습니다.

예를 들어, 이전에는 렌더링 중 Error Boundary에 의해 포착된 에러가 발생하면,

리액트가 에러를 두 번(원래 에러에 대해 한 번, 자동 복구에 실패한 후 한 번) 던진 다음 에러가 발생한 위치에 대한 정보와 함께 console.error()를 호출했습니다.

그 결과 에러가 발견될 때마다 아래와 같이 세 개의 에러 로그가 출력되었습니다.

Error reporting before

하지만 리액트 19에서는 아래와 같이 모든 에러 정보가 포함된 단일 에러 로그가 출력됩니다.

Error reporting after

또한 onRecoverableError()를 보완하기 위해 아래와 같이 두 가지의 새로운 루트 옵션을 추가했습니다.

  • onCaughtError(): 리액트가 Error Boundary 내에서 에러를 포착할 때 호출됨
  • onUncaughtError(): 에러가 발생했지만 Error Boundary 내에서 포착되지 않을 때 호출됨
  • onRoverableError(): 오류가 발생하고 자동으로 복구될 때 호출됩니다.

🔗 더 자세한 내용은 아래 공식문서 링크를 참고하시기 바랍니다.
https://react.dev/reference/react-dom/client/createRoot
https://react.dev/reference/react-dom/client/hydrateRoot

3.12 Custom Elements를 위한 지원 개선

리액트 19는 커스텀 엘리먼트를 완전하게 지원하도록 업데이트 되었으며, Custom Elements Everywhere의 모든 테스트를 통과했습니다.

이전 버전에서는 리액트가 unrecognized props를 properties가 아닌 attributes로 취급했기 때문에 Custom Elements를 사용하는 것이 어려웠습니다.

리액트 19에서는 아래와 같은 전략으로 클라이언트 및 SSR 중에 작동하는 properties에 대한 지원을 추가했습니다.

Server Side Rendering

사용자 정의 요소에 전달된 props의 타입이 string, number 같은 원시값이거나 값이 true인 경우 attributes로 렌더링됩니다.

object, symbol, function, 또는 값이 false인 경우와 같이 Props의 타입이 원시값이 아닐경우 생략됩니다.

Client Side Rendering

Custom Element 인스턴스의 property와 일치하는 props는 properties로 할당되고, 그렇지 않으면 attributes로 할당됩니다.


🔗 참고 링크


이번 매거진에서는 정식 릴리즈 된 리액트19에 대해 알아보았습니다.

다가올 2025년에는 새로운 기능들을 하나씩 공부해서 적용해보면 좋을 것 같습니다.

올 한 해 FrontOverflow 매거진에 관심을 가져주시고 구독해주셔서 진심으로 감사드립니다.

저는 내년에도 계속해서 유익한 글로 찾아뵙겠습니다!

지금까지 소플이었습니다. 감사합니다 😀

지금 가입하고 프론트엔드 개발 관련 매거진을 이메일로 받아보세요!

주식회사 핫티스트랩

대표이사 이인제

서울 강남구 테헤란로 128, 3층 58호(역삼동, 성곡빌딩)

사업자등록번호: 318-87-02079

통신판매번호: 2021-서울강남-00547

Copyright ⓒ Hottest Lab Inc. All rights reserved.