Smithy API에 React 연결
connection 제너레이터는 React 웹사이트를 Smithy TypeScript API 백엔드와 빠르게 통합할 수 있는 방법을 제공합니다. 타입 세이프한 방식으로 Smithy API 연결에 필요한 모든 구성을 설정하며, 클라이언트 및 TanStack Query 훅 생성, AWS IAM 및 Cognito 인증 지원, 적절한 오류 처리를 포함합니다.
필수 조건
섹션 제목: “필수 조건”이 제너레이터를 사용하기 전에 React 애플리케이션이 다음을 갖추었는지 확인하세요:
- 애플리케이션을 렌더링하는
main.tsx파일 - 작동하는 Smithy TypeScript API 백엔드 (
ts#smithy-api제너레이터로 생성) - 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>,);사용 방법
섹션 제목: “사용 방법”제너레이터 실행
섹션 제목: “제너레이터 실행”- 설치 Nx Console VSCode Plugin 아직 설치하지 않았다면
- VSCode에서 Nx 콘솔 열기
- 클릭
Generate (UI)"Common Nx Commands" 섹션에서 - 검색
@aws/nx-plugin - connection - 필수 매개변수 입력
- 클릭
Generate
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| 매개변수 | 타입 | 기본값 | 설명 |
|---|---|---|---|
| sourceProject 필수 | string | - | 소스 프로젝트 |
| targetProject 필수 | string | - | 연결할 대상 프로젝트 |
| sourceComponent | string | - | 연결을 시작할 소스 컴포넌트 (컴포넌트 이름, 소스 프로젝트 루트 기준 상대 경로, 또는 generator id). 프로젝트를 소스로 명시적으로 선택하려면 '.'을 사용하세요. |
| targetComponent | string | - | 연결할 대상 컴포넌트 (컴포넌트 이름, 대상 프로젝트 루트 기준 상대 경로, 또는 generator id). 프로젝트를 대상으로 명시적으로 선택하려면 '.'을 사용하세요. |
제너레이터 출력
섹션 제목: “제너레이터 출력”제너레이터는 React 애플리케이션의 다음 파일들을 변경합니다:
디렉터리src
디렉터리components
- <ApiName>Provider.tsx API 클라이언트용 프로바이더
- QueryClientProvider.tsx TanStack React Query 클라이언트 프로바이더
디렉터리RuntimeConfig/ 로컬 개발용 런타임 설정 컴포넌트
- …
디렉터리hooks
- use<ApiName>.tsx TanStack Query로 상태 관리되는 API 호출 훅 추가
- use<ApiName>Client.tsx 기본 API 클라이언트 인스턴스 생성 훅 추가
- useSigV4.tsx IAM 인증 선택 시 SigV4 서명 훅 추가
- project.json 타입 세이프 클라이언트 생성용 새 빌드 타겟 추가
- .gitignore 기본적으로 생성된 클라이언트 파일 무시
제너레이터는 Smithy 모델에도 파일을 추가합니다:
디렉터리model
디렉터리src
- extensions.smithy 생성된 클라이언트 커스터마이즈용 트레이트 정의
또한 제너레이터는 웹사이트 인프라에 런타임 구성을 추가하여 Smithy 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 훅 옵션 생성 메서드 제공
생성된 코드 사용
섹션 제목: “생성된 코드 사용”생성된 타입 세이프 클라이언트로 React 애플리케이션에서 Smithy API를 호출할 수 있습니다. TanStack Query 훅을 사용하는 것이 권장되지만 기본 클라이언트도 사용 가능합니다.
API 훅 사용
섹션 제목: “API 훅 사용”제너레이터는 TanStack Query로 API를 호출할 수 있는 use<ApiName> 훅을 제공합니다.
queryOptions 메서드를 사용하여 TanStack Query의 useQuery 훅에 필요한 옵션을 검색할 수 있습니다:
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(); // 생성된 뮤테이션 옵션 사용 const createItem = useMutation(api.createItem.mutationOptions());
const handleSubmit = (e) => { e.preventDefault(); createItem.mutate({ name: 'New Item', description: 'A new item' }); };
return ( <form onSubmit={handleSubmit}> {/* 폼 필드 */} <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) => { // 뮤테이션 성공 시 실행 console.log('Item created:', data); // 새 아이템으로 이동 가능 navigate(`/items/${data.id}`); }, onError: (error) => { // 뮤테이션 실패 시 실행 console.error('Failed to create item:', error); }, onSettled: () => { // 뮤테이션 완료 시 실행 (성공 또는 실패) // 영향을 받을 수 있는 쿼리 무효화에 적합한 위치 queryClient.invalidateQueries({ queryKey: api.listItems.queryKey() }); }});기본 클라이언트로 뮤테이션 사용
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); // 새 아이템으로 이동 가능 // navigate(`/items/${newItem.id}`); } catch (err) { setError(err); console.error('Failed to create item:', err); } finally { setIsLoading(false); } };
return ( <form onSubmit={handleSubmit}> {/* 폼 필드 */} <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, // 페이지당 아이템 수 }, { // 다음 페이지의 'cursor'로 전달될 파라미터를 반환하는 // getNextPageParam 함수를 정의해야 합니다 getNextPageParam: (lastPage) => lastPage.nextCursor || undefined }), });
if (items.isLoading) { return <LoadingSpinner />; }
if (items.isError) { return <ErrorMessage message={items.error.message} />; }
return ( <div> {/* 모든 아이템을 렌더링하기 위해 pages 배열 평탄화 */} <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 값이 응답에서 추출되어 다음 페이지 호출에 사용됩니다.
기본 클라이언트로 페이지네이션 사용
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);
// 초기 데이터 가져오기 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]);
// 더 많은 아이템 로드 함수 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는 CreateItem400Response로 타입 지정됨 return ( <div> <h2>Invalid input:</h2> <p>{createItem.error.error.message}</p> </div> ); case 403: // error.error는 CreateItem403Response로 타입 지정됨 return ( <div> <h2>Not authorized:</h2> <p>{createItem.error.error.reason}</p> </div> ); case 500: case 502: // error.error는 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는 CreateItem400Response로 타입 지정됨 return ( <div> <h2>Invalid input:</h2> <p>{error.error.message}</p> </div> ); case 403: // error.error는 CreateItem403Response로 타입 지정됨 return ( <div> <h2>Not authorized:</h2> <p>{error.error.reason}</p> </div> ); case 500: case 502: // error.error는 CreateItem5XXResponse로 타입 지정됨 return ( <div> <h2>Server error:</h2> <p>{error.error.message}</p> </div> ); } }
return <button onClick={handleClick}>Create Item</button>;}생성된 코드 커스터마이징
섹션 제목: “생성된 코드 커스터마이징”Smithy model 프로젝트의 extensions.smithy에 추가된 여러 트레이트를 사용하여 생성된 클라이언트를 커스터마이즈할 수 있습니다.
쿼리와 뮤테이션
섹션 제목: “쿼리와 뮤테이션”기본적으로 Smithy API의 PUT, POST, PATCH, DELETE HTTP 메서드를 사용하는 작업은 뮤테이션으로, 나머지는 쿼리로 간주됩니다.
@query 및 @mutation Smithy 트레이트를 사용하여 이 동작을 변경할 수 있습니다.
@query
섹션 제목: “@query”@query 트레이트를 적용하여 POST 메서드임에도 쿼리로 처리되도록 강제:
@http(method: "POST", uri: "/items")@queryoperation ListItems { input: ListItemsInput output: ListItemsOutput}생성된 훅은 POST 메서드임에도 queryOptions를 제공합니다:
const items = useQuery(api.listItems.queryOptions());@mutation
섹션 제목: “@mutation”@mutation 트레이트를 적용하여 GET 메서드임에도 뮤테이션으로 처리되도록 강제:
@http(method: "GET", uri: "/start-processing")@mutationoperation StartProcessing { input: StartProcessingInput output: StartProcessingOutput}생성된 훅은 GET 메서드임에도 mutationOptions를 제공합니다:
const startProcessing = useMutation(api.startProcessing.mutationOptions());커스텀 페이지네이션 커서
섹션 제목: “커스텀 페이지네이션 커서”기본적으로 생성된 훅은 cursor 파라미터 이름을 가정합니다. @cursor 트레이트로 커스터마이즈 가능합니다.
inputToken으로 입력 파라미터 이름 변경:
@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 := { // 'cursor'라는 이름의 입력 파라미터는 기본적으로 이 작업을 페이지네이션 작업으로 처리하게 함 cursor: String } output := { ... }}작업 그룹화
섹션 제목: “작업 그룹화”생성된 훅과 클라이언트 메서드는 Smithy 작업의 @tags 트레이트를 기반으로 자동 구성됩니다. 동일한 태그를 가진 작업은 그룹화되어 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 작업은 api.items 아래에 그룹화됨 const items = useQuery(api.items.listItems.queryOptions()); const createItem = useMutation(api.items.createItem.mutationOptions());
// Users 작업은 api.users 아래에 그룹화됨 const users = useQuery(api.users.listUsers.queryOptions());
// 사용 예시 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의 코드 완성 기능을 개선합니다.
기본 클라이언트로 그룹화된 작업 사용
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);
// 데이터 로드 useEffect(() => { const fetchData = async () => { try { setIsLoading(true);
// Items 작업은 api.items 아래에 그룹화됨 const itemsData = await api.items.listItems(); setItems(itemsData);
// Users 작업은 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 { // 그룹화된 메서드를 사용하여 아이템 생성 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 모델에 오류 구조 정의:
@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();
// 타입화된 오류 처리가 포함된 쿼리 const getItem = useQuery({ ...api.getItem.queryOptions({ itemId: '123' }), onError: (error) => { // 오류는 Smithy 모델의 오류를 기반으로 타입 지정됨 switch (error.status) { case 404: // error.error는 ItemNotFoundError로 타입 지정됨 console.error('Not found:', error.error.message); break; case 500: // error.error는 InternalServerError로 타입 지정됨 console.error('Server error:', error.error.message); console.error('Trace ID:', error.error.traceId); break; } } });
// 타입화된 오류 처리가 포함된 뮤테이션 const createItem = useMutation({ ...api.createItem.mutationOptions(), onError: (error) => { switch (error.status) { case 400: // error.error는 InvalidRequestError로 타입 지정됨 console.error('Validation error:', error.error.message); console.error('Field errors:', error.error.fieldErrors); break; case 403: // error.error는 UnauthorizedError로 타입 지정됨 console.error('Unauthorized:', error.error.reason); break; } } });
// 오류 처리가 포함된 컴포넌트 렌더링 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> {/* 컴포넌트 내용 */} </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);
// 오류 처리가 포함된 아이템 가져오기 useEffect(() => { const fetchItem = async () => { try { setLoading(true); const data = await api.getItem({ itemId: '123' }); setItem(data); } catch (e) { // 오류는 Smithy 모델의 오류를 기반으로 타입 지정됨 const err = e as GetItemError; setError(err);
switch (err.status) { case 404: // err.error는 ItemNotFoundError로 타입 지정됨 console.error('Not found:', err.error.message); break; case 500: // err.error는 InternalServerError로 타입 지정됨 console.error('Server error:', err.error.message); console.error('Trace ID:', err.error.traceId); break; } } finally { setLoading(false); } };
fetchItem(); }, [api]);
// 오류 처리가 포함된 아이템 생성 const handleCreateItem = async (data) => { try { await api.createItem(data); } catch (e) { const err = e as CreateItemError;
switch (err.status) { case 400: // err.error는 InvalidRequestError로 타입 지정됨 console.error('Validation error:', err.error.message); console.error('Field errors:', err.error.fieldErrors); break; case 403: // err.error는 UnauthorizedError로 타입 지정됨 console.error('Unauthorized:', err.error.reason); break; } } };
// 오류 처리가 포함된 컴포넌트 렌더링 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> {/* 컴포넌트 내용 */} </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는 ListItems403Response로 타입 지정됨 return <ErrorMessage message={err.error.reason} />; case 500: case 502: // err.error는 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> );}기본 클라이언트로 로딩 상태 처리
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는 ListItems403Response로 타입 지정됨 return <ErrorMessage message={err.error.reason} />; case 500: case 502: // err.error는 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();
// 아이템 가져오기 쿼리 const itemsQuery = useQuery(api.listItems.queryOptions());
// 낙관적 업데이트가 포함된 아이템 삭제 뮤테이션 const deleteMutation = useMutation({ ...api.deleteItem.mutationOptions(), onMutate: async (itemId) => { // 진행 중인 리페치 취소 await queryClient.cancelQueries({ queryKey: api.listItems.queryKey() });
// 이전 값 스냅샷 const previousItems = queryClient.getQueryData(api.listItems.queryKey());
// 새 값으로 낙관적 업데이트 queryClient.setQueryData( api.listItems.queryKey(), (old) => old.filter((item) => item.id !== itemId) );
// 스냅샷이 포함된 컨텍스트 객체 반환 return { previousItems }; }, onError: (err, itemId, context) => { // 뮤테이션 실패 시 onMutate에서 반환된 컨텍스트를 사용하여 롤백 queryClient.setQueryData(api.listItems.queryKey(), context.previousItems); console.error('Failed to delete item:', err); }, onSettled: () => { // 오류 또는 성공 후 항상 리페치하여 서버와 데이터 동기화 보장 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> );}기본 클라이언트로 낙관적 업데이트
function ItemList() { const api = useMyApiClient(); const [items, setItems] = useState([]);
const handleDelete = async (itemId) => { // 낙관적으로 아이템 제거 const previousItems = items; setItems(items.filter((item) => item.id !== itemId));
try { await api.deleteItem(itemId); } catch (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();
// 아이템 생성을 위한 타입 세이프 뮤테이션 const createItem = useMutation({ ...api.createItem.mutationOptions(), // ✅ onSuccess 콜백이 올바른 응답 타입을 처리하지 않으면 타입 오류 발생 onSuccess: (data) => { // data는 API의 응답 스키마를 기반으로 완전히 타입 지정됨 console.log(`Item created with ID: ${data.id}`); }, });
const handleSubmit = (data: CreateItemInput) => { // ✅ 입력이 스키마와 일치하지 않으면 타입 오류 발생 createItem.mutate(data); };
// 오류 UI는 타입 좁히기를 사용하여 다양한 오류 유형 처리 가능 if (createItem.error) { const error = createItem.error; switch (error.status) { case 400: // error.error는 InvalidRequestError로 타입 지정됨 return ( <FormError message="Invalid input" errors={error.error.fieldErrors} /> ); case 403: // error.error는 UnauthorizedError로 타입 지정됨 return <AuthError reason={error.error.reason} />; default: // error.error는 500 등에 대해 InternalServerError로 타입 지정됨 return <ServerError message={error.error.message} />; } }
return ( <form onSubmit={(e) => { e.preventDefault(); handleSubmit({ name: 'New Item' }); }}> {/* 폼 필드 */} <button type="submit" disabled={createItem.isPending} > {createItem.isPending ? 'Creating...' : 'Create Item'} </button> </form> );}기본 클라이언트 타입 안전성
function ItemForm() { const api = useMyApiClient(); const [error, setError] = useState<CreateItemError | null>(null);
const handleSubmit = async (data: CreateItemInput) => { try { // ✅ 입력이 스키마와 일치하지 않으면 타입 오류 발생 await api.createItem(data); } catch (e) { // ✅ 오류 타입은 가능한 모든 오류 응답을 포함 const err = e as CreateItemError; switch (err.status) { case 400: // err.error는 InvalidRequestError로 타입 지정됨 console.error('Validation errors:', err.error.fieldErrors); break; case 403: // err.error는 UnauthorizedError로 타입 지정됨 console.error('Not authorized:', err.error.reason); break; case 500: // err.error는 InternalServerError로 타입 지정됨 console.error('Server error:', err.error.message); break; } setError(err); } };
// 오류 UI는 타입 좁히기를 사용하여 다양한 오류 유형 처리 가능 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 변경 사항은 빌드 후 프론트엔드 코드에 반영됩니다.
Custom Auth
섹션 제목: “Custom Auth”If your Smithy API uses Custom authentication (Lambda Authorizer), you will need to edit the generated client provider to add the authorization headers your authorizer expects. Look for the fetch configuration in the generated <ApiName>Provider.tsx and add your token or API key to the request headers.