React19가 stable된지도 좀 지났으니 무슨 변경사항이 있나 파악해보기로 했다. 최근 Next.js 15 + react 19를 함께 스터디중이기도해서 중간정리할겸 오랜만에 글을쓴다.🤤
19버전의 핵심은 무엇일까?
사실 18버전의 key-note 에서도 언급되었지만 React팀은 자동으로 메모이제이션을 해주는 컴파일러를 개발중이라고 언급했었다. 메모이제이션을 하면 성능은 좋아지지만 사실 DX가 매우 나쁘기때문에 (모든곳에 useMemo, useCallback, React.memo ..) 나뿐만아니라 다른사람들도 많은 관심을 가지고 지켜봤고, 19버전에서 해당 컴파일러가 베타로 출시되었기때문에 많이들 주목하는거 같다.
음 하지만 이것보다는 RSC의 stable
이 메인이 된다고 생각했다!
React Server Component (RSC)
서버컴포넌트는 기존 CSR, SSR 서버와 다른곳에서 렌더링 되는 새로운 컴포넌트다.
CSR의 렌더링은 모두 클라이언트에서 처리되고, SSR은 서버에서 html을 먼저 렌더링해 보내고 클라이언트 에서 hydrate를 통해 남은 JS를 넣지만 서버컴포넌트는 컴포넌트 전부가 서버에서 렌더링되고 오직 html만 클라이언트로
보내진다.
그렇기때문에 번들링 사이즈가 작아지고, 별 도의 api를 만들 필요없이 ORM등을 통해 db에 바로 접근이 가능하는 등 장점이 있는 반면
event-handler
나 useState와 같은 클라이언트 환경에서 실행되어야하는 함수들은 실행되지 못한다.
그래서 Server-Action을 통해 클라이언트가 액션에대한 참조를 받아 request형태로 그 액션을 호출시 서버에서 실행되는 형태로 인터랙티브함을 넣을 수 있다.
컴포넌트가 async기 때문에 Suspense, Error-Boundary와도 통합이된다. 이제 어느 상황에서 서버-클라이언트컴포넌트를 사용할지 구분하는게 중요해 진 것 같다.
const Component = async() => {
const data = await db.get();
return <div>{data}</div>
}
use (New)
Promise 나 context의 데이터를 기반으로 컴포넌트 렌더링을 할수 있게 해준다. 기타 hooks와 마찬가지로 Component나 customHooks에 사용 가능하지만
다른 hooks와 다르게 conditional 및 loop에서도 사용이 가능
하다.
import { Suspense } from "react";
import { Fruits } from "../src/components/Fruits";
export default function App() {
return (
<Suspense fallback={<div>loading...</div>}>
<Fruits />
</Suspense>
);
}
// Fruits.jsx
import { use } from "react";
const getFruits = async () => {
await new Promise((r) => setTimeout(r, 1000));
return new Promise((resolve) => {
resolve(["apple", "banana", "mango"]);
});
};
export const Fruits = () => {
const fruits = use(getFruits());
return (
<ul>
{fruits.map((fruit, idx) => (
<li key={idx}>{fruit}</li>
))}
</ul>
);
};
use
가 promise와 사용된다면 Suspense와 Error-Boundary와 통합이 가능해진다. 만약 에 러핸들링시 Error-Boundary를 사용하지 않는다면 use
는 try/catch에서는 사용할 수 없기때문에 promise의 catch문에서 에러핸들링할 값을 넣어줘야 한다.
RSC에서도 사용이 가능하지만 RSC에선 async/await을 통한 promise 렌더링을 추천한다. use
는 promise가 resolved되면 리렌더링을 트리거하지만 RSC의 async/await은 그 작업 자체에서 렌더링 작업을 추출해내기 때문이다.
const value = useContext(context);
const _value = use(context);
둘의 차이는 무엇일까? 사실 거의 비슷하다. 하지만 use
는 조건부 및 loop에서 사용이 가능하기 때문에 좀더 유연하다.
하지만 그런 유연함이 필요 없다면 hooks자체에서 값에대한 유추가 가능하기때문에 useContext
사용이 좀더 나아보인다.
useActionState (New)
React에서 말하는 Action은 컨벤션적으로 UI transition을 트리거하는 async 함수를 말한다.
이런 Action에 연관된 상태관리 처리를 할 수 있게 도와주는 hooks다. 추가로 useTransition
hooks역시 Action을 서포팅하게 확장되었다.
actionState
는 매개변수로 받은 action callback에 결과가 들어오고 action
은 callback에 대한 참조이며 prevState,formData
를 받는다.
"use client";
import { useActionState } from "react";
const getData = async () => {
await new Promise((r) => setTimeout(r, 1000));
return "response-value";
};
export default function Page() {
const [actionState, action, isPending] = useActionState(async (prevState,formData) => {
try {
const response = await getData();
return response;
} catch (err) {
return null;
}
}, "initial-value");
return (
<>
<form action={action}>
<button type="submit">submit</button>
</form>
{isPending && <p>isPending</p>}
<span>{actionState}</span>
</>
);
}
useOptimistic (New)
Optimistic-update를 도와주는 hooks다 optimistic은 사전상의미로 '긍정적인' 이다. 즉 상태의 업데이트가 비동기에 의존할때 그 업데이트가 성공적일것이라고 (긍정적)으로 해석하고 그에 맞는 예상되는 상태를 즉각 업데이트하고 에러를 뱉는다면 다시 revert하는 형태의 업데이트를 말한다.
말만 들어도 핸들링할게 많아보이는데 이를 도와주는거다..^^
"use client";
import React from "react";
const send = async (msg) => {
await new Promise((r) => setTimeout(r, 1000));
throw new Error("some error");
// return msg;
};
export const Thread = () => {
const formRef = React.useRef();
const [messages, setMessages] = React.useState([]);
const [optimisticMessages, addOptimisticMessage] = React.useOptimistic(
messages,
(state, newMessage) => [
...state,
{
text: newMessage,
sending: true,
},
]
);
const sendMessage = async (formData) => {
try {
const sentMessage = await send(formData.get("message"));
setMessages((messages) => [...messages, { text: sentMessage }]);
} catch (err) {
alert("메세지 전송 실패");
}
};
const formAction = async (formData) => {
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>
</>
);
};
현재상태, 업데이트 함수(순수함수)를 인자로 받고 optimistic-state와 dispatch함수를 return한다.
위의 예시는 1초뒤에 에러를 뱉는데 요청이 resolve되면 optimistic-state는 다시 current-state를 기준으로 ‘초기화’된다. 하지만 테스트결과 optimistic-state랑 current-state가 합쳐져서 보이는 시점이 존재하는거같긴 하다 🥹
useFormStatus (New)
import { Submit } from "../src/components/Submit";
export default function Page() {
return (
<form action={getData}>
<Submit>submit</Submit>
<input type="text" name="name" />
</form>
);
}
"use client";
import React from "react";
import { useFormStatus } from "react-dom";
export const Submit = ({ children }) => {
const status = useFormStatus();
return (
<button disabled={status.pending} type="submit">
{children}
{status.data && status.data.get("name")}
</button>
);
};
상위 form 엘리먼트의 각종 status를 가져올 수 있는 hook이다. 보통 디자인컴포넌트 설계를 생각하면 상위 form상태를 가져오기위해서 (disabled를 위한 pending상태 등) 데이터를 props로 넘길텐데 이때를 위한 기능이고 object를 리턴하는데 다음과 같은 필드를 가진다
- pending : 말그대로.
- data : submitting된 form의 FormData
- method : form에 특별한 method props가 없다면 ‘get’이 기본이다
- action : form의 action props로 넘겨진 액션에대한 참조다
기타 업데이트 사항
ref as props
ref가 드디어 props로 넘길 수 있게 되었다! 즉 forwardRef가 필요없어진다는 말이므로 미래에 비활성화 및 제거된다고 한다.
Context as Provider
Context가 그 자체로 Provider할 수 있게된다. <Context.Provider>
가 아닌 <Context>
로!
supporting to metadata
기존에는 <head>
태그에 수동으로 적어주거나 react-helmet
등 라이브러리의 도움을 받았어야하는데, 이제 네이티브로 컴포넌트 안에서 metadata와 관련된 태그 link, metadata등을 사용하면 head까지 호이스팅 된다고한다!
ref with clean-up
ref callback에 clean-up함수가 생겼다. 그렇기때문에 return 타입은 함수로 정해져있어서 잘못하면 typed-error가 발생할거다
컴파일러를 위한 eslint-plugin@beta 추가
당장히 사용해보지는 않을거지만, 추후 완전한 컴파일러가 준비됐을때 빠른 채택을 위해 eslint-plugin-react-compiler@beta
라는 린트플러그인을 적용해 룰위반을 수정하는것도 나쁘지않아보인다.
컴파일러와는 별개니 당장 설치해서 적용해도 된다고한다.
나름 자세히 살펴본 것 같다. 언급한것 외에도 에러로깅을 포함한 여러 support 및 업데이트가 존재하니 자세한건 공식문서를 봐보자!