리액트 컴포넌트 라이브러리 만들기 (Typescript + Rollup + React)
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
저는 “v19.0.0-beta-26f2496093-20240514” 버전을 사용했는데요, 아래 링크를 통해서 원하시는 버전을 선택하여 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 12 |
const handleSubmit = () => { const cache = name startTransition(async () => { setDisplayName(newName) const { isSuccess } = await updateName(newName) if (isSuccess === false) { setError(true) setDisplayName(cache) return } }) } |
위와 같이, 이름 그대로 낙관적으로 판단하고, 우선 업데이트 통신이 성공했다고 가정하고 데이터를 선 반영하고 통신에서 실패했을 때에는 캐시 해놓은 값으로 다시 값을 복구하는 업데이트 방법으로, 일반적으로도 많이 사용하는 방법인데요.
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 |
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 = () => { 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