React와 FastAPI 연결
api-connection
제너레이터는 React 웹사이트와 FastAPI 백엔드의 통합을 빠르게 설정할 수 있는 방법을 제공합니다. 타입 세이프한 방식으로 FastAPI 백엔드 연결에 필요한 모든 구성을 설정하며, 클라이언트 및 TanStack Query 훅 생성, AWS IAM 및 Cognito 인증 지원, 적절한 오류 처리를 포함합니다.
필수 조건
섹션 제목: “필수 조건”이 제너레이터를 사용하기 전에 React 애플리케이션이 다음을 갖추었는지 확인하세요:
- 애플리케이션을 렌더링하는
main.tsx
파일 - 작동하는 FastAPI 백엔드 (FastAPI 제너레이터로 생성된 것)
- 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 - 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 |
제너레이터 출력
섹션 제목: “제너레이터 출력”제너레이터는 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 기본적으로 생성된 클라이언트 파일 무시
제너레이터는 또한 웹사이트 인프라에 런타임 구성을 추가하여 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 훅을 통해 클라이언트를 사용하는 것이 권장되지만, 일반 클라이언트도 사용 가능합니다.
watch-generate:<ApiName>-client
는 nx watch
명령에 의존하며 Nx Daemon 실행이 필요합니다. 따라서 데몬이 비활성화된 경우 FastAPI 변경 시 클라이언트가 자동으로 재생성되지 않습니다.
API 훅 사용
섹션 제목: “API 훅 사용”제너레이터는 TanStack Query의 useQuery
훅으로 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>;}
일반 클라이언트 직접 사용 예시
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> {/* 모든 항목 렌더링을 위해 페이지 배열 평탄화 */} <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> );}
오류 처리
섹션 제목: “오류 처리”통합에는 타입화된 오류 응답이 내장되어 있습니다. <operation-name>Error
타입이 생성되어 OpenAPI 명세에 정의된 가능한 오류 응답을 캡슐화합니다. 각 오류는 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> <ul> {createItem.error.error.validationErrors.map((err) => ( <li key={err.field}>{err.message}</li> ))} </ul> </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> <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: // error.error는 CreateItem400Response 타입 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: // 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> <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> );}
생성 코드 커스터마이징
섹션 제목: “생성 코드 커스터마이징”쿼리와 뮤테이션
섹션 제목: “쿼리와 뮤테이션”기본적으로 FastAPI의 PUT
, POST
, PATCH
, DELETE
HTTP 메서드를 사용하는 작업은 뮤테이션으로, 나머지는 쿼리로 간주됩니다.
x-query
와 x-mutation
을 사용하여 이 동작을 변경할 수 있습니다.
x-query
섹션 제목: “x-query”@app.post( "/items", openapi_extra={ "x-query": True })def list_items(): # ...
생성된 훅은 POST
HTTP 메서드 사용에도 queryOptions
제공:
const items = useQuery(api.listItems.queryOptions());
x-mutation
섹션 제목: “x-mutation”@app.get( "/start-processing", openapi_extra={ "x-mutation": True })def start_processing(): # ...
생성된 훅은 GET
HTTP 메서드 사용에도 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 태그 기반으로 자동 구성됩니다. 이는 관련 API 호출을 조직화하고 찾기 쉽게 합니다.
예시:
@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();
// items 작업은 api.items 아래 그룹화 const items = useQuery(api.items.list.queryOptions()); const createItem = useMutation(api.items.create.mutationOptions());
// users 작업은 api.users 아래 그룹화 const users = useQuery(api.users.list.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.list(); setItems(itemsData);
// users 작업은 api.users 아래 그룹화 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> );}
커스텀 예외 클래스, 예외 핸들러, 다양한 상태 코드에 대한 응답 모델 정의로 오류 응답을 커스터마이즈할 수 있습니다. 생성된 클라이언트는 자동으로 이 커스텀 오류 타입을 처리합니다.
커스텀 오류 모델 정의
섹션 제목: “커스텀 오류 모델 정의”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에서 커스텀 오류 타입 사용
섹션 제목: “React에서 커스텀 오류 타입 사용”생성된 클라이언트는 이 커스텀 오류 타입을 자동 처리하여 타입 검사 및 다양한 오류 응답 처리가 가능합니다:
import { useMutation, useQuery } from '@tanstack/react-query';
function ItemComponent() { const api = useMyApi();
// 타입화된 오류 처리와 쿼리 const getItem = useQuery({ ...api.getItem.queryOptions({ itemId: '123' }), onError: (error) => { // 오류는 FastAPI의 응답에 따라 타입화됨 switch (error.status) { case 404: // error.error는 응답에 지정된 문자열 console.error('Not found:', error.error); break; case 500: // error.error는 ErrorDetails 타입 console.error('Server error:', error.error.message); break; } } });
// 타입화된 오류 처리와 뮤테이션 const createItem = useMutation({ ...api.createItem.mutationOptions(), onError: (error) => { switch (error.status) { case 400: // error.error는 ValidationError 타입 console.error('Validation error:', error.error.message); console.error('Field errors:', error.error.field_errors); break; case 403: // error.error는 응답에 지정된 문자열 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) { // 오류는 FastAPI의 응답에 따라 타입화됨 const err = e as GetItemError; setError(err);
switch (err.status) { case 404: // err.error는 응답에 지정된 문자열 console.error('Not found:', err.error); break; case 500: // err.error는 ErrorDetails 타입 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: // err.error는 ValidationError 타입 console.error('Validation error:', err.error.message); console.error('Field errors:', err.error.field_errors); break; case 403: // err.error는 응답에 지정된 문자열 console.error('Forbidden:', err.error); break; } } };
// 오류 처리와 컴포넌트 렌더링 if (loading) { return <div>Loading...</div>; }
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: // err.error는 ListItems403Response 타입 return <ErrorMessage message={err.error.reason} />; case 500: case 502: // err.error는 ListItems5XXResponse 타입 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: // err.error는 ListItems403Response 타입 return <ErrorMessage message={err.error.reason} />; case 500: case 502: // err.error는 ListItems5XXResponse 타입 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) => { // 뮤테이션 실패 시 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는 CreateItem400Response 타입 return ( <FormError message="Invalid input" errors={error.error.validationErrors} /> ); case 403: // error.error는 CreateItem403Response 타입 return <AuthError reason={error.error.reason} />; default: // error.error는 500, 502 등에 대해 CreateItem5XXResponse 타입 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는 CreateItem400Response 타입 console.error('Validation errors:', err.error.validationErrors); break; case 403: // err.error는 CreateItem403Response 타입 console.error('Not authorized:', err.error.reason); break; case 500: case 502: // err.error는 CreateItem5XXResponse 타입 console.error( 'Server error:', err.error.message, 'Trace:', err.error.traceId, ); break; } setError(err); } };
// 오류 UI는 타입 좁히기를 사용하여 다양한 오류 처리 가능 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 변경사항은 빌드 후 프론트엔드 코드에 반영됩니다.