기술 이야기
home
Programming
home

React 19의 핵심 업데이트 사항과 관련 이슈 살펴보기

Upload date
2024/09/12
Tag
프론트엔드
프론트엔드개발자
코드
Editor
프론트게임플랫폼팀_최대욱
Editor is
프론트엔드 개발을 하고 있는 최대욱입니다.
3 more properties

들어가며

안녕하세요, 프론트게임플랫폼팀의 프론트엔드 개발자 최대욱입니다.
2012년 안드로이드 앱 개발을 시작으로 2015년부터 프론트엔드 개발에 주력해 왔습니다. 이전에는 커머스 및 금융 도메인에서 선물하기 서비스와 블록체인, 스톡옵션, 마케팅 플랫폼 등 다양한 프로젝트를 수행했습니다.
기술적으로는 ES2015 이전에 전통적인 자바스크립트 프로토타입 기반 개발 방식을 활용한 경험이 있으며, 이후 ES2015부터는 모던 자바스크립트와 함께 Backbone.js, Node.js, Angular, React, Next.js, Vue.js다양한 프론트엔드 기술을 다뤄왔습니다.
이번 글에서는 널리 사용되는 프론트엔드 라이브러리인 React의 최신 버전, React 19 RC(Release Candidate)에 대해 다룹니다. RC 버전은 정식 릴리스 전에 변경될 가능성이 있으므로, 이 글의 내용 역시 이후에 변경될 수 있음을 미리 알려드립니다. React 19 관련 공식 블로그와 React Reference를 기반으로 추가적인 설명을 더해 작성했습니다.
그림 1: State of js 2023의 시간 경과에 따른 프론트엔드 프레임워크 비율

1. Hook 변경 사항

먼저, React 19에서 기능이 개선된 Hook새롭게 추가된 Hook에 대해 설명드리겠습니다.
기능이 개선된 Hook
useTransition
useActionState
새롭게 추가된 Hook
useOptimistic
useFormStatus
use

1-1. useTransition

useTransition이 이제 비동기 함수와 함께 사용 할 수 있도록 업데이트되었습니다.
useTransition은 React 18에서 처음 도입된 Hook으로, UI를 차단하지 않으면서 상태 업데이트를 효율적으로 처리하는 데 도움이 됩니다. 그러나 React 18에서는 useTransition을 비동기 작업과 함께 사용하는 데 제약이 있어, pending 상태 처리, 오류 핸들링, 낙관적 업데이트, Sequential Requests 등을 수동으로 처리해야 했습니다.
아래는 React 19 이전 방식을 활용한 예제로, 주석을 통해 내용을 설명드리겠습니다.
// Before Actions function UpdateName({}) { const [name, setName] = useState(""); const [error, setError] = useState(null); const [isPending, setIsPending] = useState(false); const handleSubmit = async () => { setIsPending(true); // pending 상태 수동 설정 const error = await updateName(name); setIsPending(false); // pending 상태 수동 설정 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> ); }
JavaScript
복사
React 19부터는 useTransition이 비동기 함수를 지원하므로, 아래와 같이 코드를 작성하면 isPending 상태를 별도로 관리할 필요가 없습니다. 비동기 전환이 진행될 때 isPending은 자동으로 true로 설정되고, 전환이 완료되면 false로 변경됩니다. 이를 통해 데이터가 변경되는 동안에도 사용자는 끊김 없이 UI와 원활하게 상호작용할 수 있습니다.
// Using pending state from Actions function UpdateName({}) { const [name, setName] = useState(""); const [error, setError] = useState(null); const [isPending, startTransition] = useTransition(); const handleSubmit = () => { startTransition(async () => { // 비동기 함수 전달 가능 const error = await updateName(name); 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> ); }
JavaScript
복사

1-2. useActionState

