콘텐츠로 이동

React에서 tRPC 사용하기

Nx용 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#react-website-auth 제너레이터를 통해 추가된 Cognito 인증
필요한 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>,
);
  1. 설치 Nx Console VSCode Plugin 아직 설치하지 않았다면
  2. VSCode에서 Nx 콘솔 열기
  3. 클릭 Generate (UI) "Common Nx Commands" 섹션에서
  4. 검색 @aws/nx-plugin - connection
  5. 필수 매개변수 입력
    • 클릭 Generate
    매개변수 타입 기본값 설명
    sourceProject 필수 string - The source project
    targetProject 필수 string - The target project to connect to
    sourceComponent string - The source component to connect from (component name, path relative to source project root, or generator id). Use '.' to explicitly select the project as the source.
    targetComponent string - The target component to connect to (component name, path relative to target project root, or generator id). Use '.' to explicitly select the project as the target.

    제너레이터는 React 애플리케이션에 다음 구조를 생성합니다:

    • 디렉터리src
      • 디렉터리components
        • <ApiName>ClientProvider.tsx tRPC 클라이언트 설정 및 백엔드 스키마 바인딩. ApiName은 API 이름으로 대체됨
        • QueryClientProvider.tsx TanStack React Query 클라이언트 프로바이더
      • 디렉터리hooks
        • useSigV4.tsx SigV4를 사용한 HTTP 요청 서명 훅 (IAM 전용)
        • use<ApiName>.tsx TanStack Query 통합을 위한 tRPC 옵션 프록시를 반환하는 훅
        • use<ApiName>Client.tsx 직접 API 호출을 위한 바닐라 tRPC 클라이언트를 반환하는 훅

    추가적으로 다음 의존성을 설치합니다:

    • @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();
    // 쿼리 예시
    const { data, isLoading, error } = useQuery(trpc.users.list.queryOptions());
    // 뮤테이션 예시
    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 호출과 구독에 유용한 바닐라 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>오류 발생:</h2>
    <p>{error.message}</p>
    {error.data?.code && <p>코드: {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로 자동 구성됩니다. 즉, 추가 구성 없이도 구독이 즉시 작동합니다.

    백엔드에서 구독 프로시저를 정의하는 방법에 대한 정보는 ts#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 훅을 통해 바닐라 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);
    },
    },
    );
    // 언마운트 시 구독 정리
    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) => {
    // 진행 중인 요청 취소
    await queryClient.cancelQueries(trpc.users.list.queryFilter());
    // 현재 데이터 스냅샷 저장
    const previousUsers = queryClient.getQueryData(
    trpc.users.list.queryKey(),
    );
    // 낙관적 사용자 삭제
    queryClient.setQueryData(trpc.users.list.queryKey(), (old) =>
    old?.filter((user) => user.id !== userId),
    );
    return { previousUsers };
    },
    onError: (err, userId, context) => {
    // 오류 시 이전 데이터 복원
    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)}>삭제</button>
    </li>
    ))}
    </ul>
    );
    }

    성능 향상을 위해 데이터를 미리 가져오세요:

    function UserList() {
    const trpc = useMyApi();
    const users = useQuery(trpc.users.list.queryOptions());
    const queryClient = useQueryClient();
    // 호버 시 사용자 상세 정보 프리페치
    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 ? '로딩 중...' : '더 보기'}
    </button>
    )}
    </div>
    );
    }

    무한 쿼리는 cursor라는 입력 속성을 가진 프로시저에만 사용할 수 있습니다.

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

    function UserForm() {
    const trpc = useMyApi();
    // ✅ 입력이 완전한 타입 안전성 보장
    const createUser = trpc.users.create.useMutation();
    const handleSubmit = (data: CreateUserInput) => {
    // ✅ 스키마와 불일치 시 타입 오류 발생
    createUser.mutate(data);
    };
    return <form onSubmit={handleSubmit}>{/* ... */}</form>;
    }

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

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