
React에서 비동기 데이터를 다루는 다양한 방식
React는 state 중심의 UI 프레임워크다.
즉, 사용자 인터페이스에 표시되는 동적 데이터는 모두 state를 통해 관리되며, 이 state가 변경될 때마다 컴포넌트는 다시 렌더링된다.
그렇다면 이런 흐름 속에서 비동기 데이터는 어떻게 처리해야 할까?
예를 들어 서버에서 상품 목록을 가져온다거나, 외부 API에서 날씨 정보를 받아오는 등의 작업은 **비동기(Promise 기반)**이다.
이러한 데이터를 React에서 표현하기 위해 가장 기본적인 패턴은 다음과 같다:
useEffect를 활용한 비동기 데이터 로딩
React의 대표적인 비동기 데이터 처리 방법은 useEffect 훅을 사용하는 것이다.
'use client'; import { useEffect, useState } from 'react'; import { Product } from '@/libs/types/types'; import { fetchProducts } from '@/libs/fetchProducts'; export function Products() { const [products, setProducts] = useState<Product[]>([]); useEffect(() => { (async () => { const result = await fetchProducts(); setProducts(result); })(); }, []); return ( <ul className="flex flex-col gap-4"> {products.map((product) => ( <li key={product.id} className="border border-green-400"> <strong>{product.title}</strong> </li> ))} </ul> ); }
흐름 요약
- products라는 초기 상태값을 빈 배열로 설정
- useEffect 안에서 비동기 데이터를 호출
- setState로 데이터를 갱신
- 컴포넌트가 리렌더링되어 새로운 데이터가 화면에 표시됨
이 방식은 클라이언트에서 데이터를 가져오는 전통적인 패턴이며, CSR(Client Side Rendering)에서 주로 사용된다.
서버 사이드에서 비동기 데이터를 처리하려면?
Next.js나 Remix처럼 **서버 사이드 렌더링(SSR)**을 지원하는 프레임워크에서는
초기 렌더링 시점부터 데이터를 가져와 HTML을 완성해야 하는 요구가 있다.
하지만 서버에서는 useEffect나 useState를 사용할 수 없다.
(서버는 DOM이 없고, 리렌더링이라는 개념 자체가 존재하지 않기 때문)
그래서 등장한 방식이 바로 비동기 컴포넌트다.
비동기 컴포넌트와 Suspense
컴포넌트 자체를 비동기로 만들고, Suspense를 활용해 로딩 상태를 처리하는 패턴이다.
// 잘못된 사용 (클라이언트 컴포넌트에 async 사용) 'use client'; import { fetchProducts } from '@/libs/fetchProducts'; export async function Products() { const products = await fetchProducts(); return ( <ul> {products.map((product) => ( <li key={product.id}>{product.title}</li> ))} </ul> ); }
이 방식은 잘못된 사용이다. async 컴포넌트는 서버에서만 사용할 수 있으며, 클라이언트 컴포넌트에는 async를 붙일 수 없다.
올바른 구조
// 클라이언트 컴포넌트 'use client'; import { Product } from '@/libs/types/types'; export function Products({ products }: { products: Product[] }) { return ( <ul> {products.map((product) => ( <li key={product.id}>{product.title}</li> ))} </ul> ); }
// 서버 컴포넌트 import { fetchProducts } from '@/libs/fetchProducts'; import { Products } from './products'; export async function ProductsContainer() { const products = await fetchProducts(); return <Products products={products} />; }
이렇게 서버에서 데이터를 미리 받아서 클라이언트 컴포넌트에 props로 전달하면, 클라이언트는 useEffect 없이 데이터를 렌더링할 수 있다.
하지만 이런 방식은 비동기 데이터를 다루기 위해 컴포넌트 자체가 비동기 컴포넌트가 되어야하며, 이에 따라 컴포넌트 구조가 분리되어야 하는 불편함이 따르기도한다.
use 훅의 등장
React 19에서 use(promise)라는 새로운 훅이 실험적으로 도입되며 이 문제를 해결할 수 있게 되었다.
구조를 단순하게
// 서버 컴포넌트 import { fetchProducts } from '@/libs/fetchProducts'; import { Suspense } from 'react'; import { Products } from './products'; export default function Page() { const productsPromise = fetchProducts(); return ( <div> <h1>Use Example</h1> <Suspense fallback={<div>Loading...</div>}> <Products productsPromise={productsPromise} /> </Suspense> </div> ); }
// 클라이언트 컴포넌트 'use client'; import { Product } from '@/libs/types/types'; import { use } from 'react'; export function Products({ productsPromise }: { productsPromise: Promise<Product[]> }) { const products = use(productsPromise); return ( <ul> {products.map((product) => ( <li key={product.id}>{product.title}</li> ))} </ul> ); }
이제는 클라이언트 컴포넌트에서도 직접 비동기 데이터를 받아 사용할 수 있으며
useEffect도, 별도의 서버 컴포넌트도 필요 없다.
마무리
React는 원래 state 중심으로 동작하는 라이브러리이기 때문에, 비동기 데이터와 UI의 관계를 잘 정리해두는 것이 중요하다.
- useEffect는 클라이언트에서의 비동기 데이터 패칭에 적합
- 서버에서 데이터를 패칭할 땐 서버 컴포넌트를 분리하거나
- 실험적인 use를 활용하면 더욱 깔끔한 구조를 만들 수 있다
react19에 추가된 use훅으로 비동기 데이터를 다루는 방식 또한 훨씬 자연스럽고 직관적으로 진화하고 있다.
이를 잘 활용하면 사용자 경험과 개발 효율성 모두를 향상시킬 수 있다.
예제
이곳에서 예제를 확인해보실 수 있습니다.
- client-component
- async-component
- use-component
순으로 비동기 데이터 처리 방식의 변화를 확인해보시는것을 추천드립니다.
react-use
entrolEC • Updated Jun 29, 2025