useActionState는 form의 Action 결과를 기반으로 상태를 업데이트할 수 있도록 제공되는 Hook입니다. Canary 버전에서 사용되던 useFormState는 이제 useActionState로 대체되며, 폐기되었습니다.
React Server Component(RSC)를 지원하는 프레임워크와 함께 사용할 경우, Hydration이 완료되기 전에도 form을 상호작용 가능한 상태로 유지할 수 있습니다. RSC를 사용하지 않는다면, 일반적인 컴포넌트의 state와 동일하게 동작합니다.
아래는 Server Action과 useActionState가 함께 구성된 샘플 코드입니다. Hydration이 완료되기 전에도 앱과 상호작용이 가능합니다.
// App.js "use client"; import {updateName} from './actions'; function UpdateName() { // updateName은 action. {error: null} 은 초기 값. const [state, submitAction, isPending] = useActionState(updateName, {error: null}); return ( <form action={submitAction}> <input type="text" name="name" disabled={isPending}/> {state.error && <span>Failed: {state.error}</span>} </form> ); } // actions.js "use server"; export async function updateName(name) { if (!name) { // 이 상황에서 return 값은 useActionState의 새로운 state가 된다. return {error: 'Name is required'}; } await db.users.updateName(name); }
JavaScript
복사
React 19부터는 form의 action 속성에 Actions를 지정하여 작업을 수행할 수 있습니다. 이는 새로 추가된 Hook과 함께 사용하면 반복적인 상태 관리 로직을 제거하고, pending 상태를 더 쉽게 관리할 수 있게 해줍니다. 또한, RSC를 사용하지 않는 환경(SPA)에서도 useActionState를 통해 비동기 액션과 상태 관리를 간편하게 처리할 수 있습니다. 이 기능은 서버 렌더링뿐만 아니라 클라이언트 렌더링에서도 유용합니다.
form에 action props가 지정되면 자동으로 Actions로 간주되어 submit 후 자동으로 reset이 수행됩니다. 수동으로 reset을 처리해야 할 경우에는 requestFormReset을 사용하면 됩니다.

1-3. useFormStatus

이번에 새롭게 등장한 useFormStatus는 하위 컴포넌트에서 상위 form의 상태를 읽을 수 있도록 해주는 Hook입니다.
이 Hook을 사용하면 상위 form의 유효성 검사나 제출 상태를 쉽게 확인할 수 있어, 복잡한 form 구조에서도 매우 유용하게 사용할 수 있습니다.
import {useFormStatus} from 'react-dom'; function DesignButton() { const {pending} = useFormStatus(); return <button type="submit" disabled={pending} /> }
JavaScript
복사

1-4. useOptimistic

또한, UI를 낙관적으로 업데이트할 수 있는 Hook인 useOptimistic도 추가 되었습니다.
API 요청 후 실제 응답이 오기 전에, 마치 응답이 이미 완료된 것처럼 UI를 미리 업데이트할 수 있습니다. 이러한 낙관적 업데이트는 UI의 반응성을 높여 사용자에게 더욱 빠르고 원활한 경험을 제공합니다.
아래 예시에서는 사용자가 입력한 "안녕하세요" 메시지를 낙관적으로 먼저 UI에 업데이트한 후, 실제 요청이 완료되면 UI가 다시 렌더링되어 상태가 갱신됩니다. 이때 sending… 메시지가 제거됩니다.
그림 2: React reference의 useOptimistic 용법 중
// App.js import { useOptimistic, useState, useRef } from "react"; import { deliverMessage } from "./actions.js"; function Thread({ messages, sendMessage }) { const formRef = useRef(); const [optimisticMessages, addOptimisticMessage] = useOptimistic( messages, // state는 currentState를 의미한다. // newMessage는 optimisticValue를 의미한다. (state, newMessage) => [ ...state, // 여기서 사용자가 입력한 optimistic value인 newMessage가 설정되고, sending 상태가 된다. { text: newMessage, sending: true } ] ); async function formAction(formData) { // 사용자가 입력한 value를 가지고 addOptimisticMessage를 호출한다. // 넘기는 파라미터는 위 useOptimistic callback에서 newMessage에 해당되게 된다. addOptimisticMessage(formData.get("message")); formRef.current.reset(); await sendMessage(formData); } return ( <> {optimisticMessages.map((message, index) => ( <div key={index}> {message.text} {!!message.sending && <small> (Sending...)</small>} </div> ))} <form action={formAction} ref={formRef}> <input type="text" name="message" placeholder="Hello!" /> <button type="submit">Send</button> </form> </> ); } export default function App() { const [messages, setMessages] = useState([ { text: "Hello there!", sending: false, key: 1 } ]); async function sendMessage(formData) { const sentMessage = await deliverMessage(formData.get("message")); setMessages((messages) => [...messages, { text: sentMessage }]); } return <Thread messages={messages} sendMessage={sendMessage} />; } // actions.js export async function deliverMessage(message) { await new Promise((res) => setTimeout(res, 1000)); return message; }
JavaScript
복사

