컨텐츠로 건너뛰기

React와 FastAPI 연결

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

필수 조건

이 제너레이터를 사용하기 전에 React 애플리케이션이 다음을 갖추었는지 확인하세요:

  1. 애플리케이션을 렌더링하는 main.tsx 파일
  2. 작동 중인 FastAPI 백엔드 (FastAPI 제너레이터로 생성된 것)
  3. Cognito 또는 IAM 인증을 사용하는 API 연결 시 ts#cloudscape-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>,
);

사용 방법

제너레이터 실행

  1. 설치 Nx Console VSCode Plugin 아직 설치하지 않았다면
  2. VSCode에서 Nx 콘솔 열기
  3. 클릭 Generate (UI) "Common Nx Commands" 섹션에서
  4. 검색 @aws/nx-plugin - api-connection
  5. 필수 매개변수 입력
    • 클릭 Generate

    옵션

    매개변수 타입 기본값 설명
    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 생성된 클라이언트 파일 기본적으로 제외

    제너레이터는 또한 웹사이트 인프라에 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>;
    }
    기본 클라이언트 직접 사용 예시 보기

    뮤테이션

    생성된 훅은 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 ? '생성 중...' : '아이템 생성'}
    </button>
    {createItem.isSuccess && (
    <div className="success">
    생성된 아이템 ID: {createItem.data.id}
    </div>
    )}
    {createItem.isError && (
    <div className="error">
    오류: {createItem.error.message}
    </div>
    )}
    </form>
    );
    }

    다양한 뮤테이션 상태에 대한 콜백 추가 가능:

    const createItem = useMutation({
    ...api.createItem.mutationOptions(),
    onSuccess: (data) => {
    console.log('생성된 아이템:', data);
    navigate(`/items/${data.id}`);
    },
    onError: (error) => {
    console.error('아이템 생성 실패:', error);
    },
    onSettled: () => {
    queryClient.invalidateQueries({ queryKey: api.listItems.queryKey() });
    }
    });
    기본 클라이언트 직접 사용 예시 보기

    무한 쿼리 페이지네이션

    cursor 파라미터를 입력으로 받는 엔드포인트의 경우, TanStack Query의 useInfiniteQuery 훅을 사용한 무한 쿼리 지원을 제공합니다. “더 보기” 또는 무한 스크롤 기능 구현이 용이합니다.

    import { useInfiniteQuery } from '@tanstack/react-query';
    import { useMyApi } from './hooks/useMyApi';
    function ItemList() {
    const api = useMyApi();
    const items = useInfiniteQuery({
    ...api.listItems.infiniteQueryOptions({
    limit: 10,
    }, {
    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
    ? '더 불러오는 중...'
    : items.hasNextPage
    ? '더 보기'
    : '더 이상 항목 없음'}
    </button>
    </div>
    );
    }

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

    기본 클라이언트 직접 사용 예시 보기

    오류 처리

    통합에는 타입화된 오류 응답이 내장되어 있습니다. OpenAPI 명세에 정의된 가능한 오류 응답을 캡슐화하는 <operation-name>Error 타입이 생성됩니다. 각 오류는 statuserror 속성을 가지며, 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>잘못된 입력:</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>권한 없음:</h2>
    <p>{createItem.error.error.reason}</p>
    </div>
    );
    case 500:
    case 502:
    return (
    <div>
    <h2>서버 오류:</h2>
    <p>{createItem.error.error.message}</p>
    <p>추적 ID: {createItem.error.error.traceId}</p>
    </div>
    );
    }
    }
    return <button onClick={handleClick}>아이템 생성</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>
    );
    }

    스트림 상태 확인을 위해 isLoadingfetchStatus 속성을 사용할 수 있습니다. 스트림 생명주기:

    1. 스트리밍 시작 HTTP 요청 전송

      • isLoading: true
      • fetchStatus: 'fetching'
      • data: undefined
    2. 첫 번째 청크 수신

      • isLoading: false
      • fetchStatus: 'fetching'
      • data: 첫 번째 청크 포함 배열
    3. 후속 청크 수신

      • isLoading: false
      • fetchStatus: 'fetching'
      • data: 수신 즉시 업데이트
    4. 스트림 완료

      • isLoading: false
      • fetchStatus: 'idle'
      • data: 모든 청크 포함 배열
    기본 클라이언트 직접 사용 예시 보기

    생성 코드 커스터마이징

    쿼리 및 뮤테이션

    기본적으로 FastAPI의 PUT, POST, PATCH, DELETE HTTP 메서드는 뮤테이션으로, 나머지는 쿼리로 간주됩니다.

    x-queryx-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-cursorFalse로 설정:

    @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 태그 기반으로 자동 구성됩니다. 관련 작업을 쉽게 찾고 관리할 수 있도록 도와줍니다.

    예시:

    items.py
    @app.get(
    "/items",
    tags=["items"],
    )
    def list():
    # ...
    @app.post(
    "/items",
    tags=["items"],
    )
    def create(item: Item):
    # ...
    users.py
    @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());
    const handleCreateItem = () => {
    createItem.mutate({ name: 'New Item' });
    };
    return (
    <div>
    <h2>아이템</h2>
    <ul>
    {items.data?.map(item => (
    <li key={item.id}>{item.name}</li>
    ))}
    </ul>
    <button onClick={handleCreateItem}>아이템 추가</button>
    <h2>사용자</h2>
    <ul>
    {users.data?.map(user => (
    <li key={user.id}>{user.name}</li>
    ))}
    </ul>
    </div>
    );
    }

    이 그룹화는 API 호출 조직화와 IDE 코드 완성에 도움을 줍니다.

    기본 클라이언트 직접 사용 예시 보기

    오류

    커스텀 예외 클래스, 예외 핸들러, 응답 모델 정의로 오류 응답을 커스터마이징할 수 있습니다. 생성된 클라이언트는 자동으로 이 오류 유형을 처리합니다.

    커스텀 오류 모델 정의

    Pydantic으로 오류 모델 정의:

    models.py
    from pydantic import BaseModel
    class ErrorDetails(BaseModel):
    message: str
    class ValidationError(BaseModel):
    message: str
    field_errors: list[str]

    커스텀 예외 생성

    다양한 오류 시나리오용 예외 클래스 생성:

    exceptions.py
    class NotFoundException(Exception):
    def __init__(self, message: str):
    self.message = message
    class ValidationException(Exception):
    def __init__(self, details: ValidationError):
    self.details = details

    예외 핸들러 추가

    예외를 HTTP 응답으로 변환:

    main.py
    from fastapi import Request
    from 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(),
    )

    응답 모델 지정

    엔드포인트 정의 시 오류 상태 코드별 응답 모델 지정:

    main.py
    @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('찾을 수 없음:', error.error);
    break;
    case 500:
    console.error('서버 오류:', error.error.message);
    break;
    }
    }
    });
    const createItem = useMutation({
    ...api.createItem.mutationOptions(),
    onError: (error) => {
    switch (error.status) {
    case 400:
    console.error('유효성 오류:', error.error.message);
    break;
    case 403:
    console.error('권한 없음:', 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 { 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={`추적 ID: ${err.error.traceId}`}
    />
    );
    default:
    return <ErrorMessage message="알 수 없는 오류 발생" />;
    }
    }
    return (
    <ul>
    {items.data.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('아이템 삭제 실패:', err);
    },
    onSettled: () => {
    queryClient.invalidateQueries({ queryKey: api.listItems.queryKey() });
    },
    });
    if (itemsQuery.isLoading) {
    return <LoadingSpinner />;
    }
    if (itemsQuery.isError) {
    return <ErrorMessage message="아이템 불러오기 실패" />;
    }
    return (
    <ul>
    {itemsQuery.data.map((item) => (
    <li key={item.id}>
    {item.name}
    <button
    onClick={() => deleteMutation.mutate(item.id)}
    disabled={deleteMutation.isPending}
    >
    {deleteMutation.isPending ? '삭제 중...' : '삭제'}
    </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(`생성된 아이템 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="잘못된 입력"
    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 ? '생성 중...' : '아이템 생성'}
    </button>
    </form>
    );
    }
    기본 클라이언트 직접 사용 예시 보기

    타입은 FastAPI의 OpenAPI 스키마에서 자동 생성되며, API 변경 사항은 빌드 후 프론트엔드 코드에 반영됩니다.