React 19에서 새로운 use() 훅을 사용하여 더 깔끔한 Async 컴포넌트 만들기
Source: Dev.to
React를 오래 사용해 왔다면, 컴포넌트에서 비동기 데이터를 처리하는 번거로움을 겪어봤을 것입니다. useEffect, useState, 그리고 다양한 데이터‑fetching 라이브러리 사이에서 코드는 종종 복잡해집니다. React 19는 게임 체인저인 솔루션을 도입했습니다: use() 훅.
이 새로운 훅이 어떻게 비동기 데이터 처리를 단순화하고 코드를 훨씬 깔끔하게 만드는지 보여드리겠습니다.
옛 방식: 장황하고 오류가 발생하기 쉬움
React 19 이전에, 컴포넌트 내부에서 데이터를 가져오는 코드는 다음과 같은 형태였습니다:
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
setLoading(true);
fetch(`/api/users/${userId}`)
.then(res => res.json())
.then(data => {
setUser(data);
setLoading(false);
})
.catch(err => {
setError(err);
setLoading(false);
});
}, [userId]);
if (loading) return ;
if (error) return ;
return {user.name};
}
간단한 데이터 가져오기에도 불구하고 많은 보일러플레이트가 필요합니다. 세 가지 상태를 관리하고, effect 의존성을 올바르게 처리하며, 어떤 엣지 케이스도 놓치지 않았는지 기대해야 합니다.
새로운 방법: 깔끔하고 선언적
React 19의 use() 훅은 모든 것을 바꿉니다. 프로미스가 해결되는 동안 컴포넌트를 일시 중단시켜, 로딩 및 오류 상태를 컴포넌트 트리의 더 높은 수준에서 처리합니다.
import { use } from 'react';
function UserProfile({ userId }) {
const user = use(fetchUser(userId));
return {user.name};
}
그게 전부입니다. 상태 관리도, 효과도, 보일러플레이트도 없습니다. 컴포넌트는 단순히 프로미스가 해결될 때까지 기다립니다.
실제 작동 방식
use() 훅은 프로미스를 받아서 다음과 같이 동작합니다:
- 보류 중인 프로미스가 있으면 컴포넌트를 Suspended 상태로 만들고
- 프로미스가 해결되면 재개하여 반환된 값을 사용합니다
- 프로미스가 거부되면 오류를 발생시키며 (Error Boundary 로 잡을 수 있습니다)
다음은 현실적인 데이터‑fetching 시나리오를 포함한 전체 예시입니다:
import { use, Suspense, ErrorBoundary } from 'react';
// Async function that returns a promise
async function fetchUser(userId) {
const res = await fetch(`/api/users/${userId}`);
if (!res.ok) throw new Error('Failed to fetch user');
return res.json();
}
function UserProfile({ userId }) {
const user = use(fetchUser(userId));
return (
<>
<h2>{user.name}</h2>
<p>{user.email}</p>
<span>{user.role}</span>
</>
);
}
// Wrap it with Suspense and ErrorBoundary
function App() {
return (
<ErrorBoundary fallback={<div>Something went wrong.</div>}>
<Suspense fallback={<div>Loading...</div>}>
<UserProfile userId="123" />
</Suspense>
</ErrorBoundary>
);
}
여러 비동기 값 처리하기
가장 뛰어난 기능 중 하나는 use()가 여러 프라미스를 우아하게 처리한다는 점입니다:
function Dashboard({ userId }) {
const [user, posts] = use(Promise.all([
fetchUser(userId),
fetchPosts(userId)
]));
return (
<>
<h2>Welcome, {user.name}</h2>
{/* render posts here */}
</>
);
}
이제 Promise.all + useState + useEffect 조합을 사용할 필요 없이, 컴포넌트 안에서 비동기 코드를 바로 작성할 수 있습니다.
기존 패턴과 통합
use()를 기존 데이터‑패칭 라이브러리와 함께 계속 사용할 수 있습니다. 예를 들어 TanStack Query를 사용할 때:
import { useQuery } from '@tanstack/react-query';
import { use } from 'react';
function UserAvatar({ userId }) {
const query = useQuery({
queryKey: ['user', userId],
queryFn: () => fetchUser(userId)
});
// Convert to promise for use()
const user = use(
query.isSuccess ? Promise.resolve(query.data) : new Promise(() => {})
);
return <img src={user.avatar} alt={user.name} />;
}
또는 간단한 래퍼를 만들 수 있습니다:
function useAsync(asyncFn) {
return use(Promise.resolve().then(() => asyncFn()));
}
function UserCard({ userId }) {
const user = useAsync(() => fetchUser(userId));
return {user.name};
}
올바른 오류 처리
use() 훅은 React의 Error Boundary 패턴과 아름답게 통합됩니다:
function PostWithComments({ postId }) {
const post = use(fetchPost(postId));
const comments = use(fetchComments(postId));
return (
<>
<h2>{post.title}</h2>
<p>{post.content}</p>
{/* render comments here */}
</>
);
}
// Any error in the component tree bubbles up to the Error Boundary
function ErrorFallback({ error }) {
return <div>Something went wrong: {error.message}</div>;
}
function App() {
return (
<ErrorBoundary fallback={<ErrorFallback />}>
<Suspense fallback={<div>Loading...</div>}>
<PostWithComments postId="456" />
</Suspense>
</ErrorBoundary>
);
}
use()와 useEffect를 언제 사용할까
use()를 사용할 때:
- 컴포넌트에서 렌더링되는 데이터를 가져와야 할 때.
- 더 간단하고 가독성이 높은 비동기 코드를 원할 때.
- Suspense를 염두에 두고 구축할 때.
useEffect를 고수해야 할 때:
- 부수 효과(로그 기록, 분석, 구독 등)를 처리할 때.
- 데이터가 렌더링 결과에 직접적인 영향을 주지 않을 때.
왜 컴포넌트에서 렌더링할까?
- when fetching이 발생하는 시점을 세밀하게 제어해야 합니다.
더 큰 그림
The use() hook은 단순한 문법 설탕이 아니라 React의 “React 19: The Era of Async.” 비전의 일부입니다.
Server Components와 Actions와 결합하면, 이는 React 애플리케이션을 구축하는 방식에 근본적인 변화를 나타냅니다.
각 컴포넌트마다 로딩 상태를 관리하는 대신, 최상위 레벨에서 한 번 정의하고 React가 나머지를 처리하도록 합니다.
이렇게 하면 더 깔끔한 사고 모델을 제공하고 코드 양도 줄어듭니다.
시도해 보기
React 19(또는 베타)를 사용하고 있다면, 컴포넌트에서 use()를 실험해 보세요. 보일러플레이트가 얼마나 빨리 사라지는지, 비동기 코드가 얼마나 더 읽기 쉬워지는지 놀라실 겁니다.
React의 미래는 async‑native입니다. 이를 받아들이세요.
새로운 use() 훅에 대해 어떻게 생각하시나요?
이미 사용해 보셨나요? 아래 댓글에 여러분의 생각을 남겨 주세요.
