콘텐츠로 이동

React to Smithy API

connection 생성기는 React 웹사이트를 Smithy TypeScript API 백엔드와 빠르게 통합할 수 있는 방법을 제공합니다. 클라이언트 및 TanStack Query 훅 생성, AWS IAM 및 Cognito 인증 지원, 적절한 오류 처리를 포함하여 타입 안전한 방식으로 Smithy API에 연결하는 데 필요한 모든 구성을 설정합니다.

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

  1. 애플리케이션을 렌더링하는 main.tsx 파일
  2. 작동하는 Smithy TypeScript API 백엔드 (ts#api 생성기--framework=smithy와 함께 사용하여 생성)
  3. 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>,
);

이 제너레이터 실행@aws/nx-plugin:connection

pnpm nx g @aws/nx-plugin:connection
명령 구성하기5

필수

필수

제너레이터 옵션5 옵션
sourceProject필수string

소스 프로젝트

targetProject필수string

연결할 대상 프로젝트

sourceComponentstring

연결할 소스 컴포넌트 (컴포넌트 이름, 소스 프로젝트 루트 기준 상대 경로, 또는 generator id). 프로젝트를 소스로 명시적으로 선택하려면 '.'을 사용하세요.

targetComponentstring

연결할 대상 컴포넌트 (컴포넌트 이름, 대상 프로젝트 루트 기준 상대 경로, 또는 generator id). 프로젝트를 대상으로 명시적으로 선택하려면 '.'을 사용하세요.

preferInstallDependenciesboolean기본값: true

생성기 실행 후 의존성 설치를 선호할지 여부입니다. 여러 생성기를 일괄 처리할 때 설치를 연기하려면 false로 설정하세요 (후속 생성기가 Nx 프로젝트 그래프를 계산할 수 있도록 필요한 경우 설치는 여전히 실행됩니다); 마지막에 한 번만 설치합니다.

생성기는 React 애플리케이션의 다음 파일을 변경합니다:

  • 디렉터리src
    • 디렉터리components
      • <ApiName>Provider.tsx API 클라이언트용 Provider
      • QueryClientProvider.tsx TanStack React Query 클라이언트 provider
      • 디렉터리RuntimeConfig/ 로컬 개발을 위한 런타임 구성 컴포넌트
    • 디렉터리hooks
      • use<ApiName>.tsx TanStack Query로 상태가 관리되는 API 호출을 위한 훅 추가
      • use<ApiName>Client.tsx API를 호출할 수 있는 바닐라 API 클라이언트를 인스턴스화하는 훅 추가
      • useSigV4.tsx SigV4로 HTTP 요청에 서명하는 훅 추가 (IAM 인증을 선택한 경우)
  • project.json 타입 안전 클라이언트를 생성하는 새 타겟이 빌드에 추가됨
  • .gitignore 생성된 클라이언트 파일은 기본적으로 무시됨

생성기는 또한 Smithy 모델에 파일을 추가합니다:

  • 디렉터리model
    • 디렉터리src
      • extensions.smithy 생성된 클라이언트를 커스터마이즈하는 데 사용할 수 있는 trait 정의

생성기는 또한 아직 없는 경우 웹사이트 인프라에 Runtime Config를 추가하여 Smithy API의 API URL이 웹사이트에서 사용 가능하고 use<ApiName>.tsx 훅에 의해 자동으로 구성되도록 합니다.

빌드 시 Smithy API의 OpenAPI 사양에서 타입 안전 클라이언트가 생성됩니다. 이렇게 하면 React 애플리케이션에 세 개의 새 파일이 추가됩니다:

  • 디렉터리src
    • 디렉터리generated
      • 디렉터리<ApiName>
        • types.gen.ts Smithy 모델 구조에서 생성된 타입
        • client.gen.ts API 호출을 위한 타입 안전 클라이언트
        • options-proxy.gen.ts TanStack Query를 사용하여 API와 상호작용하기 위한 TanStack Query 훅 옵션을 생성하는 메서드 제공