1-5. use

마지막으로 소개할 새로운 Hook은 Promise나 context와 같은 리소스의 값을 읽을 수 있도록 해주는 React API인 use입니다.
React 19 이전에는 Hook을 컴포넌트의 최상위에서만 호출해야 했고, 반복문, 조건문, 중첩된 함수 내에서는 사용할 수 없다는 제약이 있었습니다. 그러나 use는 이러한 제약 없이 사용할 수 있어 더 유연한 방식으로 리소스에 접근할 수 있습니다.
use를 사용하면 비동기 데이터를 가져올 수 있습니다.
import {use} from 'react'; function Comments({commentsPromise}) { // commentsPromise가 resolve 되기 전 까지 suspend 상태가 된다. const comments = use(commentsPromise); return comments.map(comment => <p key={comment.id}>{comment}</p>); } function Page({commentsPromise}) { // promise가 resolve 되기 전까지 Suspense의 fallback이 표시된다. return ( <Suspense fallback={<div>Loading...</div>}> <Comments commentsPromise={commentsPromise} /> </Suspense> ) }
JavaScript
복사
또한, use를 사용해 context를 읽을 수도 있습니다. 이를 통해 기존의 useContext를 대체할 수 있습니다. React는 컴포넌트 트리에서 가장 가까운 상위 컨텍스트 제공자를 찾아 값을 반환합니다. 또한, use는 최상위에서 호출될 필요가 없기 때문에 반복문이나 조건문 등과 함께 사용할 수 있어 더욱 유연한 사용이 가능합니다.
function HorizontalRule({ show }) { if (show) { const theme = use(ThemeContext); return <hr className={theme} />; } return false; }
JavaScript
복사

2. 편의성 개선 사항

또한, 이번에 출시된 React 19에서는 개발자의 편의성을 높이기 위한 다양한 개선 사항들이 포함되었습니다.

2-1. ref

React 19 이전에는 하위 컴포넌트에 ref를 전달하기 위해 반드시 forwardRef를 사용해야 했지만, 이제는 forwardRef를 사용하지 않고도 props로 ref를 전달할 수 있습니다. 이로써 코드가 간결해지고, ref 전달 방식이 더 직관적이게 개선되었습니다. 또한, React 19에서는 ref 콜백에서 cleanup 함수를 반환하는 기능이 새로 추가되었습니다. 기존에는 React가 컴포넌트가 unmount될 때 refcurrent 속성을 자동으로 null로 설정해주었지만, 이제 cleanup 함수를 사용할 경우 이러한 처리를 생략할 수 있습니다. 이를 통해 더 유연한 자원 해제가 가능해졌습니다.

2-2. Context

React 19에서는 <Context.Provider> 대신 <Context>를 provider로 직접 사용할 수 있습니다. 이를 통해 context의 사용 방식이 더 간결해지고, 코드를 작성할 때 불필요한 반복을 줄일 수 있습니다.

2-3. Hydration 에러

React 19에서는 react-dom의 Hydration 에러에 대한 불일치 리포팅이 크게 개선되었습니다. 이로 인해 서버와 클라이언트 간의 불일치로 인한 Hydration 에러를 더 명확하게 진단하고, 문제를 빠르게 파악할 수 있게 되었습니다. 이러한 개선은 서버 사이드 렌더링(SSR) 사용 시 개발자가 더 나은 디버깅 경험을 제공받을 수 있도록 돕습니다.

2-4. Metadata

React 19에서는 Metadata 지원이 추가되었습니다. 아래와 같이 코드를 작성하면, title, meta, link와 같은 메타데이터가 자동으로 <head>로 끌어 올려집니다.
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> ); }
JavaScript
복사

