본문으로 건너뛰기
Kreath Archive
TechProjectsBooksAbout
TechProjectsBooksAbout

내비게이션

  • Tech
  • Projects
  • Books
  • About
  • Tags

카테고리

  • AI / ML
  • 웹 개발
  • 프로그래밍
  • 개발 도구

연결

  • GitHub
  • Email
  • RSS
© 2026 Kreath Archive. All rights reserved.Built with Next.js + MDX
홈TechProjectsBooksAbout
//
  1. 홈
  2. 테크
  3. 4장: use() API와 새로운 데이터 패턴
2026년 1월 29일·웹 개발·

4장: use() API와 새로운 데이터 패턴

React 19의 use() API로 Promise와 Context를 조건부로 소비하는 방법, 서버-클라이언트 스트리밍 패턴, 기존 훅과의 차이를 다룹니다.

15분931자8개 섹션
reactnextjsperformancefrontendtypescript
공유
react19-rsc4 / 11
1234567891011
이전3장: Server Actions로 서버-클라이언트 통합하기다음5장: 새로운 훅 - useActionState, useFormStatus, useOptimistic

3장에서 Server Actions로 데이터를 변경하는 방법을 다루었습니다. 이번 장에서는 React 19에 새로 도입된 use() API를 살펴봅니다. use()는 Promise와 Context를 렌더링 중에 읽을 수 있는 새로운 API로, 기존 훅과 달리 조건문 안에서도 호출할 수 있다는 특별한 성질을 갖습니다.

use()란 무엇인가

use()는 Promise 또는 Context를 인자로 받아 그 값을 반환하는 API입니다.

use() 기본 사용법
typescript
import { use } from 'react';
 
// Promise를 읽기
function Comments({ commentsPromise }: { commentsPromise: Promise<Comment[]> }) {
  const comments = use(commentsPromise);
  return (
    <ul>
      {comments.map(c => <li key={c.id}>{c.text}</li>)}
    </ul>
  );
}
 
// Context를 읽기
function ThemedButton() {
  const theme = use(ThemeContext);
  return <button style={{ background: theme.primary }}>클릭</button>;
}
Info

use()는 React 훅처럼 보이지만, 훅의 규칙(Rules of Hooks)을 따르지 않습니다. 조건문, 반복문, 조기 반환 이후에도 호출할 수 있습니다. 이것이 useContext와의 가장 큰 차이점입니다.

use()로 Promise 읽기

Suspense와의 통합

use()로 Promise를 읽으면, Promise가 해결될 때까지 가장 가까운 Suspense 경계가 fallback을 표시합니다.

use() + Suspense 기본 패턴
typescript
import { Suspense } from 'react';
import { use } from 'react';
 
// Server Component에서 Promise 생성
async function ArticlePage({ params }: { params: { id: string } }) {
  const article = await getArticle(params.id);
 
  // Promise를 await하지 않고 전달
  const commentsPromise = getComments(params.id);
 
  return (
    <article>
      <h1>{article.title}</h1>
      <p>{article.content}</p>
 
      <Suspense fallback={<CommentsSkeleton />}>
        <CommentList commentsPromise={commentsPromise} />
      </Suspense>
    </article>
  );
}
 
// Client Component에서 use()로 소비
'use client';
 
function CommentList({ commentsPromise }: {
  commentsPromise: Promise<Comment[]>
}) {
  const comments = use(commentsPromise);
 
  return (
    <section>
      <h2>댓글 {comments.length}개</h2>
      {comments.map(comment => (
        <div key={comment.id}>
          <strong>{comment.author}</strong>
          <p>{comment.text}</p>
        </div>
      ))}
    </section>
  );
}

