React 19가 온다

date
May 4, 2024
slug
react19
summary
React 19에서 업데이트되는 부분들을 알아보자
thumbnail
status
publish

React 19

드디어 React 19가 등장했다. Release Candidate인 만큼 조금의 수정을 걸쳐서 곧 바로 공식 버전으로 업데이트 되지 않을까 싶다.
공식문서에서 기술하는 React 19새로운 기능 및 개선점을 알아보자.

신규 기능

💡
React에서 비동기 Transition을 사용하는 함수들을 actions라 부른다. actions은 데이터 트랜지션 과정을 관리한다

Form Action

<form>, <input>, <button> 태그들에 action 또는 formAction props를 전달할 수 있게 된다.
form이 제출될 때 action이 성공적으로 수행되면 form이 자동으로 reset 된다. action은 비동기로 작동되며 첫번째 인자로 form data를 전달받는다.
💡
action 또는 formAction는 HTTP method 속성 값과 상관없이 POST가 된다!
function Search() { function search(formData) { // form data를 받는다. const searchInput = formData.get("searchInput"); } return ( <form action={search}> <input name="searchInput" /> <button type="submit">Search</button> </form> ); }

Action에 추가적인 정보 전달

action 및 formAction 내부적으로 form data만 접근이 가능하다. 하지만 bind() 메소드를 통해 추가적인 정보 제공하는게 가능해진다.
function Search() { async function search(formData, categoryId) { const results = await searchAPI(categoryId, formData); } const searchWithCategory = search.bind(null, categoryId); return ( <form action={searchWithCategory}> <input name="searchInput" /> <button type="submit">Search</button> </form> ); }

useFormStatus

form의 상태(action의 비동기 상태)를 감지하기 위해서는 <form/> 태그가 존재하는 컴포넌트에서 const [pending, setPending] = useState(false) 같이 상태 값을 따로 관리하고 이를 props로 자식 컴포넌트에 내려줘야 했다.
이는 복잡한 컴포넌트 구조에서 props drilling를 야기시킨다.
React 19 부터는 <form/> 컴포넌트의 자식 컴포넌트에서 useFormStatus를 사용하게 되면 form의 상태를 알 수 있다 🥹
function Form() { const action = async (formData) { ... } return ( <form action={action}> ... <SubmitButton /> </form> ); }
import {useFormStatus} from 'react-dom'; function SubmitButton() { const {pending} = useFormStatus(); return <button type="submit" disabled={pending} /> }

useActionState

💡
Canary Releases에서의 ReactDOM.useFormState 이름이 deprecated되고 React.useActionState으로 수정되었다. 또한 React DOM 내부 API 였지만 React API로 이전된 듯 하다.
const [state, action] = useActionState(fn, initialState, permalink?)
form의 제출 상태 및 action 자체를 커스텀 하게 할 수 있게 한다.
useState 및 useReducer를 사용하지 않고 대기, 오류, 성공 상태를 하나의 훅(hook)으로 관리 할 수 있다. 이제는 form의 validation만 수월하게 할 수 있다면 react-hook-form 같은 라이브러리의 도움이 최소화 될 것 같다.
반환 값인 state action이 성공적인지를 나타내는 불리언 값, 오류 메시지, 또는 업데이트된 정보를 포함하는 객체일 수 있다. 이를 활용해 form의 성공 및 에러 상태를 커스텀하게 보여줄 수 있다.
import { useActionState } from 'react'; function action(currentState, formData) { // ... return 'next state'; } function MyComponent() { const [state, formAction] = useActionState(action, null); return ( <form action={formAction}> {/* ... */} </form> ); }
  • form이 제출되기
    • state: useActionState의 초기 값
    • action의 첫번째 인자: useActionState의 초기 값
    • action의 두번재 인자: 제출된 formData
  • form이 제출된
    • state: action의 반환값 (action의 반환값은 useActionState의 새로운 현재 state가 된다.)
    • action의 첫번째 인자: 이전에 제출된 formData (제출될 때 마다 업데이트)
    • action의 두번재 인자: 다시 제출된 formData
직관적인 사용 방법이지만, useActionState 반환 값과 action의 인자의 변화는 기억해두자.

useOptimistic

사용자가 form을 제출하고 요청이 처리되는 동안 사용자에게 즉각적인 피드백을 보여 줄 수 있게 된다. 사용자는 Loading Spinner 같은 pending 상태에 대한 UI를 볼 필요가 없다. 요청이 성공했을 때의 데이터를 이용하여 곧 바로 필요한 UI를 볼 수 있게 된다.
무조건 성공을 예상하고 데이터를 사용하기 때문에 optimistic, 낙관적이라는 단어를 썼지 않을까?
개인적으로 이 훅에 대해서는 조금 회의적인 생각이 든다.
만약 응답이 실패하게 되면 사용자는 성공이 예측된 데이터를 보다가 다시 이전의 데이터를 보게 될 것이다. 이 과정이 오히려 UX를 해치고 응답 실패를 더욱 더 오류 처럼 보이게 하는 것이 아닌가 싶다. 또한 성공한 UI가 사라지기 때문에 CLS 지표에도 영향을 미칠 것 같다.
하지만 예시에서 보여주듯 해당 훅은 채팅같은 실시간 데이터를 사용하는 UI에서는 낙관적 데이터를 이용하여 UI를 업데이트 해주는게 UX에서 좋을 듯 하다. 채팅 하나 보내는데 Loading Spinner 뜨는것도 이상하니깐
결과적으로 사용할 곳이 많아 보이지는 않지만, 적절한 곳에 사용하면 UI/UX적으로 도움이 될 것은 확실해 보인다.

use

const value = use(resource);
promise 또는 context를 인자(resource)로 받아 해당 값을 읽을 수 있는 함수이다.

Promise

import { use, Suspense } from "react"; import { ErrorBoundary } from "react-error-boundary"; function App({ messagePromise }) { return ( <ErrorBoundary fallback={...}> <Suspense fallback={...}> <Message messagePromise={messagePromise} /> </Suspense> </ErrorBoundary> ); } function Message({ messagePromise }) { const content = use(messagePromise); return <p>{content}</p>; }
promise가 인자로 넘겨질 경우 promise가 resolve 혹은 reject 될 때 까지 React는 중단(Suspense)된다.
이를 이용해서 useEffect를 사용하지 않고 Data Fetch 등의 Promise 처리가 가능해진다. 또한 Suspense, ErrorBoundary를 이용하여 상태에 따른 조건부 UI 처리도 간단해진다.

Context

function Button({ show, children }) { if (show) { const theme = use(ThemeContext); const className = 'button-' + theme; return ( <button className={className}> {children} </button> ); } return false }
Context가 인자로 넘겨질 경우 useContext과 동일하게 동작한다. 하지만 use의 경우 If 문 for 문 안에서 조건부로 호출이 가능하다!

개선점

ref as a props

React Conf 2024
React Conf 2024
이제 refprops로 넘겨줄 때 forwardRef 로 컴포넌트를 감싸지 않아도 된다. 추후 forwardRef는 삭제될 예정이라고 한다.

ref, clean up 함수

<input ref={(ref) => { // ref가 생성됨 // NEW: ref가 DOM에서 제거될 때 실행되는 cleanup 함수 반환 return () => { // ref clean up (정리 작업) }; }} />
ref 콜백에 clean up 함수가 추가된다. 컴포넌트가 언마운트되거나 ref가 해제될 때 호출되며 ref와 관련된 리소스를 정리하거나 필요한 해제 작업을 수행이 가능하다.

Context.Provider 삭제

const ThemeContext = createContext(''); function App({children}) { return ( <ThemeContext value="dark"> {children} </ThemeContext> ); }
Context 사용시 <Context.Provider> 대신 <Context>로 렌더링이 가능해진다.
추후 <Context.Provider> 삭제될 예정이기 때문에 코드상에서 Context를 사용한 부분 및 라이브러리 업데이트 등의 대응이 필요할 듯하다.

useDeferredValue 초기값

useDeferredValue(value, initialValue?)
useDeferredValue의 initialValue(초기값)이 추가된다. initialValue로 초기 렌더링 후 value 인자를 return 하는 렌더링이 진행된다.

컴포넌트 내부, metadata tag 지원

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> ... </p> </article> ); }
기존에는 metadata tag을 사용하기 위해서 document의 <head>에 지정하거나 react-helmet 같은 라이브러리의 도움을 받았어야 했다.
이제는 컴포넌트에서 <title> <link> <meta> 태그 사용이 가능해진다. 해당 태그들은 <head> 로 자동으로 호이스팅된다.

stylesheets 지원

stylesheets(스타일시트)의 순서를 precedence 속성을 사용하여 지정할 수 있게 된다. 또한 중복되는 스타일시트는 제거한다. 이는 스타일시트가 올바른 순서대로 로드되고 중복되지 않도록 보장한다.
컴포넌트에서 <link> 태그 사용이 가능 해짐에 따라 스타일시트도 컴포넌트 별로 불러올 수 있게 된다.
CSR, SSR 모두에서 렌더링 전 스타일시트를 불러오게 된다. 이를 통해 스타일이 적용되지 않은 콘텐츠가 뒤늦게 스타일이 적용되면서 깜빡이는 현상을 방지할 수 있다.

비동기 scripts 지원

function MyComponent() { return ( <div> <script async={true} src="..." /> Hello World </div> ) }
<script> 태그를 통해 필요한 스크립트를 컴포넌트 단에서 불러올 수 있다. 만약 동일한 자원을 여러 컴포넌트에서 불러온다면 자체적으로 스크립트 중복을 제거해준다.
notion image
자체적으로 async 속성이 생기면서 Server Component 와의 호환을 위해 defer 속성을 추천하지 않게 되었다.

preloading resouces 지원

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://...') // 브라우저가 자원을 연결을 시켜둠 }
<html> <head> <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가 추가되었다. 컴포넌트에서 <link> 태그를 사용하지 않고 제공하는 API를 이용해 직관적으로 리소스를 불러올 수 있는 것 같다.

정리

form 및 기타 편의성이 대폭 강화되었다. 기존의 불편했던 React의 사용성을 개선한 서드 파티 라이브러리의 기능을 React 내부로 가져온 느낌이 든다.
기술된 내용들은 코드상에서 사용가능한 부분(새로운 훅) 및 개선점이지만 업데이트를 통해 react-compiler와 같이 React의 내부 동작 방식이 바뀌는 부분도 있기 때문에 필히 이를 인지하고 업데이트 및 사용해야겠다.
Next.js도 이에 맞춰서 14 버전 이후로 15 버전을 준비하고 있는 것 같은데, 요즘 프론트엔드 프레임워크 생태계가 아주 활발하게 돌아가고 있는 것 같아서 흥미롭다. 그리고 새롭게 알아야 할 지식이 점점 많아진다. 🤡