React와 FastAPI 연결
api-connection
제너레이터는 React 웹사이트와 FastAPI 백엔드의 통합을 빠르게 설정할 수 있는 방법을 제공합니다. 타입 안전성을 보장하는 방식으로 FastAPI 백엔드 연결에 필요한 모든 구성을 설정하며, 클라이언트 및 TanStack Query 훅 생성, AWS IAM 인증 지원, 적절한 오류 처리 등을 포함합니다.
사전 요구사항
이 제너레이터를 사용하기 전에 React 애플리케이션이 다음을 갖추었는지 확인하세요:
- 애플리케이션을 렌더링하는
main.tsx
파일 - 작동 중인 FastAPI 백엔드 (FastAPI 제너레이터로 생성된 것)
필요한 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 - api-connection
- 필수 매개변수 입력
- 클릭
Generate
pnpm nx g @aws/nx-plugin:api-connection
yarn nx g @aws/nx-plugin:api-connection
npx nx g @aws/nx-plugin:api-connection
bunx nx g @aws/nx-plugin:api-connection
어떤 파일이 변경될지 확인하기 위해 드라이 런을 수행할 수도 있습니다
pnpm nx g @aws/nx-plugin:api-connection --dry-run
yarn nx g @aws/nx-plugin:api-connection --dry-run
npx nx g @aws/nx-plugin:api-connection --dry-run
bunx nx g @aws/nx-plugin:api-connection --dry-run
옵션
매개변수 | 타입 | 기본값 | 설명 |
---|---|---|---|
sourceProject 필수 | string | - | The source project which will call the API |
targetProject 필수 | string | - | The target project containing your API |
auth | string | IAM | Authentication strategy (choose from IAM or None) |
제너레이터 출력
제너레이터는 FastAPI 프로젝트의 다음 파일들을 변경합니다:
디렉터리scripts
- generate_open_api.py API용 OpenAPI 명세를 생성하는 스크립트 추가
- project.json 위 생성 스크립트를 호출하는 새로운 빌드 타겟 추가
제너레이터는 React 애플리케이션의 다음 파일들을 변경합니다:
디렉터리src
디렉터리components
- <ApiName>Provider.tsx API 클라이언트 제공자
- QueryClientProvider.tsx TanStack React Query 클라이언트 제공자
디렉터리hooks
- use<ApiName>.tsx TanStack Query로 상태 관리되는 API 호출 훅 추가
- use<ApiName>Client.tsx 기본 API 클라이언트 인스턴스화 훅 추가
- useSigV4.tsx IAM 인증 선택 시 SigV4로 HTTP 요청 서명 훅 추가
- project.json 타입 안전 클라이언트 생성하는 새로운 빌드 타겟 추가
- .gitignore 기본적으로 생성된 클라이언트 파일 버전 관리 제외
제너레이터는 또한 웹사이트 인프라에 Runtime Config를 추가합니다(아직 없는 경우). 이는 FastAPI의 API URL이 웹사이트에서 사용 가능하고 use<ApiName>.tsx
훅에 의해 자동 구성되도록 보장합니다.
코드 생성
빌드 시점에 FastAPI의 OpenAPI 명세로부터 타입 안전 클라이언트가 생성됩니다. 이는 React 애플리케이션에 세 개의 새 파일을 추가합니다:
디렉터리src
디렉터리generated
디렉터리<ApiName>
- types.gen.ts FastAPI에 정의된 pydantic 모델에서 생성된 타입
- client.gen.ts API 호출용 타입 안전 클라이언트
- options-proxy.gen.ts TanStack Query 훅 옵션 생성 메서드 제공
생성된 코드 사용
생성된 타입 안전 클라이언트를 사용하여 React 애플리케이션에서 FastAPI를 호출할 수 있습니다. TanStack Query 훅을 통해 클라이언트를 사용하는 것이 권장되지만, 기본 클라이언트를 직접 사용할 수도 있습니다.
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); } 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, }, { getNextPageParam: (lastPage) => lastPage.nextCursor || undefined }), });
if (items.isLoading) { return <LoadingSpinner />; }
if (items.isError) { return <ErrorMessage message={items.error.message} />; }
return ( <div> <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> );}
오류 처리
통합에는 타입이 지정된 오류 응답 처리 기능이 내장되어 있습니다. OpenAPI 명세에 정의된 가능한 오류 응답을 캡슐화하는 <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: return ( <div> <h2>Invalid input:</h2> <p>{createItem.error.error.message}</p> <ul> {createItem.error.error.validationErrors.map((err) => ( <li key={err.field}>{err.message}</li> ))} </ul> </div> ); case 403: return ( <div> <h2>Not authorized:</h2> <p>{createItem.error.error.reason}</p> </div> ); case 500: case 502: return ( <div> <h2>Server error:</h2> <p>{createItem.error.error.message}</p> <p>Trace ID: {createItem.error.error.traceId}</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: return ( <div> <h2>Invalid input:</h2> <p>{error.error.message}</p> <ul> {error.error.validationErrors.map((err) => ( <li key={err.field}>{err.message}</li> ))} </ul> </div> ); case 403: return ( <div> <h2>Not authorized:</h2> <p>{error.error.reason}</p> </div> ); case 500: case 502: return ( <div> <h2>Server error:</h2> <p>{error.error.message}</p> <p>Trace ID: {error.error.traceId}</p> </div> ); } }
return <button onClick={handleClick}>Create Item</button>;}
스트림 소비
FastAPI 응답 스트리밍을 구성한 경우, useQuery
훅은 새로운 스트림 청크가 도착할 때마다 데이터를 자동으로 업데이트합니다.
예시:
function MyStreamingComponent() { const api = useMyApi(); const stream = useQuery(api.myStream.queryOptions());
return ( <ul> {(stream.data ?? []).map((chunk) => ( <li> {chunk.timestamp.toISOString()}: {chunk.message} </li> ))} </ul> );}
스트림 상태 확인을 위해 isLoading
및 fetchStatus
속성을 사용할 수 있습니다. 스트림 생명주기:
-
스트리밍 시작 HTTP 요청 전송
isLoading
:true
fetchStatus
:'fetching'
data
:undefined
-
첫 번째 청크 수신
isLoading
:false
fetchStatus
:'fetching'
data
: 첫 번째 청크 포함 배열
-
후속 청크 수신
isLoading
:false
fetchStatus
:'fetching'
data
: 수신 즉시 업데이트
-
스트림 완료
isLoading
:false
fetchStatus
:'idle'
data
: 모든 청크 배열
기본 클라이언트로 스트리밍 처리
FastAPI 스트리밍 응답을 구성한 경우>, 생성된 클라이언트는 for await
구문을 사용한 스트림 청크 비동기 반복 메서드를 포함합니다.
예시:
function MyStreamingComponent() { const api = useMyApiClient(); const [chunks, setChunks] = useState<Chunk[]>([]);
useEffect(() => { const streamChunks = async () => { for await (const chunk of api.myStream()) { setChunks((prev) => [...prev, chunk]); } }; streamChunks(); }, [api]);
return ( <ul> {chunks.map((chunk) => ( <li> {chunk.timestamp.toISOString()}: {chunk.message} </li> ))} </ul> );}
생성 코드 커스터마이징
쿼리 및 뮤테이션
기본적으로 PUT
, POST
, PATCH
, DELETE
HTTP 메서드를 사용하는 FastAPI 작업은 뮤테이션으로, 나머지는 쿼리로 간주됩니다.
x-query
및 x-mutation
으로 이 동작을 변경할 수 있습니다.
x-query
@app.post( "/items", openapi_extra={ "x-query": True })def list_items(): # ...
POST
메서드 사용에도 queryOptions
제공:
const items = useQuery(api.listItems.queryOptions());
x-mutation
@app.get( "/start-processing", openapi_extra={ "x-mutation": True })def start_processing(): # ...
GET
메서드 사용에도 mutationOptions
제공:
const startProcessing = useMutation(api.startProcessing.mutationOptions());
커스텀 페이징 커서
기본 커서 파라미터 이름 cursor
를 x-cursor
확장으로 커스터마이징:
@app.get( "/items", openapi_extra={ "x-cursor": "page_token" })def list_items(page_token: str = None, limit: int = 10): # ... return { "items": items, "page_token": next_page_token }
infiniteQueryOptions
생성을 원하지 않을 경우 x-cursor
를 False
로 설정:
@app.get( "/items", openapi_extra={ "x-cursor": False })def list_items(page: int = 1, limit: int = 10): # ... return { "items": items, "total": total_count, "page": page, "pages": total_pages }
작업 그룹화
생성된 훅과 클라이언트 메서드는 FastAPI 엔드포인트의 OpenAPI 태그 기반으로 자동 조직화됩니다. 관련 작업을 쉽게 찾고 관리할 수 있도록 도와줍니다.
예시:
@app.get( "/items", tags=["items"],)def list(): # ...
@app.post( "/items", tags=["items"],)def create(item: Item): # ...
@app.get( "/users", tags=["users"],)def list(): # ...
생성된 훅은 태그별로 그룹화:
import { useQuery, useMutation } from '@tanstack/react-query';import { useMyApi } from './hooks/useMyApi';
function ItemsAndUsers() { const api = useMyApi();
const items = useQuery(api.items.list.queryOptions()); const createItem = useMutation(api.items.create.mutationOptions());
const users = useQuery(api.users.list.queryOptions());
return ( <div> <h2>Items</h2> <ul> {items.data?.map(item => ( <li key={item.id}>{item.name}</li> ))} </ul> <button onClick={() => createItem.mutate({ name: 'New Item' })}>Add Item</button>
<h2>Users</h2> <ul> {users.data?.map(user => ( <li key={user.id}>{user.name}</li> ))} </ul> </div> );}
기본 클라이언트로 그룹화 작업 사용
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); const itemsData = await api.items.list(); setItems(itemsData); const usersData = await api.users.list(); setUsers(usersData); } catch (error) { console.error('Error fetching data:', error); } finally { setIsLoading(false); } };
fetchData(); }, [api]);
const handleCreateItem = async () => { try { const newItem = await api.items.create({ 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> );}
오류
FastAPI에서 커스텀 예외 클래스, 예외 핸들러, 응답 모델을 정의하여 오류 응답을 커스터마이징할 수 있습니다. 생성된 클라이언트는 자동으로 이들 커스텀 오류 유형을 처리합니다.
커스텀 오류 모델 정의
Pydantic으로 오류 모델 정의:
from pydantic import BaseModel
class ErrorDetails(BaseModel): message: str
class ValidationError(BaseModel): message: str field_errors: list[str]
커스텀 예외 생성
다른 오류 시나리오용 예외 클래스 생성:
class NotFoundException(Exception): def __init__(self, message: str): self.message = message
class ValidationException(Exception): def __init__(self, details: ValidationError): self.details = details
예외 핸들러 추가
예외를 HTTP 응답으로 변환:
from fastapi import Requestfrom fastapi.responses import JSONResponse
@app.exception_handler(NotFoundException)async def not_found_handler(request: Request, exc: NotFoundException): return JSONResponse( status_code=404, content=exc.message, )
@app.exception_handler(ValidationException)async def validation_error_handler(request: Request, exc: ValidationException): return JSONResponse( status_code=400, content=exc.details.model_dump(), )
응답 모델 지정
엔드포인트 정의 시 오류 상태 코드별 응답 모델 지정:
@app.get( "/items/{item_id}", responses={ 404: {"model": str} 500: {"model": ErrorDetails} })def get_item(item_id: str) -> Item: item = find_item(item_id) if not item: raise NotFoundException(message=f"Item with ID {item_id} not found") return item
@app.post( "/items", responses={ 400: {"model": ValidationError}, 403: {"model": str} })def create_item(item: Item) -> Item: if not is_valid(item): raise ValidationException( ValidationError( message="Invalid item data", field_errors=["name is required"] ) ) return save_item(item)
React에서 커스텀 오류 유형 사용
생성된 클라이언트는 이들 커스텀 오류 유형을 자동 처리:
import { useMutation, useQuery } from '@tanstack/react-query';
function ItemComponent() { const api = useMyApi();
const getItem = useQuery({ ...api.getItem.queryOptions({ itemId: '123' }), onError: (error) => { switch (error.status) { case 404: console.error('Not found:', error.error); break; case 500: console.error('Server error:', error.error.message); break; } } });
const createItem = useMutation({ ...api.createItem.mutationOptions(), onError: (error) => { switch (error.status) { case 400: console.error('Validation error:', error.error.message); break; case 403: console.error('Forbidden:', error.error); break; } } });
if (getItem.isError) { if (getItem.error.status === 404) { return <NotFoundMessage message={getItem.error.error} />; } else { 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) { const err = e as GetItemError; setError(err); switch (err.status) { case 404: console.error('Not found:', err.error); break; case 500: console.error('Server error:', err.error.message); 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: console.error('Validation error:', err.error.message); break; case 403: console.error('Forbidden:', err.error); break; } } };
if (loading) return <LoadingSpinner />;
if (error) { if (error.status === 404) { return <NotFoundMessage message={error.error} />; } 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: return <ErrorMessage message={err.error.reason} />; case 500: case 502: return ( <ErrorMessage message={err.error.message} details={`Trace ID: ${err.error.traceId}`} /> ); 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: return <ErrorMessage message={err.error.reason} />; case 500: case 502: return ( <ErrorMessage message={err.error.message} details={`Trace ID: ${err.error.traceId}`} /> ); 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) => { 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: (data) => { console.log(`Item created with ID: ${data.id}`); }, });
const handleSubmit = (data: CreateItemInput) => { createItem.mutate(data); };
if (createItem.error) { const error = createItem.error; switch (error.status) { case 400: return ( <FormError message="Invalid input" errors={error.error.validationErrors} /> ); case 403: return <AuthError reason={error.error.reason} />; default: 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: console.error('Validation errors:', err.error.validationErrors); break; case 403: console.error('Not authorized:', err.error.reason); break; case 500: case 502: console.error( 'Server error:', err.error.message, 'Trace:', err.error.traceId, ); break; } setError(err); } };
if (error) { switch (error.status) { case 400: return ( <FormError message="Invalid input" errors={error.error.validationErrors} /> ); case 403: return <AuthError reason={error.error.reason} />; default: return <ServerError message={error.error.message} />; } }
return <form onSubmit={handleSubmit}>{/* ... */}</form>;}
타입은 FastAPI의 OpenAPI 스키마에서 자동 생성되며, API 변경 사항은 빌드 후 프론트엔드 코드에 반영됩니다.