이 패턴에서 데이터 흐름은 다음과 같습니다.

  1. Server Component가 article을 await로 즉시 가져옵니다 (블로킹).
  2. commentsPromise는 await하지 않고 Client Component에 전달합니다.
  3. 서버는 article 내용을 먼저 스트리밍합니다.
  4. Client Component는 use(commentsPromise)로 댓글을 읽으려 시도하고, 아직 해결되지 않았으면 Suspense가 skeleton을 표시합니다.
  5. Promise가 해결되면 댓글이 자동으로 나타납니다.

서버-클라이언트 스트리밍

이 패턴의 핵심 가치는 서버에서 생성된 Promise를 클라이언트로 스트리밍할 수 있다는 점입니다. 서버는 Promise의 해결을 기다리지 않고 즉시 응답을 시작하며, Promise가 해결되면 결과를 클라이언트로 스트리밍합니다.

다중 스트리밍 패턴
typescript
async function DashboardPage() {
  // 즉시 필요한 데이터
  const user = await getCurrentUser();
 
  // 병렬로 시작하되, 스트리밍할 데이터
  const statsPromise = fetchStats(user.id);
  const notificationsPromise = fetchNotifications(user.id);
  const activityPromise = fetchRecentActivity(user.id);
 
  return (
    <div>
      <header>
        <h1>{user.name}의 대시보드</h1>
      </header>
 
      <div className="grid grid-cols-3 gap-6">
        <Suspense fallback={<StatsSkeleton />}>
          <StatsPanel statsPromise={statsPromise} />
        </Suspense>
 
        <Suspense fallback={<NotificationsSkeleton />}>
          <NotificationsPanel notificationsPromise={notificationsPromise} />
        </Suspense>
 
        <Suspense fallback={<ActivitySkeleton />}>
          <ActivityFeed activityPromise={activityPromise} />
        </Suspense>
      </div>
    </div>
  );
}

세 개의 Promise가 병렬로 실행되며, 각각 독립적으로 스트리밍됩니다. 가장 빠르게 해결되는 데이터부터 화면에 나타나므로 사용자는 점진적으로 콘텐츠를 볼 수 있습니다.

use()로 Context 읽기

조건부 Context 소비

useContext는 훅이므로 조건문 안에서 호출할 수 없습니다. use()는 이 제약이 없습니다.

조건부 Context 소비
typescript
import { use } from 'react';
 
function UserGreeting({ user }: { user: User | null }) {
  if (!user) {
    return <p>로그인해주세요.</p>;
  }
 
  // 조기 반환 이후에 use() 호출 가능
  const theme = use(ThemeContext);
  const locale = use(LocaleContext);
 
  return (
    <div style={{ color: theme.text }}>
      <p>
        {locale === 'ko'
          ? `${user.name}님, 환영합니다.`
          : `Welcome, ${user.name}.`}
      </p>
    </div>
  );
}

useContext로는 이 패턴이 불가능합니다.

useContext의 제약
typescript
function UserGreeting({ user }: { user: User | null }) {
  // useContext는 항상 최상위에서 호출해야 함
  const theme = useContext(ThemeContext);   // 항상 실행됨
  const locale = useContext(LocaleContext); // 항상 실행됨
 
  if (!user) {
    return <p>로그인해주세요.</p>;
  }
 
  // theme과 locale을 사용하지 않는 경로에서도
  // 이미 호출됨 (불필요한 구독)
  return <div style={{ color: theme.text }}>...</div>;
}

반복문 안에서의 Context

반복문 내 use()
typescript
function DynamicFieldList({ fields }: { fields: Field[] }) {
  return (
    <div>
      {fields.map(field => {
        if (field.type === 'themed') {
          const theme = use(ThemeContext);
          return (
            <input
              key={field.id}
              style={{ borderColor: theme.border }}
              placeholder={field.label}
            />
          );
        }
        return <input key={field.id} placeholder={field.label} />;
      })}
    </div>
  );
}

use()와 에러 처리

Error Boundary 활용

use()에서 Promise가 reject되면, 가장 가까운 Error Boundary가 에러를 잡습니다.

