Genkit을 사용해 AI 기반 React 앱 만들기
Source: Dev.to
Introduction
지난 몇 달 동안 다양한 AI 프레임워크를 사용해 보았고, Genkit이 프로덕션 애플리케이션에 가장 적합한 선택이 되었습니다. 구글의 오픈소스 프레임워크인 Genkit은 AI 워크플로우를 구축하기 위한 도구이며, LLM을 실제 애플리케이션에 연결할 때 발생하는 많은 문제들을 해결해 줍니다.
프로덕션 앱에 AI 기능을 넣어보았다면 아마 다음과 같은 문제들을 겪었을 것입니다:
- 프롬프트 스파게티 – 프롬프트가 간단한 문자열로 시작했다가, 컨텍스트 주입이 포함된 다중 라인 템플릿으로 성장하고, 결국 명확한 소유자가 없는 서비스 전역에 흩어집니다.
- 디버깅 블랙홀 – AI 파이프라인에서 무언가 잘못됩니다. 프롬프트 때문인가요? 모델 때문인가요? 전달한 컨텍스트 때문인가요? 적절한 관찰성이 없으면 문제를 추적하는 것이 악몽이 됩니다.
- 통합 혼란 – 외부 API를 호출하고, 그 데이터를 LLM에 전달하고, 후처리를 수행한 뒤 구조화된 응답을 반환해야 합니다. 깨끗한 추상화가 없으면 금세 지저분해집니다.
Genkit은 이러한 문제들을 flow 원시를 제공함으로써 해결합니다—여러 단계(API 호출, LLM 호출, 변환)를 포함할 수 있는 함수라고 생각하면 되며, 내장 로깅과 개발 UI를 통해 각 단계를 검사할 수 있습니다.
우리는 도시 분석기를 만들 것입니다. 이 분석기는:
- 프론트엔드에서 도시 이름을 받습니다.
- 외부 API에서 실제 날씨 데이터를 가져옵니다.
- 해당 데이터를 Gemini에 전달해 분석합니다.
- 원시 데이터와 AI가 생성한 인사이트를 모두 반환합니다.
혁신적인 내용은 아니지만, 실제 AI 기능을 구현할 때 대부분 사용하게 될 패턴을 보여줍니다.
Backend Setup
mkdir genkit-backend && cd genkit-backend
npm init -y
npm install @genkit-ai/core @genkit-ai/flow @genkit-ai/google-ai express cors
npm install -D typescript ts-node @types/node @types/express
Flow Implementation (src/flows/analyzeCity.ts)
// src/flows/analyzeCity.ts
import { flow } from '@genkit-ai/flow';
import { genkit } from '@genkit-ai/core';
import { googleAI } from '@genkit-ai/google-ai';
const ai = genkit({
plugins: [googleAI()],
});
interface WeatherData {
temp: number;
humidity: number;
wind_speed: number;
}
async function fetchWeather(city: string): Promise<WeatherData> {
const response = await fetch(
`https://api.api-ninjas.com/v1/weather?city=${encodeURIComponent(city)}`,
{
headers: { 'X-Api-Key': process.env.WEATHER_API_KEY! },
}
);
if (!response.ok) {
throw new Error(`Weather API failed: ${response.status}`);
}
return response.json();
}
export const analyzeCityFlow = flow(
'analyzeCityFlow',
async ({ city }: { city: string }) => {
// Step 1: Get external data
const weather = await fetchWeather(city);
// Step 2: Generate AI analysis
const result = await ai.generate({
model: 'google/gemini-pro',
prompt: `Given the current weather in ${city}:
- Temperature: ${weather.temp}°C
- Humidity: ${weather.humidity}%
- Wind Speed: ${weather.wind_speed} km/h
Write 2‑3 sentences describing what it's like there right now and any practical suggestions for someone visiting today.`,
});
return {
city,
weather,
analysis: result.text(),
};
}
);
Express Server (src/server.ts)
// src/server.ts
import express from 'express';
import cors from 'cors';
import { analyzeCityFlow } from './flows/analyzeCity';
const app = express();
app.use(cors());
app.use(express.json());
app.post('/api/analyze-city', async (req, res) => {
try {
const result = await analyzeCityFlow(req.body);
res.json(result);
} catch (error) {
console.error('Flow failed:', error);
res.status(500).json({ error: 'Analysis failed' });
}
});
const PORT = process.env.PORT || 4000;
app.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
});
You can test flows in isolation using:
npx genkit dev
This spins up a local UI where you can invoke flows with test inputs and inspect each step—saving hours of debugging.
Frontend Setup
npm create vite@latest genkit-react -- --template react-ts
cd genkit-react
npm install
React App (src/App.tsx)
// src/App.tsx
import { useState } from 'react';
interface AnalysisResult {
city: string;
weather: {
temp: number;
humidity: number;
wind_speed: number;
};
analysis: string;
}
function App() {
const [city, setCity] = useState('');
const [result, setResult] = useState<AnalysisResult | null>(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const handleAnalyze = async () => {
if (!city.trim()) return;
setLoading(true);
setError(null);
try {
const response = await fetch('http://localhost:4000/api/analyze-city', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ city }),
});
if (!response.ok) throw new Error('Request failed');
setResult(await response.json());
} catch {
setError('Failed to analyze city. Check if the backend is running.');
} finally {
setLoading(false);
}
};
return (
<div style={{ maxWidth: 600, margin: '0 auto', padding: 20 }}>
<h2>City Weather Analyzer</h2>
<div style={{ display: 'flex', gap: 8, marginBottom: 12 }}>
<input
value={city}
onChange={e => setCity(e.target.value)}
placeholder="Enter a city name"
onKeyDown={e => e.key === 'Enter' && handleAnalyze()}
style={{ flex: 1, padding: 8 }}
/>
<button onClick={handleAnalyze} disabled={loading}>
{loading ? 'Analyzing...' : 'Analyze'}
</button>
</div>
{error && <p style={{ color: 'red' }}>{error}</p>}
{result && (
<div>
<h3>{result.city}</h3>
<p>{result.analysis}</p>
<pre>{JSON.stringify(result.weather, null, 2)}</pre>
</div>
)}
</div>
);
}
export default App;
Why This Setup Works
Separation of Concerns
The React app knows nothing about LLMs or external APIs—it simply calls an endpoint and renders the response. Swapping Gemini for GPT‑4 or changing the data source requires no frontend changes.
Testability
Flows are just async functions. They can be unit‑tested, mocked, and verified independently of the UI.
Observability Out of the Box
The Genkit dev UI shows exactly what went into each step and what came out, making production debugging feasible.
Incremental Complexity
Start with a single‑step flow and add steps as needed (caching, moderation, etc.). The architecture scales with your requirements.
Where to Go From Here
This example barely scratches the surface. In a production setting you’d likely add:
- Input validation and rate limiting
- Caching for external API calls
- Streaming responses for better UX
- Authentication
- Robust error handling with retries
Genkit also supports retrieval‑augmented generation (RAG), evaluations, and deployment to Firebase or Cloud Run. See the official Genkit documentation for detailed guidance.
If you’re building AI features into production apps and are tired of ad‑hoc solutions, give Genkit a serious look. The learning curve is minimal for anyone comfortable with TypeScript, and the structure it provides pays dividends as your AI logic grows more complex.