생성된 타입 안전 클라이언트를 사용하여 React 애플리케이션에서 Smithy API를 호출할 수 있습니다. TanStack Query 훅을 통해 클라이언트를 사용하는 것이 권장되지만, 원하는 경우 바닐라 클라이언트를 사용할 수도 있습니다.

생성기는 TanStack Query로 API를 호출하는 데 사용할 수 있는 use<ApiName> 훅을 제공합니다.

queryOptions 메서드를 사용하여 TanStack Query의 useQuery 훅을 사용하여 API를 호출하는 데 필요한 옵션을 검색할 수 있습니다:

import { useQuery } from '@tanstack/react-query';
import { useState, useEffect } from 'react';
import { useMyApi } from './hooks/useMyApi';
function MyComponent() {
const api = useMyApi();
const item = useQuery(api.getItem.queryOptions({ itemId: 'some-id' }));
if (item.isPending) return <div>Loading...</div>;
if (item.isError) return <div>Error: {JSON.stringify(item.error.error)}</div>;
return <div>Item: {item.data.name}</div>;
}

오류는 Error가 아니라 { status, error } 객체의 판별된 유니온이므로 렌더링할 최상위 message가 없습니다. 특정 응답 본문을 읽으려면 status로 좁히세요 — 아래 오류 처리를 참조하세요.

바닐라 클라이언트를 직접 사용하는 예시를 보려면 여기를 클릭하세요.

생성된 훅에는 TanStack Query의 useMutation 훅을 사용한 뮤테이션 지원이 포함되어 있습니다. 이를 통해 로딩 상태, 오류 처리 및 낙관적 업데이트를 사용하여 생성, 업데이트 및 삭제 작업을 깔끔하게 처리할 수 있습니다.

import { useMutation } from '@tanstack/react-query';
import { useMyApi } from './hooks/useMyApi';
function CreateItemForm() {
const api = useMyApi();
// Create a mutation using the generated mutation options
const createItem = useMutation(api.createItem.mutationOptions());
const handleSubmit = (e) => {
e.preventDefault();
createItem.mutate({ name: 'New Item', description: 'A new item' });
};
return (
<form onSubmit={handleSubmit}>
{/* Form fields */}
<button
type="submit"
disabled={createItem.isPending}
>
{createItem.isPending ? 'Creating...' : 'Create Item'}
</button>
{createItem.isSuccess && (
<div className="success">
Item created with ID: {createItem.data.id}
</div>
)}
{createItem.isError && (
<div className="error">
Error: {JSON.stringify(createItem.error.error)}
</div>
)}
</form>
);
}

다양한 뮤테이션 상태에 대한 콜백을 추가할 수도 있습니다:

const createItem = useMutation({
...api.createItem.mutationOptions(),
onSuccess: (data) => {
// This will run when the mutation succeeds
console.log('Item created:', data);
// You can navigate to the new item
navigate(`/items/${data.id}`);
},
onError: (error) => {
// This will run when the mutation fails
console.error('Failed to create item:', error);
},
onSettled: () => {
// This will run when the mutation completes (success or error)
// Good place to invalidate queries that might be affected
queryClient.invalidateQueries({ queryKey: api.listItems.queryKey({}) });
}
});
클라이언트를 직접 사용하는 예시를 보려면 여기를 클릭하세요.

무한 쿼리를 사용한 페이지네이션

섹션 제목: “무한 쿼리를 사용한 페이지네이션”

입력으로 cursor 매개변수를 받는 엔드포인트의 경우, 생성된 훅은 TanStack Query의 useInfiniteQuery 훅을 사용한 무한 쿼리를 지원합니다. 이를 통해 “더 보기” 또는 무한 스크롤 기능을 쉽게 구현할 수 있습니다.