Error Boundary로 에러 처리
typescript
function ArticleWithComments({ articleId }: { articleId: string }) {
  const commentsPromise = fetchComments(articleId);
 
  return (
    <div>
      <ErrorBoundary fallback={<p>댓글을 불러오지 못했습니다.</p>}>
        <Suspense fallback={<CommentsSkeleton />}>
          <CommentList commentsPromise={commentsPromise} />
        </Suspense>
      </ErrorBoundary>
    </div>
  );
}

Promise.catch로 폴백 값 제공

Error Boundary 대신 Promise의 catch로 기본값을 제공할 수도 있습니다.

catch로 폴백 값 제공
typescript
async function Page() {
  // Promise가 실패하면 빈 배열 반환
  const commentsPromise = fetchComments(articleId)
    .catch(() => [] as Comment[]);
 
  return (
    <Suspense fallback={<CommentsSkeleton />}>
      <CommentList commentsPromise={commentsPromise} />
    </Suspense>
  );
}
Warning

use()를 try-catch 블록 안에서 사용할 수 없습니다. Promise의 에러 처리는 반드시 Error Boundary 또는 Promise.catch를 통해 해야 합니다.

typescript
// 이 코드는 동작하지 않습니다
function BadExample({ promise }: { promise: Promise<Data> }) {
  try {
    const data = use(promise); // Suspense와 충돌
  } catch (e) {
    return <p>에러</p>;
  }
}

use() vs 기존 패턴 비교

use() vs useEffect + useState

기존 패턴: useEffect + useState
typescript
'use client';
 
function Comments({ articleId }: { articleId: string }) {
  const [comments, setComments] = useState<Comment[]>([]);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState<Error | null>(null);
 
  useEffect(() => {
    let cancelled = false;
    setLoading(true);
 
    fetchComments(articleId)
      .then(data => {
        if (!cancelled) {
          setComments(data);
          setLoading(false);
        }
      })
      .catch(err => {
        if (!cancelled) {
          setError(err);
          setLoading(false);
        }
      });
 
    return () => { cancelled = true; };
  }, [articleId]);
 
  if (loading) return <Spinner />;
  if (error) return <p>에러: {error.message}</p>;
  return <CommentList comments={comments} />;
}
새 패턴: use() + Suspense
typescript
// Server Component
async function ArticlePage({ params }: { params: { id: string } }) {
  const commentsPromise = fetchComments(params.id);
 
  return (
    <ErrorBoundary fallback={<p>댓글을 불러오지 못했습니다.</p>}>
      <Suspense fallback={<Spinner />}>
        <Comments commentsPromise={commentsPromise} />
      </Suspense>
    </ErrorBoundary>
  );
}
 
// Client Component
'use client';
 
function Comments({ commentsPromise }: { commentsPromise: Promise<Comment[]> }) {
  const comments = use(commentsPromise);
  return <CommentList comments={comments} />;
}

새 패턴의 장점은 다음과 같습니다.

관점useEffect + useStateuse() + Suspense
보일러플레이트로딩/에러 상태 수동 관리선언적으로 처리
경쟁 조건수동 취소 로직 필요자동 처리
워터폴클라이언트에서 순차 요청서버에서 병렬 시작
번들데이터 페칭 로직이 클라이언트에 포함서버에서 실행

use() vs useContext

비교: useContext vs use()
typescript
// useContext: 항상 최상위에서 호출
function Component() {
  const theme = useContext(ThemeContext); // 항상 실행
  const auth = useContext(AuthContext);   // 항상 실행
 
  if (!auth.user) return <LoginPrompt />;
  return <Dashboard theme={theme} />;
}
 
// use(): 필요한 시점에 호출
function Component() {
  const auth = use(AuthContext);
 
  if (!auth.user) return <LoginPrompt />;
 
  const theme = use(ThemeContext); // 인증된 경우에만 실행
  return <Dashboard theme={theme} />;
}

실전 활용 패턴

탭 인터페이스에서의 지연 로딩