2-5. Stylesheet precedence 속성

React 19에서는 precedence 속성을 제공하여 스타일시트의 삽입 순서를 DOM 내에서 관리할 수 있게 되었습니다. 이를 통해 외부 스타일시트가 로드된 후 해당 스타일 규칙에 맞게 콘텐츠가 제대로 표시되도록 할 수 있습니다. 주요 개선 사항은 다음과 같습니다:
서버사이드 렌더링 시: 컴포넌트가 렌더링될 때 스타일시트를 삽입할 수 있습니다.
중복 방지: 여러 곳에서 동일한 컴포넌트를 렌더링할 경우, CSS는 한 번만 삽입됩니다.
모듈 번들러와의 통합: 모듈 번들러가 이 기능을 활용하면 성능과 관리 측면에서 이점을 얻을 수 있습니다.
아래 예시를 보면, bar 스타일시트는 high precedence를 가지며, 기본 precedence를 가진 foo보다 우선적으로 로드되고 DOM 상에서도 앞에 위치합니다. baz 스타일시트는 ComponentOne에서 사용된 foo와 동일한 precedence를 가지므로, DOM에서 foobar 사이에 위치하게 됩니다.
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> ) }
JavaScript
복사
React 19의 precedence 속성은 특히 CSS-in-JS 라이브러리와 함께 사용할 때 매우 유용합니다. 이 속성을 통해 스타일시트의 우선순위를 명시적으로 지정할 수 있어, 여러 스타일링 전략을 보다 효율적으로 적용할 수 있습니다.

2-6. Resource Preloading

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://...') // 연결하려는 DNS 도메인 이름의 IP 주소를 미리 가져올 수 있다. preconnect('https://...') // 아직 어떤 리소스가 필요한지 모르는 경우에도 리소스를 요청할 것으로 예상되는 서버에 연결할 수 있다. }
JavaScript
복사
<!-- the above would result in the following 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>
JavaScript
복사
preconnect를 예로 들면, 아래와 같이 페이지가 렌더링될 때 호출하거나, 이벤트 핸들러에서 호출하는 방식으로 사용할 수 있습니다. 이를 통해 브라우저가 특정 서버에 미리 연결하여 네트워크 레이턴시를 줄이고, 리소스 로딩을 더 빠르게 할 수 있습니다.
// 렌더링 시 호출 import { preconnect } from 'react-dom'; function AppRoot() { preconnect("https://example.com"); return ...; } // 이벤트 핸들러에서 호출 import { preconnect } from 'react-dom'; function CallToAction() { const onClick = () => { preconnect('http://example.com'); startWizard(); } return ( <button onClick={onClick}>Start Wizard</button> ); }
JavaScript
복사
이렇게 preconnect를 사용하면, 리소스를 요청하기 전에 서버와의 연결을 미리 설정하여 성능을 최적화할 수 있습니다.

3. 관련 이슈

하지만, React 19 발표 이후 몇 가지 이슈가 제기되었습니다.

3-1. 이슈의 시작

이슈는 PR(Pull Request)에서 시작되어 React github 에서 활발히 논의되고 있습니다.
아래 코드를 보면, React 19에 머지된 방식으로는 Suspense 내에서 형제 컴포넌트들을 병렬적으로 사전 렌더링할 수 없습니다. Foo 컴포넌트와 Bar 컴포넌트가 형제 컴포넌트에 해당하며, React 19의 새로운 렌더링 방식으로 인해 순차적으로 렌더링되면서 성능 저하가 발생할 수 있습니다.
<Suspense> <Foo /> <Bar /> </Suspense>
Plain Text
복사
아래는 Chrome에서 React 19와 React 18의 방식을 테스트한 결과입니다.
그림 3: React 18의 Suspense 렌더링 방식
그림 4: React 19의 Suspense 렌더링 방식

