
【개인 개발】Vercel AI SDK × Multi-Agent로 미국 주식·가상화폐 포트폴리오를 분석하는 앱을 만들었다
요약
Vercel AI SDK와 Multi-Agent 기술을 활용하여 미국 주식 및 가상화폐 포트폴리오를 통합 분석하는 'PortfolioX' 앱 개발 사례를 소개합니다. 실시간 시장 데이터와 5명의 AI 투자자 에이전트를 통해 다각적인 자산 진단을 제공합니다.
핵심 포인트
- Vercel AI SDK와 Multi-Agent를 이용한 자산 관리 앱 구현
- 실시간 시장 데이터와 AI 챗봇이 통합된 대시보드 제공
- 5명의 AI 에이전트를 통한 개별 종목 및 포트폴리오 진단
- Next.js, Tailwind CSS, Supabase 등 최신 기술 스택 활용
이번에는 개인 개발로서 제작한 AI 탑재형 자산 관리·분석 앱 「PortfolioX」에 대해 소개합니다.
저는 평소 미국 주식 ETF와 비트코인을 중심으로 투자를 하고 있습니다만, 기존 앱에는 다음과 같은 불만이 있었습니다.
관리의 분단: 주식 전용·가상화폐 전용 앱이 나누어져 있어, 자산 전체를 매달 수동으로 합산해야 했다 -
AI 분석의 부족함: 기존의 AI 기능은 범용적인 채팅에 머물러 있으며, 「내가 보유한 구체적인 종목」에 기반한 깊은 분석이나 저명한 투자자의 관점에서 논의할 수 있는 서비스는 없었다
그래서 이번에 실시간 시장 데이터·차트·AI 챗봇을 하나의 대시보드인 PortfolioX를 개발했습니다.
미국 주식·ETF·가상화폐를 한 화면에서 일원 관리하며, AI가 다각적인 투자 판단을 지원하는 통합 플랫폼입니다.
주식·가상화폐의 실시간 검색·추가. 포트폴리오의 총자산액과 역사적 추이를 그래프로 표시.

5명의 AI 투자자가 보유 종목 구성과 최신 시장 데이터를 읽어 들여, 개별 진단을 실행.

일본어·영어 2개 언어에 대응하며, 사용자는 원클릭으로 표시 언어를 전환 가능.

라이트·다크 모드 외에 질감에 신경을 쓴 「샴페인 브라운」의 3가지 테마를 탑재.

Framework:Next.js 16+ (App Router) -
Library:React 19 / TypeScript -
Styling:Tailwind CSS v4 / shadcn/ui -
State Management:TanStack Query (React Query)
BaaS:Supabase -
Hosting:Vercel
AI Stack:Vercel AI SDK + OpenRouter (free model) + DeepSeek (deepseek-v4) -
RAG / Search:Tavily API -
Financial Data:Yahoo Finance API / TradingView Widgets / CoinGecko Widgets
금융 데이터의 즉시성을 중시하여, 첫 로딩이 느린 SPA가 아니라 SSR(Server-Side Rendering)을 통해 첫 화면 표시를 고속화할 수 있는 Next.js를 채택했습니다.
가격, 날짜, 종목 정보 등 다각적인 데이터를 엄격하게 다룰 필요가 있었기 때문에, 정적 타이핑을 통해 타입 불일치 등의 에러를 컴파일 시점에 검출할 수 있어 금융 시스템에서 치명적인 예기치 못한 버그를 미연에 방지할 수 있는 TypeScript를 도입했습니다.
Vercel AI SDK: Next.js와의 연계가 가장 매끄럽고, DeepSeek와 OpenRouter의 전환이 한 줄로 끝나기 때문에 편리합니다.
DeepSeek: 압도적인 저비용에 더해, 개발사(DeepSeek)가 대형 퀀트 펀드이기 때문에 금융 분석에 강합니다. 또한, AI 투자 콘테스트 「Alpha Arena」에서도 주요 LLM(Large Language Model)을 제치고 최상위권 성적을 거두고 있어, 금융 도메인에 대한 높은 적성 때문에 채택했습니다.
OpenRouter: 무료 프레임을 제공할 뿐만 아니라, 단일 API를 통해 다양한 경량·고성능 모델에 접근할 수 있어 개인 개발의 검증 비용을 최소한으로 억제할 수 있기 때문입니다.
Next.js와의 궁합이 좋아 SSR을 용이하게 실현할 수 있다는 점, GitHub 연동을 통한 자동 배포로 개발 사이클을 가속할 수 있다는 점, Vercel AI SDK와의 친화성이 높아 저지연 AI 스트리밍 환경을 구축할 수 있다는 점 때문에 채택했습니다.
시장의 가격 변동을 화면에 반영하는 실시간 동기화 기능이나 인증 기능을 최소한의 개발 비용으로 일원 관리할 수 있기 때문에 Supabase를 채택했습니다.
본 앱의 기술적인 핵심은, 스타일이 다른 5명의 저명한 투자자를 모사한 AI Agent 중 사용자가 1~2명을 선택하고, 동일한 종목 데이터를 바탕으로 각각 독립적인 분석을 수행하며, 2명을 선택했을 때는 코디네이터가 총괄하는 Multi-Agent 아키텍처입니다.
구현에는 Vercel AI SDK의 generateText와 Tool Calling을 조합하여, **SSE (Server-Sent Events)**로 클라이언트에 실시간 전송하고 있습니다.
| 페르소나 | 이름 | 투자 스타일 |
|---|---|---|
buffett | Warren Buffett | 저평가 우량주 · 경제적 해자 · 장기 보유 |
lynch | Peter Lynch | 성장주 · PEG 비율 · 텐배거(Ten-bagger) 겨냥 |
wood | Cathie Wood | 파괴적 혁신 · TAM 중시 |
burry | Michael Burry | 역발상 투자 · 저평가 자산 · 버블 탐지 |
dalio | Ray Dalio | 매크로 사이클 · 지정학적 리스크 |
각 페르소나는 features/ai/lib/prompts.ts의 PERSONA_PROMPTS에 독립된 시스템 프롬프트 (System Prompt)로 정의되어 있으며, JSON 형식의 출력 (verdict / score / points / buyRange)을 강제하는 구조로 되어 있습니다.
// features/ai/lib/prompts.ts
export const PERSONA_PROMPTS: Record<string, string> = {
buffett: `You are Warren Buffett, the Oracle of Omaha.
...
순차적으로 실행하면 Agent의 수만큼 대기 시간이 쌓이지만, Promise.all을 통해 병렬화함으로써 가장 느린 1개 Agent의 처리 시간 내에 완료할 수 있습니다.
// app/api/ai-analysis/route.ts
import { deepseek } from "@ai-sdk/deepseek";
import { generateText, stepCountIs } from "ai";
...
Agent는 분석 중에 다음 3가지 도구 (Tool)를 자율적으로 호출합니다.
| 도구 | 데이터 소스 | 취득 내용 |
|---|---|---|
getStockPrice | Yahoo Finance | 실시간 주가 · 통화 |
getFinancials | Yahoo Finance | PER · PBR · ROE · FCF 등 재무 지표 |
getNews | Tavily API | 최근 뉴스 (최대 5건) |
// features/ai/lib/getStockPrice.ts
import { tool } from "ai";
import { z } from "zod";
...
stopWhen: stepCountIs(5)를 통해 Tool → LLM → Tool의 루프를 최대 5단계 (Step)로 제한하고 있습니다. 이를 통해 무한 루프로 인한 비용 폭발을 방지하면서도, 필요한 데이터를 충분히 수집할 수 있는 균형을 실현했습니다.
Server Actions 대신 Route Handler + SSE를 채택한 이유는, Agent가 완료되는 순서대로 클라이언트에 즉시 알림을 보낼 수 있기 때문입니다. 모든 Agent의 완료를 기다리지 않고 결과를 표시할 수 있어 체감 속도가 대폭 향상됩니다.
// app/api/ai-analysis/route.ts
export async function POST(request: Request) {
const stream = new ReadableStream({
...
클라이언트 측은 fetch + ReadableStream으로 SSE를 수신하며, agent1_done → agent2_done → coordinator_done 각 이벤트가 도착할 때마다 해당 카드를 단계적으로 표시합니다.
[클라이언트] [서버]
| |
|── POST /api/ai-analysis ───────>|
...
처음에는 Next.js의 관습에 따라 파일을 '종류'별로 분류하는 구성을 채택했습니다.
app/
components/
├── auth/
...
언뜻 보기에는 정돈되어 보이지만, 실제로 개발을 진행하다 보니 "자산 관리 기능을 수정하기 위해 components · hooks · lib · server · types의 5개 디렉토리를 왔다 갔다 하는" 상황이 일상화되었습니다.
features/
├── ai/ ← AI 분석 기능과 관련된 모든 것을 여기에 집약
│ ├── components/
...
설계 원칙: app/ 페이지는 얇은 오케스트레이션 계층(Orchestration Layer) 역할에 충실하며, 로직과 UI는 모두 features/ 하위에 배치한다.
| 관점 | Artifact 형 (변경 전) | Feature-Sliced 형 (변경 후) |
|---|---|---|
| 파일 검색 | 기능마다 5개의 디렉토리를 오가야 함 | features/ai/만 열면 모든 것이 있음 |
| 영향 범위 파악 | 변경 사항이 다른 기능에 파급될지 불분명함 | Feature를 넘나들지 않는 변경은 안전함 |
| 삭제·추가 | 관련 파일이 산재해 있어 누락되기 쉬움 | Feature 디렉토리째로 삭제·복제할 수 있음 |
「1 Feature = 1 디렉토리」를 철저히 함으로써, 어떤 파일을 보면 무엇을 알 수 있는지가 명확해졌고, 버그 발생 위치를 특정하는 것도 빨라졌습니다.
또한, "이 구성이 Harness Engineering과 궁합이 좋지 않은가?" 라는 생각을 수정 후에 의식하게 되었습니다.
리포지토리 루트
├── CLAUDE.md ← AI가 가장 먼저 읽는 전체상·하드 룰(Hard Rule)·문서 맵
├── INIT_CONTRACT.md ← 새 세션 시작 시의 "사전 체크"
...
이유는 다음 세 가지가 있습니다.
① 스코프(Scope)를 명시할 수 있다
CLAUDE.md에 "범위 외의 파일은 변경하지 않는다"라는 제약을 적을 수 있어, AI 에이전트에게 "이번 태스크는 features/assets/뿐이다"라고 명확하게 지시할 수 있습니다. Artifact 형이라면 "components/assets/와 hooks/와 lib/와 server/를 한꺼번에 변경해 주세요"라는 모호한 지시가 됩니다.
② 각 Feature에 ARCHITECTURE.md를 배치한다
features/
├── ai/ARCHITECTURE.md ← AI 기능의 책임·외부 의존성·변경 금지 구역을 기재
├── assets/ARCHITECTURE.md
...
AI 에이전트가 태스크에 임하기 전에 이 문서를 읽음으로써, 설계 의도를 벗어난 구현을 방지할 수 있습니다.
③ 변경의 영향 범위가 Feature 내로 한정된다
Feature를 넘나드는 의존성을 갖지 않는 설계로 되어 있기 때문에, AI 에이전트가 features/ai/를 다시 써도 features/assets/에는 영향을 주지 않습니다. 이를 통해 AI의 변경이 디그레이데이션(Degradation)을 일으킬 리스크를 구조적으로 최소화할 수 있습니다.
합계: 약 3개월
| 기간 | 내용 |
|---|---|
| 첫 2주 | 설계 (User Story / Flow / UI 디자인) |
| 나머지 2.5개월 | 구현·디버깅 |
어느 날 갑자기 /api/yahoofinance/search가 500 에러를 반환하기 시작했습니다.
원인: 조사 결과 Yahoo Finance가 응답 구조를 변경하였고, 사용 중이던 yahoo-finance2 v3.13.2가 스키마 불일치로 인해 크래시(Crash)가 발생했습니다.
대처: v3.15.2로 업그레이드하고, 유효성 검사(Validation) 에러 발생 시 빈 배열을 반환하는 폴백(Fallback)도 추가했습니다.
자산 페이지를 열 때마다 Tavily 뉴스 API를 호출하는 설계였기 때문에, 페이지 전환 시마다 비용이 발생하고 있었습니다.
채택한 해결책: Supabase의 news_cache 테이블을 사용하여 JST 날짜 단위로 캐싱
최초 액세스 또는 날짜 변경 후 → Tavily를 호출하여 캐시 쓰기
2회차 이후 (동일 날짜) → 캐시에서 반환 (Tavily 호출 없음)
수동 업데이트 버튼 → force=true로 Tavily를 강제 호출하여 캐시 갱신
locale=ja (일본어) 사용자에게 AI가 의도하지 않은 다른 언어로 응답하는 버그가 발생했습니다.
원인: 시스템 프롬프트(System Prompt)의 구조 헤더를 영어 이외의 언어로 작성했기 때문에, 모델이 언어 지시보다 프롬프트의 언어 스타일을 우선하여 학습해 버렸습니다.
대처:
// 수정 전: 구조 헤더가 영어가 아님 → 모델이 해당 언어로 판단
`## HOLDINGS SNAPSHOT(타 언어 표기)
${langInstruction}`
...
무료 모델은 언어 지시보다 "프롬프트 내의 언어 스타일"을 우선시하는 경향이 있습니다. system prompt의 구조 부분은 항상 영어로 작성하고, 언어 지시는 CRITICAL: 으로 강조한다는 규칙을 GOTCHAS.md에 기록하여, 이후 세션에서 재발을 방지하고 있습니다.
유명 투자자의 포트폴리오 추적: 유명 투자자나 KOL이 "지금 무엇을 매매하고 있는지"를 앱 내에서 확인할 수 있는 기능
AI 기능의 심화: 현재의 진단 기능에 더해, 더욱 깊이 있는 시장 예측이나 리스크 알림 (Risk Alert) 기능을 모색 중
첫 본격적인 풀스택 (Full-stack) 개인 개발이었지만, 자신의 아이디어가 형태를 갖추어 가는 과정은 최고로 즐거운 시간이었습니다. UI 설계부터 AI의 프롬프트 조정까지, 모든 것을 혼자 결정해야 하는 어려움은 있었지만, 그만큼 많은 배움이 있었습니다.
특히 Multi-Agent의 병렬 스트리밍 (Parallel Streaming) 설계는 고생했습니다. "왜 순차적으로 실행하면 느려지는가", "SSE (Server-Sent Events)로 어떻게 흘려보내야 하는가"라며 몇 번이나 막히기도 했지만, 작동하는 순간의 성취감은 남달랐습니다.
1년 후에 이 코드를 다시 보았을 때 "정말 미숙한 코드구나"라며 웃을 수 있을 정도로, 앞으로도 학습과 개발을 계속해 나가고 싶습니다.
끝까지 읽어주셔서 감사합니다!
AI 자동 생성 콘텐츠
본 콘텐츠는 Qiita AI의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기