React to Smithy API
connection 생성기는 React 웹사이트를 Smithy TypeScript API 백엔드와 빠르게 통합할 수 있는 방법을 제공합니다. 클라이언트 및 TanStack Query 훅 생성, AWS IAM 및 Cognito 인증 지원, 적절한 오류 처리를 포함하여 타입 안전한 방식으로 Smithy API에 연결하는 데 필요한 모든 구성을 설정합니다.
전제 조건
섹션 제목: “전제 조건”이 생성기를 사용하기 전에 React 애플리케이션에 다음이 있어야 합니다:
- 애플리케이션을 렌더링하는
main.tsx파일 - 작동하는 Smithy TypeScript API 백엔드 (
ts#api생성기를--framework=smithy와 함께 사용하여 생성) - 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>,);사용법
섹션 제목: “사용법”생성기 실행
섹션 제목: “생성기 실행”pnpm nx g @aws/nx-plugin:connectionyarn nx g @aws/nx-plugin:connectionnpx nx g @aws/nx-plugin:connectionbunx nx g @aws/nx-plugin:connection- 설치 Nx Console VSCode Plugin 아직 설치하지 않았다면
- VSCode에서 Nx 콘솔 열기
- 클릭
Generate (UI)"Common Nx Commands" 섹션에서 - 검색
@aws/nx-plugin - connection - 필수 매개변수 입력
- 클릭
Generate
| 매개변수 | 타입 | 기본값 | 설명 |
|---|---|---|---|
| sourceProject 필수 | string | - | 소스 프로젝트 |
| targetProject 필수 | string | - | 연결할 대상 프로젝트 |
| sourceComponent | string | - | 연결을 시작할 소스 컴포넌트 (컴포넌트 이름, 소스 프로젝트 루트 기준 상대 경로, 또는 generator id). 프로젝트를 소스로 명시적으로 선택하려면 '.'을 사용하세요. |
| targetComponent | string | - | 연결할 대상 컴포넌트 (컴포넌트 이름, 대상 프로젝트 루트 기준 상대 경로, 또는 generator id). 프로젝트를 대상으로 명시적으로 선택하려면 '.'을 사용하세요. |
| preferInstallDependencies | boolean | 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 훅을 통해 클라이언트를 사용하는 것이 권장되지만, 원하는 경우 바닐라 클라이언트를 사용할 수도 있습니다.
API 훅 사용
섹션 제목: “API 훅 사용”생성기는 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.isLoading) return <div>Loading...</div>; if (item.isError) return <div>Error: {item.error.message}</div>;
return <div>Item: {item.data.name}</div>;}API 클라이언트를 직접 사용
import { useState, useEffect } from 'react';import { useMyApiClient } from './hooks/useMyApiClient';
function MyComponent() { const api = useMyApiClient(); const [item, setItem] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null);
useEffect(() => { const fetchItem = async () => { try { const data = await api.getItem({ itemId: 'some-id' }); setItem(data); } catch (err) { setError(err); } finally { setLoading(false); } }; fetchItem(); }, [api]);
if (loading) return <div>Loading...</div>; if (error) return <div>Error: {error.message}</div>;
return <div>Item: {item.name}</div>;}뮤테이션
섹션 제목: “뮤테이션”생성된 훅에는 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: {createItem.error.message} </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() }); }});API 클라이언트를 직접 사용한 뮤테이션
import { useState } from 'react';import { useMyApiClient } from './hooks/useMyApiClient';
function CreateItemForm() { const api = useMyApiClient(); const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState(null); const [createdItem, setCreatedItem] = useState(null);
const handleSubmit = async (e) => { e.preventDefault(); setIsLoading(true); setError(null);
try { const newItem = await api.createItem({ name: 'New Item', description: 'A new item' }); setCreatedItem(newItem); // You can navigate to the new item // navigate(`/items/${newItem.id}`); } catch (err) { setError(err); console.error('Failed to create item:', err); } finally { setIsLoading(false); } };
return ( <form onSubmit={handleSubmit}> {/* Form fields */} <button type="submit" disabled={isLoading} > {isLoading ? 'Creating...' : 'Create Item'} </button>
{createdItem && ( <div className="success"> Item created with ID: {createdItem.id} </div> )}
{error && ( <div className="error"> Error: {error.message} </div> )} </form> );}무한 쿼리를 사용한 페이지네이션
섹션 제목: “무한 쿼리를 사용한 페이지네이션”입력으로 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.isLoading) { return <LoadingSpinner />; }
if (items.isError) { return <ErrorMessage message={items.error.message} />; }
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 값은 응답에서 추출되어 다음 페이지를 가져오는 데 사용됩니다.
API 클라이언트를 직접 사용한 페이지네이션
import { useState, useEffect } from 'react';import { useMyApiClient } from './hooks/useMyApiClient';
function ItemList() { const api = useMyApiClient(); const [items, setItems] = useState([]); const [isLoading, setIsLoading] = useState(true); const [error, setError] = useState(null); const [nextCursor, setNextCursor] = useState(null); const [isFetchingMore, setIsFetchingMore] = useState(false);
// Fetch initial data useEffect(() => { const fetchItems = async () => { try { setIsLoading(true); const response = await api.listItems({ limit: 10 }); setItems(response.items); setNextCursor(response.nextCursor); } catch (err) { setError(err); } finally { setIsLoading(false); } };
fetchItems(); }, [api]);
// Function to load more items const loadMore = async () => { if (!nextCursor) return;
try { setIsFetchingMore(true); const response = await api.listItems({ limit: 10, cursor: nextCursor });
setItems(prevItems => [...prevItems, ...response.items]); setNextCursor(response.nextCursor); } catch (err) { setError(err); } finally { setIsFetchingMore(false); } };
if (isLoading) { return <LoadingSpinner />; }
if (error) { return <ErrorMessage message={error.message} />; }
return ( <div> <ul> {items.map(item => ( <li key={item.id}>{item.name}</li> ))} </ul>
<button onClick={loadMore} disabled={!nextCursor || isFetchingMore} > {isFetchingMore ? 'Loading more...' : nextCursor ? 'Load More' : 'No more items'} </button> </div> );}오류 처리
섹션 제목: “오류 처리”통합에는 타입이 지정된 오류 응답과 함께 내장 오류 처리가 포함되어 있습니다. Smithy 모델에 정의된 가능한 오류 응답을 캡슐화하는 <operation-name>Error 타입이 생성됩니다. 각 오류에는 status 및 error 속성이 있으며, status 값을 확인하여 특정 오류 타입으로 좁힐 수 있습니다.
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 400: // error.error is typed as CreateItem400Response return ( <div> <h2>Invalid input:</h2> <p>{createItem.error.error.message}</p> </div> ); case 403: // error.error is typed as CreateItem403Response return ( <div> <h2>Not authorized:</h2> <p>{createItem.error.error.reason}</p> </div> ); case 500: case 502: // error.error is typed as CreateItem5XXResponse return ( <div> <h2>Server error:</h2> <p>{createItem.error.error.message}</p> </div> ); } }
return <button onClick={handleClick}>Create Item</button>;}바닐라 클라이언트를 직접 사용한 오류 처리
function MyComponent() { const api = useMyApiClient(); const [error, setError] = useState<CreateItemError | null>(null);
const handleClick = async () => { try { await api.createItem({ name: 'New Item' }); } catch (e) { const err = e as CreateItemError; setError(err); } };
if (error) { switch (error.status) { case 400: // error.error is typed as CreateItem400Response return ( <div> <h2>Invalid input:</h2> <p>{error.error.message}</p> </div> ); case 403: // error.error is typed as CreateItem403Response return ( <div> <h2>Not authorized:</h2> <p>{error.error.reason}</p> </div> ); case 500: case 502: // error.error is typed as CreateItem5XXResponse return ( <div> <h2>Server error:</h2> <p>{error.error.message}</p> </div> ); } }
return <button onClick={handleClick}>Create Item</button>;}생성된 코드 커스터마이즈
섹션 제목: “생성된 코드 커스터마이즈”생성된 클라이언트를 커스터마이즈하는 데 사용할 수 있는 Smithy trait 선택 항목이 extensions.smithy의 대상 Smithy model 프로젝트에 추가됩니다.
쿼리 및 뮤테이션
섹션 제목: “쿼리 및 뮤테이션”기본적으로 HTTP 메서드 PUT, POST, PATCH 및 DELETE를 사용하는 Smithy API의 작업은 뮤테이션으로 간주되고, 다른 모든 작업은 쿼리로 간주됩니다.
extensions.smithy의 모델 프로젝트에 추가된 @query 및 @mutation Smithy trait를 사용하여 이 동작을 변경할 수 있습니다.
@query
섹션 제목: “@query”Smithy 작업에 @query trait를 적용하여 쿼리로 처리되도록 강제합니다:
@http(method: "POST", uri: "/items")@queryoperation ListItems { input: ListItemsInput output: ListItemsOutput}생성된 훅은 POST HTTP 메서드를 사용하더라도 queryOptions를 제공합니다:
const items = useQuery(api.listItems.queryOptions());@mutation
섹션 제목: “@mutation”Smithy 작업에 @mutation trait를 적용하여 뮤테이션으로 처리되도록 강제합니다:
@http(method: "GET", uri: "/start-processing")@mutationoperation StartProcessing { input: StartProcessingInput output: StartProcessingOutput}생성된 훅은 GET HTTP 메서드를 사용하더라도 mutationOptions를 제공합니다:
const startProcessing = useMutation(api.startProcessing.mutationOptions());커스텀 페이지네이션 커서
섹션 제목: “커스텀 페이지네이션 커서”기본적으로 생성된 훅은 cursor라는 이름의 매개변수를 사용한 커서 기반 페이지네이션을 가정합니다. extensions.smithy의 모델 프로젝트에 추가된 @cursor trait를 사용하여 이 동작을 커스터마이즈할 수 있습니다.
페이지네이션 토큰에 사용되는 입력 매개변수의 이름을 변경하려면 inputToken과 함께 @cursor trait를 적용합니다:
@http(method: "GET", uri: "/items")@cursor(inputToken: "nextToken")operation ListItems { input := { nextToken: String limit: Integer } output := { items: ItemList nextToken: String }}cursor라는 이름의 입력 매개변수가 있는 작업에 대해 infiniteQueryOptions를 생성하지 않으려면 커서 기반 페이지네이션을 비활성화할 수 있습니다:
@cursor(enabled: false)operation ListItems { input := { // Input parameter named 'cursor' will cause this operation to be treated as a paginated operation by default cursor: String } output := { ... }}작업 그룹화
섹션 제목: “작업 그룹화”생성된 훅과 클라이언트 메서드는 Smithy 작업의 @tags trait를 기반으로 자동으로 구성됩니다. 동일한 태그를 가진 작업은 함께 그룹화되어 API 호출을 체계적으로 유지하고 IDE에서 더 나은 코드 완성을 제공합니다.
예를 들어, 다음 Smithy 모델의 경우:
service MyService { operations: [ListItems, CreateItem, ListUsers, CreateUser]}
@tags(["items"])operation ListItems { input: ListItemsInput output: ListItemsOutput}
@tags(["items"])operation CreateItem { input: CreateItemInput output: CreateItemOutput}
@tags(["users"])operation ListUsers { input: ListUsersInput output: ListUsersOutput}
@tags(["users"])operation CreateUser { input: CreateUserInput output: CreateUserOutput}생성된 훅은 태그별로 그룹화됩니다:
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?.map(item => ( <li key={item.id}>{item.name}</li> ))} </ul> <button onClick={handleCreateItem}>Add Item</button>
<h2>Users</h2> <ul> {users.data?.map(user => ( <li key={user.id}>{user.name}</li> ))} </ul> </div> );}이 그룹화를 통해 API 호출을 더 쉽게 구성하고 IDE에서 더 나은 코드 완성을 제공합니다.
API 클라이언트를 직접 사용한 그룹화된 작업
import { useState, useEffect } from 'react';import { useMyApiClient } from './hooks/useMyApiClient';
function ItemsAndUsers() { const api = useMyApiClient(); const [items, setItems] = useState([]); const [users, setUsers] = useState([]); const [isLoading, setIsLoading] = useState(true);
// Load data useEffect(() => { const fetchData = async () => { try { setIsLoading(true);
// Items operations are grouped under api.items const itemsData = await api.items.listItems(); setItems(itemsData);
// Users operations are grouped under api.users const usersData = await api.users.listUsers(); setUsers(usersData); } catch (error) { console.error('Error fetching data:', error); } finally { setIsLoading(false); } };
fetchData(); }, [api]);
const handleCreateItem = async () => { try { // Create item using the grouped method const newItem = await api.items.createItem({ name: 'New Item' }); setItems(prevItems => [...prevItems, newItem]); } catch (error) { console.error('Error creating item:', error); } };
if (isLoading) { return <div>Loading...</div>; }
return ( <div> <h2>Items</h2> <ul> {items.map(item => ( <li key={item.id}>{item.name}</li> ))} </ul> <button onClick={handleCreateItem}>Add Item</button>
<h2>Users</h2> <ul> {users.map(user => ( <li key={user.id}>{user.name}</li> ))} </ul> </div> );}Smithy 모델에서 커스텀 오류 구조를 정의하여 Smithy API의 오류 응답을 커스터마이즈할 수 있습니다. 생성된 클라이언트는 이러한 커스텀 오류 타입을 자동으로 처리합니다.
커스텀 오류 구조 정의
섹션 제목: “커스텀 오류 구조 정의”Smithy 모델에서 오류 구조를 정의합니다:
@error("client")@httpError(400)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();
// Query with typed error handling const getItem = useQuery({ ...api.getItem.queryOptions({ itemId: '123' }), onError: (error) => { // Error is typed based on the errors in your Smithy model switch (error.status) { case 404: // error.error is typed as ItemNotFoundError console.error('Not found:', error.error.message); break; case 500: // error.error is typed as InternalServerError console.error('Server error:', error.error.message); console.error('Trace ID:', error.error.traceId); break; } } });
// Mutation with typed error handling const createItem = useMutation({ ...api.createItem.mutationOptions(), onError: (error) => { switch (error.status) { case 400: // error.error is typed as InvalidRequestError console.error('Validation error:', error.error.message); console.error('Field errors:', error.error.fieldErrors); break; case 403: // error.error is typed as UnauthorizedError console.error('Unauthorized:', error.error.reason); break; } } });
// Component rendering with error handling if (getItem.isError) { if (getItem.error.status === 404) { return <NotFoundMessage message={getItem.error.error.message} />; } else if (getItem.error.status === 500) { return <ErrorMessage message={getItem.error.error.message} />; } }
return ( <div> {/* Component content */} </div> );}클라이언트를 직접 사용한 커스텀 오류 처리
import { useState, useEffect } from 'react';
function ItemComponent() { const api = useMyApiClient(); const [item, setItem] = useState(null); const [error, setError] = useState(null); const [loading, setLoading] = useState(true);
// Fetch item with error handling useEffect(() => { const fetchItem = async () => { try { setLoading(true); const data = await api.getItem({ itemId: '123' }); setItem(data); } catch (e) { // Error is typed based on the errors in your Smithy model const err = e as GetItemError; setError(err);
switch (err.status) { case 404: // err.error is typed as ItemNotFoundError console.error('Not found:', err.error.message); break; case 500: // err.error is typed as InternalServerError console.error('Server error:', err.error.message); console.error('Trace ID:', err.error.traceId); break; } } finally { setLoading(false); } };
fetchItem(); }, [api]);
// Create item with error handling const handleCreateItem = async (data) => { try { await api.createItem(data); } catch (e) { const err = e as CreateItemError;
switch (err.status) { case 400: // err.error is typed as InvalidRequestError console.error('Validation error:', err.error.message); console.error('Field errors:', err.error.fieldErrors); break; case 403: // err.error is typed as UnauthorizedError console.error('Unauthorized:', err.error.reason); break; } } };
// Component rendering with error handling if (loading) { return <LoadingSpinner />; }
if (error) { if (error.status === 404) { return <NotFoundMessage message={error.error.message} />; } else if (error.status === 500) { return <ErrorMessage message={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 ListItems403Response return <ErrorMessage message={err.error.reason} />; case 500: case 502: // err.error is typed as ListItems5XXResponse return ( <ErrorMessage message={err.error.message} /> ); default: return <ErrorMessage message="An unknown error occurred" />; } }
return ( <ul> {items.data.map((item) => ( <li key={item.id}>{item.name}</li> ))} </ul> );}API 클라이언트를 직접 사용한 로딩 상태 처리
function ItemList() { const api = useMyApiClient(); const [items, setItems] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null);
useEffect(() => { const fetchItems = async () => { try { const data = await api.listItems(); setItems(data); } catch (err) { setError(err); } finally { setLoading(false); } }; fetchItems(); }, [api]);
if (loading) { return <LoadingSpinner />; }
if (error) { const err = error as ListItemsError; switch (err.status) { case 403: // err.error is typed as ListItems403Response return <ErrorMessage message={err.error.reason} />; case 500: case 502: // err.error is typed as ListItems5XXResponse return ( <ErrorMessage message={err.error.message} /> ); default: return <ErrorMessage message="An unknown error occurred" />; } }
return ( <ul> {items.map((item) => ( <li key={item.id}>{item.name}</li> ))} </ul> );}낙관적 업데이트
섹션 제목: “낙관적 업데이트”더 나은 사용자 경험을 위해 낙관적 업데이트를 구현하세요:
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
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(api.listItems.queryKey());
// Optimistically update to the new value queryClient.setQueryData( api.listItems.queryKey(), (old) => old.filter((item) => item.id !== itemId) );
// Return a context object with the snapshot return { previousItems }; }, onError: (err, itemId, 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.map((item) => ( <li key={item.id}> {item.name} <button onClick={() => deleteMutation.mutate(item.id)} disabled={deleteMutation.isPending} > {deleteMutation.isPending ? 'Deleting...' : 'Delete'} </button> </li> ))} </ul> );}API 클라이언트를 직접 사용한 낙관적 업데이트
function ItemList() { const api = useMyApiClient(); const [items, setItems] = useState([]);
const handleDelete = async (itemId) => { // Optimistically remove the item const previousItems = items; setItems(items.filter((item) => item.id !== itemId));
try { await api.deleteItem(itemId); } catch (error) { // Restore previous items on error setItems(previousItems); console.error('Failed to delete item:', error); } };
return ( <ul> {items.map((item) => ( <li key={item.id}> {item.name} <button onClick={() => handleDelete(item.id)}>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 400: // error.error is typed as InvalidRequestError return ( <FormError message="Invalid input" errors={error.error.fieldErrors} /> ); case 403: // error.error is typed as UnauthorizedError return <AuthError reason={error.error.reason} />; default: // error.error is typed as InternalServerError 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> );}API 클라이언트를 직접 사용한 타입 안전성
function ItemForm() { const api = useMyApiClient(); const [error, setError] = useState<CreateItemError | null>(null);
const handleSubmit = async (data: CreateItemInput) => { try { // ✅ Type error if input doesn't match schema await api.createItem(data); } catch (e) { // ✅ Error type includes all possible error responses const err = e as CreateItemError; switch (err.status) { case 400: // err.error is typed as InvalidRequestError console.error('Validation errors:', err.error.fieldErrors); break; case 403: // err.error is typed as UnauthorizedError console.error('Not authorized:', err.error.reason); break; case 500: // err.error is typed as InternalServerError console.error('Server error:', err.error.message); break; } setError(err); } };
// Error UI can use type narrowing to handle different error types if (error) { switch (error.status) { case 400: return ( <FormError message="Invalid input" errors={error.error.fieldErrors} /> ); case 403: return <AuthError reason={error.error.reason} />; default: return <ServerError message={error.error.message} />; } }
return <form onSubmit={handleSubmit}>{/* ... */}</form>;}타입은 Smithy API의 OpenAPI 스키마에서 자동으로 생성되므로 API에 대한 모든 변경 사항이 빌드 후 프론트엔드 코드에 반영됩니다.
커스텀 인증
섹션 제목: “커스텀 인증”Smithy API가 Custom 인증(Lambda Authorizer)을 사용하는 경우, 생성된 클라이언트 provider를 편집하여 authorizer가 예상하는 인증 헤더를 추가해야 합니다. 생성된 <ApiName>Provider.tsx에서 fetch 구성을 찾아 요청 헤더에 토큰 또는 API 키를 추가하세요.