콘텐츠로 이동

React에서 tRPC로

Nx Plugin for AWS는 tRPC API를 React 웹사이트와 빠르게 통합할 수 있는 생성기를 제공합니다. AWS IAM 및 Cognito 인증 지원과 적절한 오류 처리를 포함하여 tRPC 백엔드에 연결하는 데 필요한 모든 구성을 설정합니다. 이 통합은 프론트엔드와 tRPC 백엔드 간의 완전한 엔드투엔드 타입 안전성을 제공합니다.

이 생성기를 사용하기 전에 React 애플리케이션에 다음이 있어야 합니다:

  1. 애플리케이션을 렌더링하는 main.tsx 파일
  2. tRPC 프로바이더가 자동으로 주입될 <App/> JSX 요소
  3. 작동하는 tRPC API (tRPC API 생성기를 사용하여 생성됨)
  4. Cognito 또는 IAM 인증을 사용하는 API에 연결하는 경우 ts#website#auth 생성기를 통해 추가된 Cognito Auth
필요한 main.tsx 구조 예시
import { StrictMode } from 'react';
import * as ReactDOM from 'react-dom/client';
import App from './app/app';
const root = ReactDOM.createRoot(
document.getElementById('root') as HTMLElement,
);
root.render(
<StrictMode>
<App />
</StrictMode>,
);
Terminal window
pnpm nx g @aws/nx-plugin:connection
어떤 파일이 변경될지 확인하기 위해 드라이 런을 수행할 수도 있습니다
Terminal window
pnpm nx g @aws/nx-plugin:connection --dry-run
매개변수타입기본값설명
sourceProject 필수string-소스 프로젝트
targetProject 필수string-연결할 대상 프로젝트
sourceComponent string-연결을 시작할 소스 컴포넌트 (컴포넌트 이름, 소스 프로젝트 루트 기준 상대 경로, 또는 generator id). 프로젝트를 소스로 명시적으로 선택하려면 '.'을 사용하세요.
targetComponent string-연결할 대상 컴포넌트 (컴포넌트 이름, 대상 프로젝트 루트 기준 상대 경로, 또는 generator id). 프로젝트를 대상으로 명시적으로 선택하려면 '.'을 사용하세요.
preferInstallDependencies booleantrue생성기 실행 후 의존성 설치를 선호할지 여부입니다. 여러 생성기를 일괄 처리할 때 설치를 연기하려면 false로 설정하세요 (후속 생성기가 Nx 프로젝트 그래프를 계산할 수 있도록 필요한 경우 설치는 여전히 실행됩니다); 마지막에 한 번만 설치합니다.

생성기는 React 애플리케이션에 다음 구조를 생성합니다:

  • 디렉터리src
    • 디렉터리components
      • <ApiName>ClientProvider.tsx Sets up the tRPC clients and bindings to your backend schema(s). ApiName will resolve to the name of the API
      • QueryClientProvider.tsx TanStack React Query client provider
    • 디렉터리hooks
      • useSigV4.tsx Hook for signing HTTP requests with SigV4 (IAM only)
      • use<ApiName>.tsx A hook returning the tRPC options proxy for TanStack Query integration
      • use<ApiName>Client.tsx A hook returning the vanilla tRPC client for direct API calls

또한 필요한 종속성을 설치합니다:

  • @trpc/client
  • @trpc/tanstack-react-query
  • @tanstack/react-query
  • aws4fetch (IAM 인증 사용 시)
  • event-source-polyfill (REST API 사용 시, 구독 지원용)

생성기는 useQueryuseMutation과 같은 TanStack Query 훅과 함께 사용할 수 있는 tRPC 옵션 프록시를 반환하는 use<ApiName> 훅을 제공합니다:

