항상 생각없이 리액트를 사용하면서
내가 버전 몇을 사용하고 있는지 새삼 깨닫지 못하고 있다가,
며칠 전 리액트 19버전이 릴리즈 될 수 있다는 아티클을 하나 읽었다.
내가 처음 개발을 하겠다고 나대던 시기가 2019년,
이때의 리액트는 16.8.0 버전으로 Hook이 도입되기 시작했던 버전이었다.
근데 지금은 19라고? 물론 아직 릴리즈 되지는 않았지만
미리 알아두어서 도태되지 않고 따라갈 수 있게 준비해두는 건 좋을 것 같아서
그래서 작성하게 된 글이다.
해당 글은 작성자의 허락을 맡아 New Features in React 19 – Updates with Code Examples 글을 번역 및 참조하였습니다~!
React 컴파일러
서버 컴포넌트
액션
문서 메타데이터
Assets Loading
웹 컴포넌트
향상된 Hooks
우선, 리액트 팀 멤버들의 트위터 X를 보면, React 19는 현재 Canary 버전에 적용되어 있지만 React Compiler는 아직 적용되지 않았고 다른 팀 멤버에 따르면 24년 말에 적용될 수 있다고 말했다. 즉 React 19 != React Compiler이니 React 19 버전의 변경점이라고 할 수 없지만 미리 알아두도록 하자.
그렇다면 React Compiler는 무엇일까?
React는 현재 state 값이 변경될 때, 너무 많은 리렌더링이 발생하거나 자동으로 리렌더링이 발생하지 않는다. 그렇기 때문에 수동으로 렌더링을 조정하는 방법을 사용했는데, 이때 사용했던 것들이 바로 useMemo
, useCallback
및 memo
API였다. 그러나 이러한 수동적인 조정은 결국 절충안이었으며, 코드가 복잡해지고 오류가 발생하기도 쉽고 최신 상태를 유지하려면 추가 작업이 필요했다.
여기서 적용될 React Compiler는, 바로 모든 것이 자동화 (메모화) 된다는 것이다. 그렇기 때문에 더이상 useCallback
이나 useMemo
는 사용할 필요가 없어질 것이며, 리렌더링을 줄이기 위해 state를 아래로 이동시키거나 컴포넌트를 자식으로 전달하는 것과 같은 합성 기법들은 사라질 것이다.
그동안 리액트는 주로 컴포넌트를 클라이언트 사이드에서 실행했기 때문에, 서버 사이드 렌더링과 같은 방법을 사용하려면 Next.js와 같은 프레임워크를 사용해야 했다. Next.js에서 클라이언트 사이드에서 컴포넌트를 실행하기 위해 use client
지시문을 사용하는 것처럼, React 19에서는 use server
지시문을 통해 서버사이드에서 컴포넌트를 실행할 수 있게 되었다.
'use server';
export default async function requestUsername(formData) {
const username = formData.get('username');
if (canRequest(username)) {
// ...
return 'successful';
}
return 'failed';
}
서버 컴포넌트가 제공되면서 React 19에서는 다음과 같은 이점을 기대할 수 있다.
리액트 19에서는 액션을 사용하여 HTML 태그 <form/>
과 액션을 통합할 수 있다. 즉, onSubmit
과 같은 이벤트를 액션으로 대체할 수 있는 것이다.
<form onSubmit={search}>
<input name="query" />
<button type="submit">Search</button>
</form>
리액트 19 버전 이전에는, onSubmit과 같은 이벤트들로 form 양식 제출을 했었다. 그러나 이 이벤트는 클라이언트 측에서만 사용 가능한 search
메서드를 사용해서, 서버 사이드에서는 실행될 수 없었다.
"use server"
const submitData = async (userData) => {
const newUser = {
username: userData.get('username'),
email: userData.get('email')
}
console.log(newUser)
}
const Form = () => {
return <form action={submitData}>
<div>
<label>Name</label>
<input type="text" name='username'/>
</div>
<div>
<label>Name</label>
<input type="text" name="email" />
</div>
<button type='submit'>Submit</button>
</form>
}
export default Form;
리액트 19 이후에는 서버 사이드에서 작업이 가능하므로, 클라이언트 사이드에서만 사용되는 onSubmit
과 같은 이벤트 대신에 action
속성을 사용하게 된다. action 속성의 값은 클라이언트 사이드나 서버 사이드에서 데이터를 제출할 수 있다. 이 action을 통해 동기 및 비동기 작업을 모두 실행할 수 있으므로, 데이터 제출 관리 및 state 업데이트는 간소화 된다.
위 코드에서는 submitData
는 서버 컴포넌트의 action이 되며, form
은 submitData
를 action으로 사용하는 클라이언트 사이드의 컴포넌트이다. submitData는 서버에서 실행되며, 클라이언트(form
)와 서버(submitData
) 컴포넌트간의 통신은 action
때문에 가능해진 것이다.
리액트 19 버전에서는 웹 컴포넌트를 사용하여 HTML, CSS, JavaScript를 통해 커스텀 컴포넌트를 생성하여 표준 HTML 태그인 것 처럼 웹 애플리케이션에 원활하게 통합할 수 있도록 할 수 있다. 사실 웹 컴포넌트라는 개념이 기존에 내가 알고 있던 것이 아니라 좀 생소하기는 한데, 기존에는 웹 컴포넌트를 리액트에서 사용하려면 웹 컴포넌트를 리액트 컴포넌트로 변환하거나 추가적인 패키지를 설치, 혹은 추가적인 코드를 작성해야 했다면 이것들을 배제하고 쉽게 사용할 수 있게 된다는 의미같다. 만일 Carousel 웹 컴포넌트를 발견했다면, 별도의 라이브러리 설치를 하거나 리액트 코드로의 변환이 필요 없이 가져와 사용할 수 있다는 의미이다.
현재로써는 별도의 코드 설명과 같은 자세한 업데이트 내용은 없지만, 릴리즈가 된다면 개발은 보다 간소화되고 React 애플리케이션에서 기존 웹 구성 요소의 방대한 생태계를 활용할 수 있게 될 것이다.
HTML 메타 태그와 같은 요소들은 SEO를 최적화하고 접근성을 보장하는 데에 매우 중요한 요소이다. React는 SPA(Single Page Application)이기 때문에 다양한 경로에서 이러한 요소를 관리하는 것이 번거로웠었다.
import React, { useEffect } from 'react';
const HeadDocument = ({ title }) => {
useEffect(() => {
document.title = title;
const metaDescriptionTag = document.querySelector('meta[name="description"]');
if (metaDescriptionTag) {
metaDescriptionTag.setAttribute('content', 'New description');
}
}, [title]);
return null;
};
export default HeadDocument;
기존에는 props에 따라 title
이나 meta
태그를 useEffect에서 변경하는 위와 같은 로직을 사용하고는 했다. 또한 리액트 내에서도 JavaScript를 사용했었다. 하지만 이와 같은 방법은 clean한 코드를 작성하는데에는 좋지 않았다.
Const HomePage = () => {
return (
<>
<title>Freecodecamp</title>
<meta name="description" content="Freecode camp blogs" />
// Page content
</>
);
}
리액트 19 버전 이후에는, 각각의 컴포넌트 내에 title
과 meta
태그가 사용이 가능해져서, 이전과 같은 별도의 로직 없이도 SEO를 최적화하고 접근성을 보장할 수 있게 되었다.
기존에는 브라우저에서 View가 먼저 렌더링되고, stylesheet, font, image와 같은 부수적인 요소들은 view가 렌더링 된 이후에 렌더링 되는 경우들이 종종 있었다. 이로 인해서 사용자가 브라우저를 봤을때 깜빡임 현상이 나타나곤 했었다.
리액트 19에서는 사용자가 페이지를 탐색할 때, 이미지와 기타 파일들이 백그라운드에서 로드되기 때문에 페이지 로드 시간을 줄이고 사용자의 대기 시간을 줄이는데 도움이 된다. 또한, lifeCycle Suspense를 도입하여 언제 컨텐츠들이 표시될 수 있는지에 대한 시간을 결정하여 사용자로 하여금 깜빡임 현상을 제거할 수 있게 하였다.
리액트 19에서는 여러가지 향상된 Hooks들이 등장했는데, 이에 대해 알아보자.
앞서 설명한 것처럼, React Compiler가 도입될 경우에 전체적으로 메모이제이션이 이루어지기 때문에 useMemo()
는 더이상 필요성이 사라졌다.
ref
역시 이제 props로 전달할 수 있기 때문에, forwardRef()
는 사용되지 않을 것이다. 이는 코드를 보다 간결하게 작성할 수 있게 한다.
// Before React 19
import React, { forwardRef } from 'react';
const ExampleButton = forwardRef((props, ref) => (
<button ref={ref}>
{props.children}
</button>
));
// After React 19
import React from 'react';
const ExampleButton = ({ ref, children }) => (
<button ref={ref}>
{children}
</button>
);
React 19에서는 새로이 use()
라는 Hook이 등장하였다.
const value = use(resource);
이 Hook은 Promise, async, context와 같은 코드들을 사용화 하는 것을 단순화한다.
import { use } from "react";
const fetchUsers = async () => {
const res = await fetch('https://jsonplaceholder.typicode.com/users');
return res.json();
};
const UsersItems = () => {
const users = use(fetchUsers());
return (
<ul>
{users.map((user) => (
<div key={user.id} className='bg-blue-50 shadow-md p-4 my-6 rounded-lg'>
<h2 className='text-xl font-bold'>{user.name}</h2>
<p>{user.email}</p>
</div>
))}
</ul>
);
};
export default UsersItems;
위 코드를 통해 use
의 사용법을 알아보자.
fetchUsers
라는 GET 요청을 보내는 함수를 생성한다.useEffect
나 useState
Hook 대신에 use
를 사용하여 fetchUsers
를 실행한다.users
에 fetchUsers
의 결과값이 저장된다.users
를 사용하여 리스트를 매핑한다.기존의 useEffect나 useState를 사용하던 코드보다 훨씬 간결해진 모습을 볼 수 있다!
Context API에서도 use()
를 사용할 수 있다.
import { createContext, useState, use } from 'react';
const ThemeContext = createContext();
const ThemeProvider = ({ children }) => {
const [theme, setTheme] = useState('light');
const toggleTheme = () => {
setTheme((prevTheme) => (prevTheme === 'light' ? 'dark' : 'light'));
};
return (
<ThemeContext.Provider value={{ theme, toggleTheme }}>
{children}
</ThemeContext.Provider>
);
};
const Card = () => {
// use Hook()
const { theme, toggleTheme } = use(ThemeContext);
return (
<div
className={`p-4 rounded-md ${
theme === 'light' ? 'bg-white' : 'bg-gray-800'
}`}
>
<h1
className={`my-4 text-xl ${
theme === 'light' ? 'text-gray-800' : 'text-white'
}`}
>
Theme Card
</h1>
<p className={theme === 'light' ? 'text-gray-800' : 'text-white'}>
Hello!! use() hook
</p>
<button
onClick={toggleTheme}
className='bg-blue-500 hover:bg-blue-600 text-white rounded-md mt-4 p-4'
>
{theme === 'light' ? 'Switch to Dark Mode' : 'Switch to Light Mode'}
</button>
</div>
);
};
const Theme = () => {
return (
<ThemeProvider>
<Card />
</ThemeProvider>
);
};
export default Theme
useFormStatus()
는 React 19에서 나온 새로운 Hook으로, form을 보다 효과적으로 사용할 수 있게 해주는 Hook이다. useFormStatus()
는 form 상태의 정보 - 제출이 완료되었는지, 유효성 검사가 끝났는지 - 를 처리하는 데 사용하는 Hook이다.
const { pending, data, method, action } = useFormStatus();
or
const { status } = useFormStatus()
pending
form이 pending
상태일 경우는 true
를, 아닐 경우에는 false
를 반환한다.
data
form이 제출하는 데이터를 포함하는 FormData 인터페이스를 구현한다.
method
HTTP 메서드 – GET 또는 POST. 기본적으로 GET이다.
action
함수 참조이다.
import { useFormStatus } from "react-dom";
function Submit() {
const status = useFormStatus();
return <button disabled={status.pending}>{status.pending ? 'Submitting...' : 'Submit'}</button>;
}
const formAction = async () => {
// Simulate a delay of 2 seconds
await new Promise((resolve) => setTimeout(resolve, 3000));
}
const FormStatus = () => {
return (
<form action={formAction}>
<Submit />
</form>
);
};
export default FormStatus;
위 코드처럼 useFormStatus.pending
을 통해 버튼의 disabled 값과 문구를 변경할 수 있다.
useFormState()
역시 form에 관련된 새로운 Hook으로, form 제출 결과에 따라 state를 업데이트 할 수 있다.
const [state, formAction] = useFormState(fn, initialState, permalink?);
fn
form을 제출하거나 버튼을 눌렀을 때 호출되는 함수이다.
initialState
state의 초기값이다.
permalink
선택값으로, URL이나 링크가 들어간다. fn이 서버에서 실행될 경우, permalLink로 리다이렉트 된다.
import { useFormState } from 'react-dom';
const FormState = () => {
const submitForm = (prevState, queryData) => {
const name = queryData.get("username");
console.log(prevState); // previous form state
if(name === 'john'){
return {
success: true,
text: "Welcome"
}
}
else{
return {
success: false,
text: "Error"
}
}
}
const [ message, formAction ] = useFormState(submitForm, null)
return <form action={formAction}>
<label>Name</label>
<input type="text" name="username" />
<button>Submit</button>
{message && <h1>{message.text}</h1>}
</form>
}
export default FormState;
위 코드에서는 이름이 John일 경우에는 Welcome, 아닐 경우에는 Error 메시지를 반환하게 된다.
useOptimistic()
은 비동기 작업이 진행되는 동안 다른 state를 표시할 수 있는 Hook이다.
사용자 경험을 향상키는데 도움이 되며 더 빠른 응답을 제공한다. 이는 서버와 상호 작용해야 하는 애플리케이션에 유용하다.
const [ optimisticMessage, addOptimisticMessage] = useOptimistic(state, updatefn)
예를 들어, 응답이 진행되는 동안 사용자에게 즉각적인 응답을 제공하기 위해 state를 표시할 수 있는데, 서버에서 실제 응답이 반환되면 "낙관적" 상태가 해당 응답으로 대체되게 된다.
useOptimistic 요청이 성공할 경우 즉시 UI를 업데이트 하고, 사용자는 작업을 수행하면 낙관적인 결과를 볼 수 있기 때문에 "Optimistic"이라는 이름을 갖고 있다.
import { useOptimistic, useState } from "react";
const Optimistic = () => {
const [messages, setMessages] = useState([
{ text: "Hey, I am initial!", sending: false, key: 1 },
]);
const [optimisticMessages, addOptimisticMessage] = useOptimistic(
messages,
(state, newMessage) => [
...state,
{
text: newMessage,
sending: true,
},
]
);
async function sendFormData(formData) {
const sentMessage = await fakeDelayAction(formData.get("message"));
setMessages((messages) => [...messages, { text: sentMessage }]);
}
async function fakeDelayAction(message) {
await new Promise((res) => setTimeout(res, 1000));
return message;
}
const submitData = async (userData) => {
addOptimisticMessage(userData.get("username"));
await sendFormData(userData);
};
return (
<>
{optimisticMessages.map((message, index) => (
<div key={index}>
{message.text}
{!!message.sending && <small> (Sending...)</small>}
</div>
))}
<form action={submitData}>
<h1>OptimisticState Hook</h1>
<div>
<label>Username</label>
<input type="text" name="username" />
</div>
<button type="submit">Submit</button>
</form>
</>
);
};
export default Optimistic;
사용자가 폼에 메시지를 입력하고 전송 버튼을 누르면, useOptimistic Hook은 메시지가 실제로 서버로 전송되기 전에 “Sending..” 라벨이 있는 목록에 메시지가 즉시 나타나도록 한다. 그런 다음 폼은 백그라운드에서 메시지를 실제로 전송하려고 시도하고, 서버가 메시지를 받았음을 확인하면, “Sending…” 라벨이 제거된다. 이러한 접근법은 속도와 반응성의 느낌을 주어 사용자에게 "낙관적인" 효과를 기대할 수 있다.
이와 같이, React 19 버전의 변경점들에 대해 알아보았다. 현재 일부 기능들은 react@canary 에서 사용 가능하며, 19 버전의 릴리즈일은 아직 미정이다. 이렇게 새로운 기술들이 나와서 정리할 때마다 느끼는 거지만, 결국 써봐야만 내 기술이 되고 와닿게 되는 것 같다. 그래도 생판 모르는 상태에서 쓰는거보단 정리하고 쓰는게 나으니 이번 블로깅도 만족!
https://www.freecodecamp.org/news/new-react-19-features/ (Thanks, Neha😊)
https://www.frontoverflow.com/magazine/5/%EB%A6%AC%EC%95%A1%ED%8A%B8%20%EC%BB%B4%ED%8C%8C%EC%9D%BC%EB%9F%AC%20(React%20Compiler)
https://ko.react.dev/reference/react/useOptimistic