세 번째, 광고 수익 내역과 수익금 기부하기
React 19 버전에 추가될 기능들을 알아보자
2024-10-18
Explanation
React 19 RC(Release Candidate)가 올해 4월에 공개되었으니, 이제 정식 릴리스도 머지 않았을 거 같아요?!. 그래서 오늘은 예습?의 시간으로 React 19 버전에 추가되거나 개선된 내용을 한번 알아보려 합니다.
이 포스트는 React의 공식 홈페이지의 내용을 간단하게 직접 구현해보며 다시 정리한 글이기 때문에 공식 홈페이지 내용과 크게 다르지 않습니다.
링크: https://react.dev/blog/2024/04/25/react-19
이 글에 사용되는 샘플 코드들은 아래의 리포지토리에서 모두 확인 하실 수 있습니다.
링크: https://github.com/falsy/blog-post-example/tree/main/react-19-rc
아래 링크를 통해서 원하시는 버전을 선택하여 npm 커맨드를 통해서 프로젝트에 설치하실 수 있습니다.
링크: https://www.npmjs.com/package/react?activeTab=versions
저는 아래와 같은 버전으로 설치하였습니다!
1 2 3 4 5 6 7 8 |
// package.json { ... "dependencies": { "react": "^19.0.0-beta-26f2496093-20240514", "react-dom": "^19.0.0-beta-26f2496093-20240514" } } |
“@types/react”, “@types/react-dom”는 아직 19 버전에 대한 패키지가 없어서, 자바스크립트를 사용하여 구성하였습니다.
여기서 액션은, 관례적으로 비동기 전환을 사용하는 함수를 이야기하는데요.
아주 간단한 상황을 예로 들어서, React v18의 구성으로 구현한다면 아래와 같이 만들어 볼 수 있는데요.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 |
import { useState } from "react" function updateName(name) { return new Promise((resolve) => { setTimeout(() => { resolve({ isSuccess: true, name: name, }) }, 1000) }) } export default function React18() { const [displayName, setDisplayName] = useState("") const [name, setName] = useState("") const [error, setError] = useState(false) const [isPending, setIsPending] = useState(false) const handleSubmit = async () => { setIsPending(true) const { isSuccess, name: newName } = await updateName(name) setIsPending(false) if (isSuccess === false) { setError(true) return } setDisplayName(newName) } return ( <div> <p>name: {displayName}</p> <input value={name} onChange={(event) => setName(event.target.value)} /> <button onClick={handleSubmit} disabled={isPending}> Update </button> {error && <p>error</p>} </div> ) } |
비동기 전환을 사용하는 함수를 호출하는 것을 액션이라고 한다고 했었는데요. 위 예시에서는 “handleSubmit”가 액션 함수가 되겠죠?!
위 코드는 보면 useState를 사용해서 Pending 상태와 Error 상태를 확인하고 있는데요, 이제 React v19의 코드로 다시 구성해 본다면,
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 |
import { useState, useTransition } from "react" function updateName(name) { return new Promise((resolve) => { setTimeout(() => { resolve({ isSuccess: true, name: name, }) }, 1000) }) } export default function React19() { const [displayName, setDisplayName] = useState("") const [name, setName] = useState("") const [error, setError] = useState(false) const [isPending, startTransition] = useTransition() const handleSubmit = () => { startTransition(async () => { const { isSuccess, name: newName } = await updateName(name) if (isSuccess === false) { setError(true) return } setDisplayName(newName) }) } return ( <div> <p>name: {displayName}</p> <input value={name} onChange={(event) => setName(event.target.value)} /> <button onClick={handleSubmit} disabled={isPending}> Update </button> {error && <p>error</p>} </div> ) } |
위와 같이 useTransition 훅을 사용하여 Pending 상태를 처리할 수 있답니다.
useTransition는 React v18에서 추가된 훅이지만, v18에서 startTransition는 동기 함수만 인자로 받을 수 있었는데 v19에서 비동기 함수도 사용할 수 있도록 추가되었습니다.
그리고 일반적인 경우의 작업을 더 쉽게 하기 위해서 useActionState 라는 함수가 추가되었는데요.
예를 들면 아래와 같습니다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 |
import { useState, useActionState } from "react" function updateName(name) { return new Promise((resolve) => { setTimeout(() => { resolve({ success: true, name: name, }) }, 1000) }) } export default function ActionState() { const [name, setName] = useState("") const [state, action, pending] = useActionState( async (previousState, newState) => { const res = await updateName(newState.nam) return res }, { name: "" } ) const handleClick = () => { action({ name }) } return ( <div> <p>name: {state.name}</p> <input value={name} onChange={(event) => setName(event.target.value)} /> <button onClick={handleClick} disabled={pending}> Update </button> </div> ) } |
useActionState를 사용해서 구성하니 코드가 많이 간단해졌죠?! handleClick도 없어도 되지만 newState 인자의 의미를 확실히 나타내려고 일부러 추가해 보았습니다.
이어서 React Dom의 form에 action이 추가되었는데요. 앞에 이야기했던 useActionState 훅을 사용해서 아래와 같이 구현할 수 있답니다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
import { useActionState } from "react" function updateName(name) { return new Promise((resolve) => { setTimeout(() => { resolve({ success: true, name: name, }) }, 1000) }) } export default function ActionStateForm() { const [state, formAction, pending] = useActionState( async (previousState, formData) => { const name = formData.get("name") const res = await updateName(name) return res }, { name: "" } ) return ( <form action={formAction}> <p>name: {state.name}</p> <input type="text" id="name" name="name" required /> <button type="submit" disabled={pending}> {pending ? "Submitting..." : "Submit"} </button> </form> ) } |
이어서 React Dom에 useFormStatus라는 훅이 추가되었는데요, 예를 들어서 form이 있고 만약 button 요소가 별도의 컴포넌트로 구성되어 있을 때 form의 pending 상태를 button 컴포넌트에서 알기 위해서는 props로 전달하거나 context를 사용해야 하는데 이를 useFormStatus를 사용하여 구성할 수 있습니다.
예를 들면,
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
import { useActionState } from "react" import FormStatusBtn from "./FormStatusBtn" function updateName(name) { return new Promise((resolve) => { setTimeout(() => { resolve({ success: true, name: name, }) }, 1000) }) } export default function ActionStateForm() { const [state, formAction, pending] = useActionState( async (previousState, formData) => { const name = formData.get("name") const res = await updateName(name) return res }, { name: "" } ) return ( <form action={formAction}> <p>name: {state.name}</p> <input type="text" id="name" name="name" required /> <FormStatusBtn /> </form> ) } |
위와 같이 form이 있고
1 2 3 4 5 6 7 8 9 10 11 |
import { useFormStatus } from "react-dom" export default function FormStatusBtn() { const { pending } = useFormStatus() return ( <button type="submit" disabled={pending}> {pending ? "Submitting..." : "Submit"} </button> ) } |
위와 같이 button 요소를 별도의 컴포넌트로 구성할 때, useFormStatus 훅을 사용해서 form의 pending 상태를 알 수 있습니다.
낙관적 업데이트는 데이터를 변경하는 통신을 할 때, 변경 사항을 UI에 미리 반영하는 방법을 이야기하는 데요, 간단하게 예를 들면,
1 2 3 4 5 6 7 8 9 10 11 |
const handleSubmit = () => { const cache = name startTransition(async () => { setDisplayName(newName) try { await updateName(newName) } catch { setDisplayName(cache) } }) } |
위와 같이, 이름 그대로 낙관적으로 판단하고, 우선 업데이트 통신이 성공했다고 가정하고 데이터를 선 반영하고 통신에서 실패했을 때에는 캐시 해놓은 값으로 다시 값을 복구하는 업데이트 방법으로, 일반적으로도 많이 사용하는 방법인데요.
useOptimistic 라는 훅이 추가되어서 위와 같은 낙관적 업데이트 처리를 조금 더 간단하게 구현할 수 있게 되었습니다. 간단하게 예를 들면,
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 |
import { useOptimistic, useState, useTransition } from "react" let toggle = true function updateName(name) { return new Promise((resolve, reject) => { setTimeout(() => { if (toggle) { resolve({ success: true, name: name, }) } else { reject("Error") } toggle = !toggle }, 1000) }) } export default function Optimistic() { const [originData, setOriginData] = useState("") const [optimisticName, setOptimisticName] = useOptimistic(originData) const [name, setName] = useState(originData) const [error, setError] = useState(false) const [isPending, startTransition] = useTransition() const handleSubmit = () => { setError(false) startTransition(async () => { setOptimisticName(name) try { const { name: resName } = await updateName(name) setOriginData(resName) } catch (e) { setError(true) } }) } return ( <div> <p>name: {optimisticName}</p> <input value={name} onChange={(event) => setName(event.target.value)} /> <button onClick={handleSubmit} disabled={isPending}> Update </button> {error && <p>error</p>} </div> ) } |
toggle 이라는 변수를 두고 성공과 실패를 반복하도록 구성했는데요, 위와 같이 handleSubmit 이벤트가 실행되면 우선 setOptimisticName에 의해 값이 면저 변경되고 성공하면 유지되지만, 만약 통신이 실패한다면 이전의 값으로 돌아갑니다.
use 라는 비동기 작업을 처리를 위한, Promise를 지원하는 새로운 훅이 추가되었습니다. 기존의 useEffect와 달리 use는 컴포넌트 렌더링 시점에서 데이터를 가져오는 Promise를 기다리고 데이터가 준비될 때까지 컴포넌트가 중단됩니다. 그리고 기존의 React의 훅들은 컴포넌트의 최상위에만 호출할 수 있었는데 use는 조건부로도 호출이 가능하다는 특징이 있습니다.
간단하게 구현해보면 아래와 같습니다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
import { use, Suspense } from "react" function getName() { return new Promise((resolve) => { setTimeout(() => { resolve({ success: true, name: "falsy", }) }, 1000) }) } function UserName({ userNamePromise }) { const user = use(userNamePromise) return <p>{user.name}</p> } export default function UseHook() { const userNamePromise = getName() return ( <Suspense fallback={<div>Loading...</div>}> <UserName userNamePromise={userNamePromise} /> </Suspense> ) } |
그리고 use의 특징 중에 하나인 조건부로 호출하는 예시는 아래와 같습니다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
import { use, Suspense } from "react" function getName() { return new Promise((resolve) => { setTimeout(() => { resolve({ success: true, name: "falsy", }) }, 1000) }) } function UserName({ userNamePromise, useFallback }) { if (useFallback) { return <p>Guest</p> } const user = use(userNamePromise) return <p>{user.name}</p> } export default function UseHook() { const userNamePromise = getName() const shouldUseFallback = true return ( <Suspense fallback={<div>Loading...</div>}> <UserName userNamePromise={userNamePromise} useFallback={shouldUseFallback} /> </Suspense> ) } |
그 밖의 개선 사항도 적었었는데, 적다보니..
거의 React 공식 홈페이지의 내용을 그냥 복사 붙여넣기 하는 수준이여서 링크로 대체합니다!
링크: https://ko.react.dev/blog/2024/04/25/react-19#improvements-in-react-19
2024.12.14 내용추가
뭔가.. 마지막을 링크로 대체한 게 마음에 걸려서 간단하게 그밖의 개선사항도 추가합니다.
(공식 홈페이지와 크게 다르지 않은 내용입니다.)
이제 함수형 컴포넌트에서 ref를 prop으로 전달 받아 사용할 수 있습니다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
import { useRef } from "react" function ChildRef({ ref }) { return <input ref={ref} /> } export default function ParentRef() { const TestRef = useRef(null) return ( <div> <ChildRef ref={TestRef} /> <button onClick={() => TestRef.current.focus()}>Focus</button> </div> ) } |
이전까지는 아래와 같이 props 이름을 ‘ref’가 아닌 다른 이름을 사용하거나
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
import { useRef } from "react" function ChildRef({ TestRef }) { return <input ref={TestRef} /> } export default function ParentRef() { const TestRef = useRef(null) return ( <div> <ChildRef TestRef={TestRef} /> <button onClick={() => TestRef.current.focus()}>Focus</button> </div> ) } |
또는 forwardRef를 사용하여 구현해야 했습니다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
import { forwardRef, useRef } from "react" const ChildRef = forwardRef(function ChildRef(_, ref) { return <input ref={ref} /> }) export default function ParentRef() { const TestRef = useRef(null) return ( <div> <ChildRef ref={TestRef} /> <button onClick={() => TestRef.current.focus()}>Focus</button> </div> ) } |
이후 버전에서 ‘forwardRef’는 점차 사용이 중단될 예정입니다.
react-dom의 Hydration 에러 보고 방식을 개선되었습니다.
생성된 Context를 그대로 Provider로 사용할 수 있습니다.
1 2 3 4 5 6 7 8 9 10 11 |
import { createContext } from "react" const NewContext = createContext() export default function ProviderComp({ children }) { return ( <NewContext value={{ name: "Provider" }}> {children} </NewContext> ) } |
이전까지는 아래와 같이 ‘Context.Provider’를 사용해야 했습니다.
1 2 3 4 5 6 7 8 9 10 11 |
import { createContext } from "react" const NewContext = createContext() export default function ProviderComp({ children }) { return ( <NewContext.Provider value={{ name: "Provider" }}> {children} </NewContext.Provider> ) } |
이후 버전에서 ‘Context.Provider’는 점차 사용이 중단될 예정입니다.
이제 함수형 컴포넌트에서 ref를 prop으로 전달 받아 사용할 수 있습니다.
1 2 3 4 5 6 7 8 |
<input ref={(ref) => { // ref 생성 return () => { // ref cleanup 작업 } }} /> |
1 2 3 4 5 |
function Search({ deferredValue }) { const value = useDeferredValue(deferredValue, '') // 초기값 설정 return <Results query={value} /> } |
<title>, <link>, <meta>와 같은 문서 메타데이터 태그를 네이티브로 지원합니다.
1 2 3 4 5 6 7 8 9 10 11 |
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} /> </article> ) } |
메타데이터 태그는 자동으로 head 색션으로 이동하며 SSR,CSR 모두에서 작동합니다.
스타일시트의 우선순위와 삽입 순서를 관리할 수 있는 기능을 제공합니다.
1 2 3 4 5 6 7 8 |
function ComponentOne() { return ( <Suspense fallback="loading..."> <link rel="stylesheet" href="foo" precedence="default" /> <link rel="stylesheet" href="bar" precedence="high" /> </Suspense> ) } |
SSR에서는 head에 포함되고 CSR에서는 스타일 시트가 로드된 후 콘텐츠가 표시되며 중복되는 스타일은 한 번만 삽입됩니다.
1 2 3 |
function MyComponent() { return <script async src="..."></script> } |
SSR에서는 head에 포함되고 CSR에서는 중복 없이 한번만 로드됩니다.
페이지 성능 최적화를 위해 리소스 프리로딩 API를 제공합니다
1 2 3 4 |
import { preload, preconnect } from 'react-dom' preload('https://.../font.woff', { as: 'font' }) preconnect('https://...') |
커스텀 요소를 완벽히 지원하고 props와 속성을 적절히 처리합니다. SSR, CSR 모두에서 호환됩니다.