탭별 데이터 스트리밍
typescript
// Server Component
async function UserPage({ params }: { params: { id: string } }) {
  const user = await getUser(params.id);
 
  // 모든 탭의 데이터를 병렬로 시작
  const postsPromise = getUserPosts(params.id);
  const commentsPromise = getUserComments(params.id);
  const likesPromise = getUserLikes(params.id);
 
  return (
    <div>
      <UserHeader user={user} />
      <UserTabs
        postsPromise={postsPromise}
        commentsPromise={commentsPromise}
        likesPromise={likesPromise}
      />
    </div>
  );
}
클라이언트 탭 컴포넌트
typescript
'use client';
 
import { use, useState, Suspense } from 'react';
 
type Tab = 'posts' | 'comments' | 'likes';
 
function UserTabs({ postsPromise, commentsPromise, likesPromise }: {
  postsPromise: Promise<Post[]>;
  commentsPromise: Promise<Comment[]>;
  likesPromise: Promise<Like[]>;
}) {
  const [activeTab, setActiveTab] = useState<Tab>('posts');
 
  return (
    <div>
      <nav>
        <button onClick={() => setActiveTab('posts')}>글</button>
        <button onClick={() => setActiveTab('comments')}>댓글</button>
        <button onClick={() => setActiveTab('likes')}>좋아요</button>
      </nav>
 
      <Suspense fallback={<TabSkeleton />}>
        {activeTab === 'posts' && <PostsTab promise={postsPromise} />}
        {activeTab === 'comments' && <CommentsTab promise={commentsPromise} />}
        {activeTab === 'likes' && <LikesTab promise={likesPromise} />}
      </Suspense>
    </div>
  );
}
 
function PostsTab({ promise }: { promise: Promise<Post[]> }) {
  const posts = use(promise);
  return (
    <ul>
      {posts.map(post => <li key={post.id}>{post.title}</li>)}
    </ul>
  );
}
 
function CommentsTab({ promise }: { promise: Promise<Comment[]> }) {
  const comments = use(promise);
  return (
    <ul>
      {comments.map(c => <li key={c.id}>{c.text}</li>)}
    </ul>
  );
}
 
function LikesTab({ promise }: { promise: Promise<Like[]> }) {
  const likes = use(promise);
  return (
    <ul>
      {likes.map(l => <li key={l.id}>{l.articleTitle}</li>)}
    </ul>
  );
}

서버에서 세 개의 데이터 요청이 병렬로 시작되므로, 사용자가 어떤 탭을 선택하든 데이터가 이미 로드 중이거나 완료되어 있을 가능성이 높습니다.

조건부 데이터 로딩

권한에 따른 조건부 데이터 소비
typescript
'use client';
 
import { use } from 'react';
 
interface UserDashboardProps {
  user: User;
  analyticsPromise?: Promise<Analytics>;
  adminDataPromise?: Promise<AdminData>;
}
 
function UserDashboard({
  user,
  analyticsPromise,
  adminDataPromise,
}: UserDashboardProps) {
  return (
    <div>
      <h1>{user.name}의 대시보드</h1>
 
      {analyticsPromise && (
        <Suspense fallback={<AnalyticsSkeleton />}>
          <AnalyticsSection promise={analyticsPromise} />
        </Suspense>
      )}
 
      {user.role === 'admin' && adminDataPromise && (
        <Suspense fallback={<AdminSkeleton />}>
          <AdminSection promise={adminDataPromise} />
        </Suspense>
      )}
    </div>
  );
}
 
function AnalyticsSection({ promise }: { promise: Promise<Analytics> }) {
  const analytics = use(promise);
  return <div>방문자: {analytics.visitors}</div>;
}

주의사항과 안티패턴

클라이언트에서 Promise 생성 주의

안티패턴: 렌더링 중 Promise 생성
typescript
'use client';
 