import { useInfiniteQuery } from '@tanstack/react-query';
import { useMyApi } from './hooks/useMyApi';
function ItemList() {
const api = useMyApi();
const items = useInfiniteQuery({
...api.listItems.infiniteQueryOptions({
limit: 10, // Number of items per page
}, {
// Make sure you define a getNextPageParam function to return
// the parameter that should be passed as the 'cursor' for the
// next page
getNextPageParam: (lastPage) =>
lastPage.nextCursor || undefined
}),
});
if (items.isPending) {
return <LoadingSpinner />;
}
if (items.isError) {
return <ErrorMessage message={JSON.stringify(items.error.error)} />;
}
return (
<div>
{/* Flatten the pages array to render all items */}
<ul>
{items.data.pages.flatMap(page =>
page.items.map(item => (
<li key={item.id}>{item.name}</li>
))
)}
</ul>
<button
onClick={() => items.fetchNextPage()}
disabled={!items.hasNextPage || items.isFetchingNextPage}
>
{items.isFetchingNextPage
? 'Loading more...'
: items.hasNextPage
? 'Load More'
: 'No more items'}
</button>
</div>
);
}

생성된 훅은 API가 지원하는 경우 커서 기반 페이지네이션을 자동으로 처리합니다. nextCursor 값은 응답에서 추출되어 다음 페이지를 가져오는 데 사용됩니다.

클라이언트를 직접 사용하는 예제를 보려면 여기를 클릭하세요.

통합에는 타입이 지정된 오류 응답과 함께 내장 오류 처리가 포함되어 있습니다. <OperationName>Error 타입이 생성되며, 이는 작업이 모델링하는 각 오류 상태에 대해 하나의 <OperationName><StatusCode>Error 멤버로 구성된 유니온입니다. 각 멤버에는 statuserror 속성이 있으므로 status를 전환하면 error가 해당 상태의 페이로드로 좁혀집니다.

아래 예시는 CreateItem이 페이지 아래에 정의된 오류 구조를 모델링한다고 가정합니다 — 422 InvalidRequestError, 403 UnauthorizedError500 InternalServerError. 모델이 선언한 상태만 유니온에 나타나므로 모델링하지 않은 상태에 대한 case는 컴파일 오류입니다.

import { useMutation } from '@tanstack/react-query';
function MyComponent() {
const api = useMyApi();
const createItem = useMutation(api.createItem.mutationOptions());
const handleClick = () => {
createItem.mutate({ name: 'New Item' });
};
if (createItem.error) {
switch (createItem.error.status) {
case 422:
// error.error is typed as InvalidRequestErrorResponseContent
return (
<div>
<h2>Invalid input:</h2>
<p>{createItem.error.error.message}</p>
</div>
);
case 403:
// error.error is typed as UnauthorizedErrorResponseContent
return (
<div>
<h2>Not authorized:</h2>
<p>{createItem.error.error.reason}</p>
</div>
);
case 500:
// error.error is typed as InternalServerErrorResponseContent
return (
<div>
<h2>Server error:</h2>
<p>{createItem.error.error.message}</p>
</div>
);
}
}
return <button onClick={handleClick}>Create Item</button>;
}
바닐라 클라이언트를 직접 사용하는 예제를 보려면 여기를 클릭하세요.

생성된 클라이언트를 커스터마이즈하는 데 사용할 수 있는 Smithy trait 선택 항목이 extensions.smithy의 대상 Smithy model 프로젝트에 추가됩니다.

기본적으로 HTTP 메서드 PUT, POST, PATCHDELETE를 사용하는 Smithy API의 작업은 뮤테이션으로 간주되고, 다른 모든 작업은 쿼리로 간주됩니다.

extensions.smithy의 모델 프로젝트에 추가된 @query@mutation Smithy trait를 사용하여 이 동작을 변경할 수 있습니다.

Smithy 작업에 @query trait를 적용하여 쿼리로 처리되도록 강제합니다:

@http(method: "POST", uri: "/search-items")
@query
operation SearchItems {
input: SearchItemsInput
output: SearchItemsOutput
}

생성된 훅은 POST HTTP 메서드를 사용하더라도 queryOptions를 제공합니다:

const items = useQuery(api.searchItems.queryOptions({ term: 'widget' }));

Smithy 작업에 @mutation trait를 적용하여 뮤테이션으로 처리되도록 강제합니다:

@http(method: "GET", uri: "/start-processing")
@mutation
operation StartProcessing {
input: StartProcessingInput
output: StartProcessingOutput
}

생성된 훅은 GET HTTP 메서드를 사용하더라도 mutationOptions를 제공합니다:

const startProcessing = useMutation(api.startProcessing.mutationOptions());
startProcessing.mutate({ id: 'some-id' });

기본적으로 생성된 훅은 cursor라는 이름의 매개변수를 사용한 커서 기반 페이지네이션을 가정합니다. extensions.smithy의 모델 프로젝트에 추가된 @cursor trait를 사용하여 이 동작을 커스터마이즈할 수 있습니다.

페이지네이션 토큰에 사용되는 입력 매개변수의 이름을 변경하려면 inputToken과 함께 @cursor trait를 적용합니다:

@http(method: "GET", uri: "/paged-items")
@cursor(inputToken: "nextToken")
operation ListPagedItems {
input := {
@httpQuery("nextToken")
nextToken: String
@httpQuery("limit")
limit: Integer
}
output := {
@required
items: ItemList
nextToken: String
}
}

infiniteQueryOptions는 그런 다음 nextToken에서 페이지를 매깁니다:

const items = useInfiniteQuery({
...api.listPagedItems.infiniteQueryOptions(
{ limit: 10 },
{ getNextPageParam: (lastPage) => lastPage.nextToken || undefined },
),
});

cursor라는 이름의 입력 매개변수가 있는 작업에 대해 infiniteQueryOptions를 생성하지 않으려면 커서 기반 페이지네이션을 비활성화할 수 있습니다:

@http(method: "GET", uri: "/unpaged-items")
@cursor(enabled: false)
operation ListUnpagedItems {
input := {
// Input parameter named 'cursor' will cause this operation to be treated as a paginated operation by default
@httpQuery("cursor")
cursor: String
}
output := {
@required
items: ItemList
}
}

api.listUnpagedItems는 그런 다음 queryOptions, queryKeyqueryFilter만 제공합니다.

생성된 훅과 클라이언트 메서드는 Smithy 작업의 @tags trait를 기반으로 자동으로 구성됩니다. 동일한 태그를 가진 작업은 함께 그룹화되어 API 호출을 체계적으로 유지하고 IDE에서 더 나은 코드 완성을 제공합니다.

그룹 내에서 각 작업은 자체 camelCase 이름을 유지하므로 "items" 태그가 지정된 ListItems라는 작업은 api.items.listItems에서 도달합니다 — api.items.list가 아닙니다.

예를 들어, 다음 Smithy 모델의 경우:

service MyService {
operations: [ListItems, CreateItem, ListUsers, CreateUser]
}
@tags(["items"])
@http(method: "GET", uri: "/items")
@readonly
operation ListItems {
input := {}
output := {
@required
items: ItemList
}
}
@tags(["items"])
@http(method: "POST", uri: "/items")
operation CreateItem {
input := {
@required
name: String
}
output := {
@required
id: String
}
}
@tags(["users"])
@http(method: "GET", uri: "/users")
@readonly
operation ListUsers {
input := {}
output := {
@required
users: UserList
}
}
@tags(["users"])
@http(method: "POST", uri: "/users")
operation CreateUser {
input := {
@required
name: String
}
output := {
@required
id: String
}
}

생성된 훅은 태그별로 그룹화됩니다:

import { useQuery, useMutation } from '@tanstack/react-query';
import { useMyApi } from './hooks/useMyApi';
function ItemsAndUsers() {
const api = useMyApi();
// Items operations are grouped under api.items
const items = useQuery(api.items.listItems.queryOptions());
const createItem = useMutation(api.items.createItem.mutationOptions());
// Users operations are grouped under api.users
const users = useQuery(api.users.listUsers.queryOptions());
// Usage example
const handleCreateItem = () => {
createItem.mutate({ name: 'New Item' });
};
return (
<div>
<h2>Items</h2>
<ul>
{items.data?.items.map(item => (
<li key={item.id}>{item.name}</li>
))}
</ul>
<button onClick={handleCreateItem}>Add Item</button>
<h2>Users</h2>
<ul>
{users.data?.users.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
</div>
);
}

이 그룹화를 통해 API 호출을 더 쉽게 구성하고 IDE에서 더 나은 코드 완성을 제공합니다.

클라이언트를 직접 사용하는 예제를 보려면 여기를 클릭하세요.

Smithy 모델에서 커스텀 오류 구조를 정의하여 Smithy API의 오류 응답을 커스터마이즈할 수 있습니다. 생성된 클라이언트는 이러한 커스텀 오류 타입을 자동으로 처리합니다.

Smithy 모델에서 오류 구조를 정의합니다:

@error("client")
@httpError(422)
structure InvalidRequestError {
@required
message: String
fieldErrors: FieldErrorList
}
@error("client")
@httpError(403)
structure UnauthorizedError {
@required
reason: String
}
@error("server")
@httpError(500)
structure InternalServerError {
@required
message: String
traceId: String
}
list FieldErrorList {
member: FieldError
}
structure FieldError {
@required
field: String
@required
message: String
}

작업이 반환할 수 있는 오류를 지정합니다:

operation CreateItem {
input: CreateItemInput
output: CreateItemOutput
errors: [
InvalidRequestError
UnauthorizedError
InternalServerError
]
}
operation GetItem {
input: GetItemInput
output: GetItemOutput
errors: [
ItemNotFoundError
InternalServerError
]
}
@error("client")
@httpError(404)
structure ItemNotFoundError {
@required
message: String
}

React에서 커스텀 오류 타입 사용

섹션 제목: “React에서 커스텀 오류 타입 사용”

생성된 클라이언트는 이러한 커스텀 오류 타입을 자동으로 처리하여 다양한 오류 응답을 타입 체크하고 처리할 수 있습니다:

import { useMutation, useQuery } from '@tanstack/react-query';
function ItemComponent() {
const api = useMyApi();
const getItem = useQuery(api.getItem.queryOptions({ itemId: '123' }));
// Mutation with typed error handling
const createItem = useMutation({
...api.createItem.mutationOptions(),
onError: (error) => {
// Error is typed based on the errors in your Smithy model
switch (error.status) {
case 422:
// error.error is typed as InvalidRequestErrorResponseContent
console.error('Validation error:', error.error.message);
console.error('Field errors:', error.error.fieldErrors);
break;
case 403:
// error.error is typed as UnauthorizedErrorResponseContent
console.error('Unauthorized:', error.error.reason);
break;
}
}
});
// Component rendering with error handling
if (getItem.isError) {
switch (getItem.error.status) {
case 404:
// error.error is typed as ItemNotFoundErrorResponseContent
return <NotFoundMessage message={getItem.error.error.message} />;
case 500:
// error.error is typed as InternalServerErrorResponseContent
return <ErrorMessage message={getItem.error.error.message} />;
}
}
return (
<div>
{/* Component content */}
</div>
);
}
클라이언트를 직접 사용하는 예제를 보려면 여기를 클릭하세요.

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

import { useQuery } from '@tanstack/react-query';
function ItemList() {
const api = useMyApi();
const items = useQuery(api.listItems.queryOptions({}));
if (items.isLoading) {
return <LoadingSpinner />;
}
if (items.isError) {
const err = items.error;
switch (err.status) {
case 403:
// err.error is typed as UnauthorizedErrorResponseContent
return <ErrorMessage message={err.error.reason} />;
case 500:
// err.error is typed as InternalServerErrorResponseContent
return (
<ErrorMessage
message={err.error.message}
/>
);
default:
return <ErrorMessage message="An unknown error occurred" />;
}
}
return (
<ul>
{items.data?.items.map((item) => (
<li key={item.id}>{item.name}</li>
))}
</ul>
);
}
바닐라 클라이언트를 직접 사용하는 예제를 보려면 여기를 클릭하세요.

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

import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import type { ListItemsResponseContent } from '../generated/my-api/types.gen';
function ItemList() {
const api = useMyApi();
const queryClient = useQueryClient();
// Query to fetch items
const itemsQuery = useQuery(api.listItems.queryOptions({}));
// Mutation for deleting items with optimistic updates
const deleteMutation = useMutation({
...api.deleteItem.mutationOptions(),
onMutate: async ({ itemId }) => {
// Cancel any outgoing refetches
await queryClient.cancelQueries({
queryKey: api.listItems.queryKey({}),
});
// Snapshot the previous value
const previousItems = queryClient.getQueryData<ListItemsResponseContent>(
api.listItems.queryKey({}),
);
// Optimistically update to the new value
queryClient.setQueryData<ListItemsResponseContent>(
api.listItems.queryKey({}),
(old) =>
old && {
...old,
items: old.items.filter((item) => item.id !== itemId),
},
);
// Return a context object with the snapshot
return { previousItems };
},
onError: (err, _input, context) => {
// If the mutation fails, use the context returned from onMutate to roll back
queryClient.setQueryData(
api.listItems.queryKey({}),
context?.previousItems,
);
console.error('Failed to delete item:', err);
},
onSettled: () => {
// Always refetch after error or success to ensure data is in sync with server
queryClient.invalidateQueries({ queryKey: api.listItems.queryKey({}) });
},
});
if (itemsQuery.isLoading) {
return <LoadingSpinner />;
}
if (itemsQuery.isError) {
return <ErrorMessage message="Failed to load items" />;
}
return (
<ul>
{itemsQuery.data?.items.map((item) => (
<li key={item.id}>
{item.name}
<button
onClick={() => deleteMutation.mutate({ itemId: item.id })}
disabled={deleteMutation.isPending}
>
{deleteMutation.isPending ? 'Deleting...' : 'Delete'}
</button>
</li>
))}
</ul>
);
}
바닐라 클라이언트를 직접 사용하는 예제를 보려면 여기를 클릭하세요.

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

import { useMutation } from '@tanstack/react-query';
function ItemForm() {
const api = useMyApi();
// Type-safe mutation for creating items
const createItem = useMutation({
...api.createItem.mutationOptions(),
// ✅ Type error if onSuccess callback doesn't handle the correct response type
onSuccess: (data) => {
// data is fully typed based on your API's response schema
console.log(`Item created with ID: ${data.id}`);
},
});
const handleSubmit = (data: CreateItemInput) => {
// ✅ Type error if input doesn't match schema
createItem.mutate(data);
};
// Error UI can use type narrowing to handle different error types
if (createItem.error) {
const error = createItem.error;
switch (error.status) {
case 422:
// error.error is typed as InvalidRequestErrorResponseContent
return (
<FormError
message="Invalid input"
errors={error.error.fieldErrors}
/>
);
case 403:
// error.error is typed as UnauthorizedErrorResponseContent
return <AuthError reason={error.error.reason} />;
default:
// error.error is typed as InternalServerErrorResponseContent for 500, etc.
return <ServerError message={error.error.message} />;
}
}
return (
<form onSubmit={(e) => {
e.preventDefault();
handleSubmit({ name: 'New Item' });
}}>
{/* Form fields */}
<button
type="submit"
disabled={createItem.isPending}
>
{createItem.isPending ? 'Creating...' : 'Create Item'}
</button>
</form>
);
}
바닐라 클라이언트를 직접 사용하는 예제를 보려면 여기를 클릭하세요.

타입은 Smithy API의 OpenAPI 스키마에서 자동으로 생성되므로 API에 대한 모든 변경 사항이 빌드 후 프론트엔드 코드에 반영됩니다.

Smithy API가 Custom 인증(Lambda Authorizer)을 사용하는 경우, 생성된 클라이언트 provider를 편집하여 authorizer가 예상하는 인증 헤더를 추가해야 합니다. 생성된 <ApiName>Provider.tsx에서 fetch 구성을 찾아 요청 헤더에 토큰 또는 API 키를 추가하세요.