3-2. React 19 이전 동작

<Suspense> 내에 여러 개의 자식 컴포넌트가 있을 경우, 첫 번째 자식이 suspend 상태에 있을 때 fallback이 표시됩니다. 이때 형제 컴포넌트들도 suspend될 수 있으며, React는 병렬적으로 렌더링을 진행하여 다른 형제 컴포넌트의 프로미스를 수집하고, 데이터를 병렬적으로 가져옵니다.

3-3. React 19 동작

하지만 React 19에서는 <Suspense> 내에 여러 자식 컴포넌트가 있을 때, 형제 컴포넌트들이 병렬로 렌더링되지 않고, 이전 작업(데이터 페칭 및 렌더링)이 완료된 후에 순차적으로 렌더링됩니다. 이로 인해 두 형제 컴포넌트의 데이터를 waterfall 방식으로 가져오게 됩니다.
만약 이대로 React 19가 출시된다면, 이 문제를 해결하기 위해 미리 데이터를 페칭하거나, 형제 컴포넌트가 각각 별도의 Suspense를 가지도록 설계하는 방법을 고려해야 합니다.

3-4. 정리

컴포넌트 내에서 데이터를 불러오는 작업은 React에서 매우 일반적이며, 캡슐화를 가능하게 해주는 중요한 요소입니다. 그러나 React 19가 이대로 출시된다면, 이러한 장점이 상실될 우려가 있습니다.
특히, Three.js를 위한 React 렌더러 오픈소스인 react-three-fiber도 이번 업데이트가 그대로 적용될 경우, React를 fork할지에 대해 논의한 바 있습니다. 거대 오픈소스 프로젝트에서 fork를 논의할 정도라면, 이 문제는 상당히 심각하게 받아들여질 수 있습니다.
다행히 이슈가 제기된 후, React 팀에서 문제를 해결하기 위해 적극적으로 노력하고 있으며, 현재 이 문제는 해결 대기(홀드) 상태로 전환되었습니다. Suspense 내에서 추가적인 옵션 제공이나 스펙 변경이 예상됩니다.
그림 5: React 19 이슈에 대한 Sophie Alpert의 SNS 게시글 (출처: Sophie Alpert의 X(트위터) 캡처)

4. How to upgrade

React는 현재 최신 버전인 18.2와 동일한 상태이지만, React 19에서 제거될 API에 대한 경고와 React 19에 필요한 변경 사항을 포함한 React 18.3을 준비하고 있습니다. React 19로 업그레이드하기 전에 18.3으로 먼저 업그레이드하면, 잠재적인 문제를 미리 식별할 수 있습니다.
React 19 버전을 미리 사용해보고 싶다면 React 19 업그레이드 가이드 에서 확인하실 수 있습니다.

나가며

React 19에는 여러 가지 새로운 기능들이 추가 되었습니다. 이러한 세부 내용을 좀 더 자세히 확인하고 싶으시다면 React 공식 블로그Reference를 참조하시면 좋을 것 같습니다.
React는 지속적으로 발전하고 있으며, 특히 서버 사이드에서 React를 사용하는 사례가 점차 증가하고 있습니다. 이에 대해 React 커뮤니티 내에서 논란과 반발이 존재하는 것도 사실입니다. 최신 기술을 반드시 적용할 필요는 없지만, 이를 충분히 이해하고 비즈니스 상황이나 팀의 요구에 맞춰 선택한다면, 효율성과 성능 면에서 긍정적인 영향을 미칠 수 있을 것입니다.

Reference

함께 읽으면 좋은 콘텐츠
Related Sites
 넥슨 게임 포탈
회사 소개
인텔리전스랩스 소개
인재 영입
인텔리전스랩스 블로그 운영 정책
 테크블로그 문의 gs_site_contents@nexon.co.kr