// 나쁜 예: 매 렌더링마다 새 Promise 생성
function BadExample({ articleId }: { articleId: string }) {
  // fetchComments가 매 렌더링마다 호출됨
  const comments = use(fetchComments(articleId));
  return <CommentList comments={comments} />;
}

클라이언트 컴포넌트에서 use()에 전달하는 Promise는 매 렌더링마다 새로 생성되어서는 안 됩니다. 새 Promise가 생성되면 Suspense가 다시 활성화되어 UI가 깜빡입니다. Promise는 Server Component에서 생성하여 props로 전달하는 것이 올바른 패턴입니다.

Server Component에서는 async/await 사용

Server Component에서의 올바른 패턴
typescript
// Server Component에서는 use() 대신 async/await 사용
async function ServerComponent() {
  // 좋은 예: async/await
  const data = await fetchData();
  return <div>{data.title}</div>;
}
 
// use()는 Client Component에서 서버 Promise를 소비할 때 사용

Server Component에서는 async/await를 직접 사용하는 것이 더 직관적이고 명확합니다. use()는 주로 Client Component에서 서버가 전달한 Promise를 소비하는 데 사용합니다.

핵심 요약

  • use()는 Promise와 Context를 렌더링 중에 읽는 새로운 API로, 조건문 안에서도 호출할 수 있습니다.
  • Promise를 use()로 읽으면 Suspense와 통합되어 선언적 로딩 UI를 구현할 수 있습니다.
  • 서버에서 생성한 Promise를 클라이언트로 스트리밍하는 패턴이 핵심 활용법입니다.
  • useContext와 달리 use(Context)는 조건부, 반복문 내에서 호출할 수 있습니다.
  • 에러 처리는 Error Boundary 또는 Promise.catch를 사용합니다 (try-catch 불가).
  • 클라이언트에서 매 렌더링마다 새 Promise를 생성하는 것은 안티패턴입니다.

다음 장에서는 React 19에 새로 추가된 useActionState, useFormStatus, useOptimistic 훅을 심층적으로 다���니다.

이 글이 도움이 되셨나요?

관련 주제 더 보기

#react#nextjs#performance#frontend#typescript

관련 글

웹 개발

5장: 새로운 훅 - useActionState, useFormStatus, useOptimistic

React 19의 새로운 훅 3종을 심층 분석합니다. 폼 상태 관리, 제출 상태 추적, 낙관적 UI 업데이트의 실전 패턴을 다룹니다.

2026년 1월 31일·16분
웹 개발

3장: Server Actions로 서버-클라이언트 통합하기

Server Actions의 동작 원리, 폼 처리 패턴, 데이터 뮤테이션, 에러 핸들링, 보안 고려사항을 실전 코드와 함께 다룹니다.

2026년 1월 27일·22분
웹 개발

6장: Suspense 고급 패턴과 스트리밍 SSR

React 19에서 강화된 Suspense의 고급 패턴, 스트리밍 SSR, 중첩 Suspense 전략, 배칭 동작, Partial Pre-rendering을 다룹니다.

2026년 2월 2일·16분
이전 글3장: Server Actions로 서버-클라이언트 통합하기
다음 글5장: 새로운 훅 - useActionState, useFormStatus, useOptimistic

댓글

목차

약 15분 남음
  • use()란 무엇인가
  • use()로 Promise 읽기
    • Suspense와의 통합
    • 서버-클라이언트 스트리밍
  • use()로 Context 읽기
    • 조건부 Context 소비
    • 반복문 안에서의 Context
  • use()와 에러 처리
    • Error Boundary 활용
    • Promise.catch로 폴백 값 제공
  • use() vs 기존 패턴 비교
    • use() vs useEffect + useState
    • use() vs useContext
  • 실전 활용 패턴
    • 탭 인터페이스에서의 지연 로딩
    • 조건부 데이터 로딩
  • 주의사항과 안티패턴
    • 클라이언트에서 Promise 생성 주의
    • Server Component에서는 async/await 사용
  • 핵심 요약