import { useQuery, useMutation } from '@tanstack/react-query';
import { useMyApi } from './hooks/useMyApi';
function MyComponent() {
const trpc = useMyApi();
// Example query
const { data, isLoading, error } = useQuery(trpc.users.list.queryOptions());
// Example mutation
const mutation = useMutation(trpc.users.create.mutationOptions());
const handleCreate = () => {
mutation.mutate({
name: 'John Doe',
email: 'john@example.com',
});
};
if (isLoading) return <div>Loading...</div>;
return (
<ul>
{data.map((user) => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
}

use<ApiName>Client 훅은 명령형 API 호출 및 구독에 유용한 vanilla tRPC 클라이언트에 대한 액세스를 제공합니다:

import { useState } from 'react';
import { useMyApiClient } from './hooks/useMyApi';
function MyComponent() {
const client = useMyApiClient();
const handleClick = async () => {
const result = await client.echo.query({ message: 'Hello!' });
console.log(result);
const mutationResult = await client.users.create.mutate({ name: 'Jane' });
console.log(mutationResult);
};
return <button onClick={handleClick}>Call API</button>;
}

통합에는 tRPC 오류를 적절하게 처리하는 내장 오류 처리가 포함되어 있습니다:

function MyComponent() {
const trpc = useMyApi();
const { data, error } = useQuery(trpc.users.list.queryOptions());
if (error) {
return (
<div>
<h2>Error occurred:</h2>
<p>{error.message}</p>
{error.data?.code && <p>Code: {error.data.code}</p>}
</div>
);
}
return (
<ul>
{data.map((user) => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
}

REST API tRPC 백엔드에 연결할 때, 생성된 클라이언트는 구독 작업을 httpSubscriptionLink (SSE 사용)를 통해 라우팅하고 일반 쿼리/뮤테이션을 httpLink를 통해 라우팅하는 splitLink로 자동 구성됩니다. 즉, 추가 구성 없이 구독이 즉시 작동합니다.

백엔드에서 구독 프로시저를 정의하는 방법에 대한 자세한 내용은 tRPC API 생성기 가이드를 참조하세요.

옵션 프록시의 subscriptionOptions와 함께 useSubscription 훅을 사용하여 구독을 사용할 수 있습니다:

import { useSubscription } from '@trpc/tanstack-react-query';
import { useMyApi } from './hooks/useMyApi';
function StreamingComponent() {
const trpc = useMyApi();
const subscription = useSubscription(
trpc.myStream.subscriptionOptions(
{ query: 'hello' },
{
enabled: true,
onStarted: () => {
console.log('Subscription started');
},
onData: (data) => {
console.log('Received:', data.text);
},
onError: (error) => {
console.error('Subscription error:', error);
},
},
),
);
return (
<div>
<p>Status: {subscription.status}</p>
{subscription.data && <p>Latest: {subscription.data.text}</p>}
{subscription.error && <p>Error: {subscription.error.message}</p>}
<button onClick={() => subscription.reset()}>Reset</button>
</div>
);
}

subscription 객체는 다음을 제공합니다:

  • subscription.data — 가장 최근에 수신된 데이터
  • subscription.error — 가장 최근에 수신된 오류
  • subscription.status'idle', 'connecting', 'pending' 또는 'error' 중 하나
  • subscription.reset() — 구독을 재설정합니다 (오류 복구에 유용)

또는 구독 수명 주기를 더 세밀하게 제어하기 위해 use<ApiName>Client 훅을 통해 vanilla tRPC 클라이언트를 사용할 수 있습니다:

import { useState, useEffect } from 'react';
import { useMyApiClient } from './hooks/useMyApi';
function StreamingComponent() {
const client = useMyApiClient();
const [messages, setMessages] = useState<string[]>([]);
useEffect(() => {
const subscription = client.myStream.subscribe(
{ query: 'hello' },
{
onData: (data) => {
setMessages((prev) => [...prev, data.text]);
},
onComplete: () => {
console.log('Stream complete');
},
onError: (error) => {
console.error('Stream error:', error);
},
},
);
// Clean up the subscription on unmount
return () => subscription.unsubscribe();
}, [client]);
return (
<ul>
{messages.map((msg, i) => (
<li key={i}>{msg}</li>
))}
</ul>
);
}

더 나은 사용자 경험을 위해 항상 로딩 및 오류 상태를 처리하세요:

function UserList() {
const trpc = useMyApi();
const users = useQuery(trpc.users.list.queryOptions());
if (users.isLoading) {
return <LoadingSpinner />;
}
if (users.error) {
return <ErrorMessage error={users.error} />;
}
return (
<ul>
{users.data.map((user) => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
}

더 나은 사용자 경험을 위해 낙관적 업데이트를 사용하세요:

import { useQueryClient, useQuery, useMutation } from '@tanstack/react-query';
function UserList() {
const trpc = useMyApi();
const users = useQuery(trpc.users.list.queryOptions());
const queryClient = useQueryClient();
const deleteMutation = useMutation(
trpc.users.delete.mutationOptions({
onMutate: async (userId) => {
// Cancel outgoing fetches
await queryClient.cancelQueries(trpc.users.list.queryFilter());
// Get snapshot of current data
const previousUsers = queryClient.getQueryData(
trpc.users.list.queryKey(),
);
// Optimistically remove the user
queryClient.setQueryData(trpc.users.list.queryKey(), (old) =>
old?.filter((user) => user.id !== userId),
);
return { previousUsers };
},
onError: (err, userId, context) => {
// Restore previous data on error
queryClient.setQueryData(
trpc.users.list.queryKey(),
context?.previousUsers,
);
},
}),
);
return (
<ul>
{users.map((user) => (
<li key={user.id}>
{user.name}
<button onClick={() => deleteMutation.mutate(user.id)}>Delete</button>
</li>
))}
</ul>
);
}

더 나은 성능을 위해 데이터를 프리페치하세요:

function UserList() {
const trpc = useMyApi();
const users = useQuery(trpc.users.list.queryOptions());
const queryClient = useQueryClient();
// Prefetch user details on hover
const prefetchUser = async (userId: string) => {
await queryClient.prefetchQuery(trpc.users.getById.queryOptions(userId));
};
return (
<ul>
{users.map((user) => (
<li key={user.id} onMouseEnter={() => prefetchUser(user.id)}>
<Link to={`/users/${user.id}`}>{user.name}</Link>
</li>
))}
</ul>
);
}

무한 쿼리로 페이지네이션을 처리하세요:

function UserList() {
const trpc = useMyApi();
const { data, fetchNextPage, hasNextPage, isFetchingNextPage } =
useInfiniteQuery(
trpc.users.list.infiniteQueryOptions(
{ limit: 10 },
{
getNextPageParam: (lastPage) => lastPage.nextCursor,
},
),
);
return (
<div>
{data?.pages.map((page) =>
page.users.map((user) => <UserCard key={user.id} user={user} />),
)}
{hasNextPage && (
<button onClick={() => fetchNextPage()} disabled={isFetchingNextPage}>
{isFetchingNextPage ? 'Loading...' : 'Load More'}
</button>
)}
</div>
);
}

무한 쿼리는 cursor라는 이름의 입력 속성이 있는 프로시저에만 사용할 수 있다는 점에 유의해야 합니다.

통합은 완전한 엔드투엔드 타입 안전성을 제공합니다. IDE는 모든 API 호출에 대해 완전한 자동 완성 및 타입 검사를 제공합니다:

function UserForm() {
const trpc = useMyApi();
// ✅ Input is fully typed
const createUser = trpc.users.create.useMutation();
const handleSubmit = (data: CreateUserInput) => {
// ✅ Type error if input doesn't match schema
createUser.mutate(data);
};
return <form onSubmit={handleSubmit}>{/* ... */}</form>;
}

타입은 백엔드의 라우터 및 스키마 정의에서 자동으로 추론되므로 API의 변경 사항이 빌드 없이 즉시 프론트엔드 코드에 반영됩니다.

tRPC API가 Custom 인증(Lambda Authorizer)을 사용하는 경우, 생성된 클라이언트 프로바이더에는 인증자가 예상하는 인증 헤더를 추가해야 하는 플레이스홀더 headers가 포함됩니다. 생성된 <ApiName>ClientProvider.tsx에서 // TODO: Add headers required by your custom authorizer 주석을 찾아 토큰 또는 API 키 로직으로 교체하세요.

자세한 내용은 다음을 참조하세요: