이전 3강에서 FastAPI Todo 백엔드를 구축하고, /openapi.json을 활용해 swagger-typescript-api로 TypeScript 클라이언트 코드를 자동 생성하는 과정을 살펴봤습니다. 이번 강의에서는 해당 클라이언트 코드를 Vite 기반 React 프로젝트에 연동해 실제 화면에서 Todo 데이터를 표시하고 조작하는 방법을 안내합니다.
1. Vite 기반 React 프로젝트 생성
Vite는 경량화된 번들러이자 개발 서버입니다. React를 TypeScript와 함께 쓰기에 최적화되어 있습니다.
npm create vite@latest my-react-app -- --template react-ts
- my-react-app: 프로젝트 폴더명
- --template react-ts: TypeScript 환경의 React 템플릿 사용
프로젝트가 생성된 후 디렉터리에 진입해 아래 명령어로 의존성을 설치해 주세요.
cd my-react-app
npm install
npm i axios //Axios기반의 클라이언트를 생성할 예정이므로 설치
주의사항: Yarn을 사용 중이라면, yarn create vite my-react-app --template react-ts 식으로 진행할 수 있습니다.
예시 폴더 구조
my-react-app/
├── public/
├── src/
│ ├── App.tsx
│ ├── main.tsx
│ └── api/ # 클라이언트 코드를 저장할 폴더
├── index.html
├── package.json
└── tsconfig.json
2. FastAPI Todo 백엔드와 연동할 클라이언트 가져오기
이전 강의(3강)에서 아래와 같이 클라이언트를 생성했다고 가정합니다.
npx swagger-typescript-api \
-p http://127.0.0.1:8000/openapi.json \
-r \
-o ./src/api/ \
-n ApiClient \
--axios \
--modular \
--extract-request-body \
--extract-response-body \
--extract-response-error
- --output "./src/api": 생성된 파일을 Vite 프로젝트의 src/api 폴더로 바로 저장.
- --name "TodoApiClient.ts": TodoApiClient.ts라는 파일명으로 클라이언트 파일 생성
- 자세한 옵션은 공식문서 참조
그러나 개발과정에서 빈번히 쓰이는 명령이므로 package.json 에 아래와 같은 예시로 스크립트를 구성하는 것이 효과적입니다.
"scripts": {
"gen-api-client": "npx swagger-typescript-api -p http://127.0.0.1:8000/openapi.json -r -o ./src/api/ -n ApiClient --axios --modular --extract-request-body --extract-response-body --extract-response-error"
},
생성 후, Vite 프로젝트 내부 구조는 다음과 같이 업데이트됩니다.
src/api
├── Todos.ts
├── data-contracts.ts
└── http-client.ts
...
3. React에서 Todo API 사용하기
이제 자동 생성된 Todo API 클라이언트를 가져와서, Todo 목록을 화면에 렌더링하고 CRUD를 수행하는 예시를 만들어봅시다.
1) TodoList 컴포넌트 예시
아래 예시는 TodoList.tsx라는 컴포넌트를 새로 만들어, Todo 목록을 불러와 렌더링하는 예시입니다.
// frontend/src/components/TodoList.tsx
import React, { useEffect, useState } from 'react';
import { Todos } from '../api/Todos';
import type { TodoItem } from '../api/data-contracts';
const TodoList: React.FC = () => {
const [todos, setTodos] = useState<TodoItem[]>([]);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
const fetchTodos = async () => {
// baseURL은 실제 배포 환경에 맞게 수정 가능
const api = new Todos({ baseURL: 'http://127.0.0.1:8000' });
try {
const response = await api.getTodosTodosGet();
// 생성된 코드가 AxiosResponse<T>를 반환하므로, 실제 데이터는 response.data로 접근
setTodos(response.data);
} catch (err) {
setError('Failed to fetch todos');
}
};
fetchTodos();
}, []);
if (error) return <div>{error}</div>;
return (
<div>
<h1 className='text-xl font-bold'>Todo List</h1>
<ul>
{todos.map((item) => (
<li key={item.id}>
<strong>{item.title}</strong>
{item.description && ` - ${item.description}`}
{item.done ? ' ✅' : ' ❌'}
</li>
))}
</ul>
</div>
);
};
export default TodoList;
- import { Todos } ...: Todos.ts 파일에서 자동 생성된 클래스(Todos)를 사용합니다.
- TodoItem: data-contracts.ts 내에 정의된 인터페이스로, API가 어떤 필드를 반환하는지 한눈에 확인할 수 있습니다.
- response.data: Todos 클래스의 메서드는 Promise<AxiosResponse<T>>를 반환하므로, 실제 데이터는 response.data에 위치합니다.
주의사항: 실제 배포 환경에서는 baseURL을 .env(예: VITE_API_BASE_URL) 등 환경 변수를 통해 주입할 수 있습니다.
2) TodoForm 컴포넌트 (생성 예시)
새로운 Todo를 만드는 폼 UI 예시입니다. 데이터가 생성되면 createTodoTodosPost 메서드를 사용해 FastAPI 서버에 전달합니다.
// frontend/src/components/TodoForm.tsx
import React, { useState } from 'react';
import { Todos } from '../api/Todos';
import type { TodoItem } from '../api/data-contracts';
const TodoForm: React.FC = () => {
const [title, setTitle] = useState('');
const [description, setDescription] = useState('');
const api = new Todos({ baseURL: 'http://127.0.0.1:8000' });
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
const newTodo: TodoItem = {
id: Math.floor(Math.random() * 100000),
title,
description,
done: false,
};
try {
const response = await api.createTodoTodosPost(newTodo);
alert(`Created Todo: ${response.data.title}`);
setTitle('');
setDescription('');
} catch (err) {
alert('Failed to create Todo');
}
};
return (
<form onSubmit={handleSubmit}>
<h1 className='text-xl font-bold'>Create New Todo</h1>
<div>
<label htmlFor="title">Title:</label>
<input
id="title"
value={title}
onChange={(e) => setTitle(e.target.value)}
required
/>
</div>
<div>
<label htmlFor="description">Description:</label>
<input
id="description"
value={description}
onChange={(e) => setDescription(e.target.value)}
/>
</div>
<button type="submit">Add Todo</button>
</form>
);
};
export default TodoForm;
3) App.tsx에서 컴포넌트 사용하기
// frontend/src/App.tsx
import React from 'react';
import TodoList from './components/TodoList';
import TodoForm from './components/TodoForm';
const App: React.FC = () => {
return (
<div className='flex flex-col justify-center'>
<h1 className='text-3xl font-bold'>My Todo App</h1>
<TodoForm />
<TodoList />
</div>
);
};
export default App;
프로젝트 루트(예: main.tsx)에서 App을 렌더링하도록 설정해두었다면, 이제 TodoList의 목록을 확인할 수 있습니다.
아래 예시는 tailwind css 를 조금 적용한 내용이며, vite 기본으로 할 경우 조금 다르게 나올 수 있습니다.
4. Todo 생성 & 업데이트 UI 구현 (선택 사항)
1) TodoForm (새 Todo 작성)
import React, { useState } from 'react';
import { TodoApi, TodoItem } from '../api/TodoApiClient';
const TodoForm: React.FC = () => {
const [title, setTitle] = useState<string>('');
const [description, setDescription] = useState<string>('');
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
const api = new TodoApi({ baseUrl: 'http://127.0.0.1:8000' });
const newTodo: TodoItem = {
id: Math.floor(Math.random() * 100000),
title,
description,
done: false,
};
try {
await api.createTodo(newTodo);
alert('Todo created successfully!');
setTitle('');
setDescription('');
} catch {
alert('Failed to create todo.');
}
};
return (
<form onSubmit={handleSubmit}>
<h2>Create New Todo</h2>
<div>
<label>Title:</label>
<input value={title} onChange={(e) => setTitle(e.target.value)} />
</div>
<div>
<label>Description:</label>
<input value={description} onChange={(e) => setDescription(e.target.value)} />
</div>
<button type="submit">Add Todo</button>
</form>
);
};
export default TodoForm;
- handleSubmit: form 제출 시 createTodo() API를 호출하여 새 Todo를 생성.
- ID 생성: 실제로는 서버에서 ID를 생성하거나, DB 내 오토인크리먼트를 사용하는 편이 좋습니다. 여기서는 데모 목적상 임의로 ID를 부여.
2) App.tsx에서 결합
import React from 'react';
import TodoList from './components/TodoList';
import TodoForm from './components/TodoForm';
function App() {
return (
<div>
<h1>My Todo App</h1>
<TodoForm />
<TodoList />
</div>
);
}
export default App;
출처: https://brotherdan.tistory.com/67 [IT 20년차의 복합 비즈니스 도전:티스토리]
5. 베스트 프랙티스 & 협업 시 고려사항
1) 디렉터리 구조 분리
- 자동 생성 코드(TodoApiClient.ts)와 수작업 코드(Form, List 등)를 구분하여 관리하세요.
- 예: src/api 폴더에는 자동 생성 코드만 두고, src/services 폴더에서 API 호출 로직을 래핑할 수도 있습니다.
2) API 변경 & 자동화
- 백엔드(FastAPI)에서 스펙이 변경되면, swagger-typescript-api를 주기적으로 실행하여 최신 인터페이스를 반영합니다.
- CI/CD 파이프라인에 이를 추가해 자동화하면 문서와 코드가 늘 일치하게 됩니다.
3) 에러 처리 & 상태 관리
- React Query(tanstack-query)나 SWR 같은 라이브러리를 쓰면 캐싱, 에러 처리 등이 용이합니다.
- 대규모 애플리케이션이라면 전역 상태(예: Redux, Zustand)와 결합하는 전략도 고민해볼 만합니다.
6. 마무리 & 다음 강의 예고
이번 강의에서는 Vite 기반의 React 프로젝트에 자동 생성된 TodoApi를 연동하여, 프론트엔드 UI와 백엔드 간 타입 안전한 통신을 구현해봤습니다. TodoList와 TodoForm을 통해 CRUD 로직을 간단히 시연했지만, 팀 협업과 확장을 고려하면 자동화된 코드 생성의 이점을 더 크게 체감하실 수 있을 것입니다.
다음 5강에서는 프로젝트를 실제 운영 환경에 배포할 때 유의해야 할 확장 및 배포 전략, 대규모 팀/조직에서 적용할 때의 고급 팁을 다룰 예정이니, 많은 관심 부탁드립니다.