TypeScript AI 에이전트 프레임워크 Mastra: 타입 안전한 에이전트 설계와 프로덕션 적용
솔직히 말하면, 처음 AI 에이전트를 TypeScript로 만들려고 했을 때 꽤 막막했습니다. Python 생태계엔 LangChain, LlamaIndex 같은 검증된 선택지가 즐비한데, JavaScript/TypeScript 쪽은 뭔가 "이걸 써도 괜찮나?" 싶은 불안감이 있었거든요. 타입 안전성은 포기하고 any를 난무시키거나, Python 쪽 코드를 억지로 래핑하는 식이었는데요. 그러다 Mastra를 만났습니다.
Mastra는 Gatsby.js를 공동창업한 팀 출신 개발자들이 설립한 오픈소스 TypeScript AI 에이전트 프레임워크입니다. 2025년 Y Combinator(W25)에서 시작해 2026년 1월 v1.0을 출시했고, 지금은 GitHub 스타 22,000개를 넘기며 TypeScript 개발자 커뮤니티에서 LangChain.js 대안으로 빠르게 자리잡고 있습니다. PayPal은 Mastra로 고객 지원 자동화 에이전트를 구현했고, Elastic은 Elasticsearch와 연동한 에이전트 RAG 파이프라인 도입 사례를 공개했습니다.
이 글에서는 Mastra의 핵심 추상화 6가지를 이해하고, 실제 에이전트를 TypeScript로 타입 안전하게 만드는 방법을 구체적인 코드와 함께 살펴봅니다. TypeScript 중급 이상 개발자를 대상으로 하며, Zod와 pgvector 같은 도구에 대한 기본적인 이해가 있다면 코드 예시를 바로 따라가실 수 있습니다. AI 에이전트 자체가 처음이라면 아래 "AI 에이전트란?" 설명부터 읽으시면 흐름이 잡힐 겁니다.
핵심 개념
Mastra가 제공하는 6개의 프리미티브
Mastra의 설계 철학은 "배터리 포함(batteries-included)"입니다. AI 앱 개발에 필요한 요소들을 각자 조립하는 게 아니라, 하나의 일관된 프레임워크 안에서 해결하도록 설계됐습니다. 그 뼈대가 되는 게 아래 6개의 추상화입니다.
| 프리미티브 | 역할 | 간단한 비유 |
|---|---|---|
| Agent | LLM과 도구를 결합한 자율 실행 단위 | 생각하고 행동하는 직원 |
| Workflow | 다단계 결정론적 파이프라인 | 업무 프로세스 매뉴얼 |
| Tool | 에이전트가 호출하는 외부 기능 | 직원이 쓰는 도구·API |
| Memory | 대화 간 컨텍스트 유지 | 메모장·장기 기억 |
| RAG | 문서 검색 및 임베딩 파이프라인 | 사내 지식 검색 시스템 |
| Evals | 에이전트 품질 평가 | QA 테스트 시나리오 |
이 여섯 가지가 서로 맞물려서 동작하는 구조입니다. Agent가 Tool을 호출하고, Memory로 이전 대화를 기억하고, RAG로 사내 문서를 참조하고, Workflow로 여러 Agent를 조율하는 식이죠.
AI 에이전트란? 단순 API 호출과 다른 점
처음 에이전트를 접하면 "그냥 LLM API 호출하는 거랑 뭐가 달라?"라는 생각이 드는 게 당연합니다. 저도 처음엔 그랬거든요.
핵심 차이는 ReAct(Reasoning + Acting) 루프입니다. 일반적인 LLM 호출은 입력 → 출력으로 끝나지만, 에이전트는 목표를 받으면 스스로 판단합니다. "이 목표를 달성하려면 어떤 Tool을 써야 하지? Tool을 쓴 결과를 보고 다음 행동은 뭘 해야 하지?" 하는 추론과 행동을 반복하죠. 예를 들어 "GitHub에 이슈 만들어줘"라는 요청을 받으면, 에이전트는 이슈 제목을 어떻게 짓고 어떤 레이블을 붙일지 스스로 판단한 후 createIssueTool을 호출합니다. 코드로 제어 흐름을 명시하지 않아도 됩니다.
Workflow vs Agent Workflow는 실행 순서가 미리 정해진 결정론적 파이프라인이고, Agent는 LLM이 스스로 어떤 Tool을 어떤 순서로 쓸지 판단하는 자율적 구조입니다. 예측 가능성이 중요한 비즈니스 로직엔 Workflow가, 유연한 대응이 필요한 인터랙티브 작업엔 Agent가 어울립니다.
Vercel AI SDK + Zod = 타입 안전한 에이전트
Mastra는 내부적으로 Vercel AI SDK를 기반으로 LLM 호출과 스트리밍을 처리하고, Zod로 입력·출력 스키마를 정의합니다.
저도 처음엔 "LLM 호출에 무슨 타입 안전성이 필요해?" 싶었는데, Tool 파라미터 스키마를 Zod로 정의하고 나서 생각이 바뀌었습니다. 에이전트가 Tool을 호출할 때 IDE 자동완성이 동작하고, 잘못된 형태의 응답은 즉시 걸러지니까요. any를 쓰다가 런타임에 터지는 그 당혹감을 겪어보셨다면 공감하실 겁니다.
import { Agent } from '@mastra/core/agent';
import { openai } from '@ai-sdk/openai';
import { createTool } from '@mastra/core/tools';
import { z } from 'zod';
// Tool 정의 — 입력 스키마를 Zod로 명시
const getWeatherTool = createTool({
id: 'get-weather',
description: '도시 이름으로 현재 날씨(기온, 날씨 상태)를 조회합니다. 날씨 관련 질문에만 사용하세요.',
inputSchema: z.object({
city: z.string().describe('날씨를 조회할 도시 이름'),
}),
outputSchema: z.object({
temperature: z.number(),
condition: z.string(),
}),
execute: async ({ context }) => {
// 실제 날씨 API 호출 위치
return { temperature: 22, condition: '맑음' };
},
});
// Agent 정의
const weatherAgent = new Agent({
name: 'weatherAgent',
instructions: '사용자의 날씨 질문에 답변하는 에이전트입니다.',
model: openai('gpt-4o'),
tools: { getWeatherTool },
});Zod란? TypeScript에서 런타임 타입 검증을 위한 라이브러리입니다.
z.object({ city: z.string() })같이 스키마를 선언하면 TypeScript 타입이 자동 생성되고 런타임 검증도 함께 처리됩니다.
Memory: 단기·장기·시맨틱 리콜의 작동 방식
Mastra의 메모리 시스템은 세 가지 계층으로 나뉩니다.
- 단기 메모리: 현재 세션의 최근 N턴 대화 이력을 컨텍스트 창에 직접 포함합니다.
lastMessages: 20이라면 최근 20턴이 매 요청마다 LLM에 전달됩니다. - 장기 메모리: 사용자 선호도, 핵심 정보를 DB에 영구 저장합니다. 에이전트가 중요하다고 판단하는 정보를 명시적으로 저장하거나, 대화 요약 방식으로 축적됩니다.
- 시맨틱 리콜: 현재 질문과 의미적으로 유사한 과거 대화를 임베딩 벡터 검색으로 찾아 컨텍스트에 추가합니다.
topK: 5라면 가장 유사한 5개 대화 세그먼트가 포함됩니다.
PostgreSQL, LibSQL(SQLite 호환), Upstash Redis를 스토리지 백엔드로 지원합니다.
import { Memory } from '@mastra/memory';
import { LibSQLStore } from '@mastra/memory/storage';
const memory = new Memory({
storage: new LibSQLStore({ url: 'file:local.db' }),
options: {
lastMessages: 20, // 단기: 최근 20턴을 컨텍스트에 포함
semanticRecall: {
topK: 5, // 시맨틱: 유사 대화 5개를 벡터 검색으로 추가
messageRange: 3,
},
},
});
const agentWithMemory = new Agent({
name: 'chatAgent',
instructions: '사용자를 기억하는 대화 에이전트입니다.',
model: openai('gpt-4o'),
memory,
});실전 적용
예시 1: GitHub 자동화 에이전트
실무에서 자주 맞닥뜨리는 상황이 있습니다. PR이 열릴 때마다 이슈를 연결하고 담당자에게 알림을 보내는 루틴한 작업 말이죠. 이런 걸 Mastra + GitHub Tool 조합으로 비교적 빠르게 구현할 수 있습니다.
처음엔 Tool 하나에 이슈 생성, PR 조회, 코멘트 작성을 다 때려 넣었는데, 나중에 에이전트가 어떤 Tool을 써야 할지 헷갈려하는 걸 보고 역할별로 분리했습니다. 아래 예시는 이슈 생성만 담당하는 Tool입니다.
import { Agent } from '@mastra/core/agent';
import { createTool } from '@mastra/core/tools';
import { anthropic } from '@ai-sdk/anthropic';
import { z } from 'zod';
const createIssueTool = createTool({
id: 'create-github-issue',
description: 'GitHub 저장소에 이슈를 생성합니다. PR 분석 결과나 버그 보고 시 사용합니다.',
inputSchema: z.object({
owner: z.string(),
repo: z.string(),
title: z.string(),
body: z.string(),
labels: z.array(z.string()).optional(),
}),
execute: async ({ context }) => {
const response = await fetch(
`https://api.github.com/repos/${context.owner}/${context.repo}/issues`,
{
method: 'POST',
headers: {
Authorization: `token ${process.env.GITHUB_TOKEN}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
title: context.title,
body: context.body,
labels: context.labels,
}),
}
);
if (!response.ok) {
throw new Error(`GitHub API 오류: ${response.status} ${response.statusText}`);
}
const issue = await response.json();
return { issueNumber: issue.number, url: issue.html_url };
},
});
const githubAgent = new Agent({
name: 'githubAgent',
instructions: `
GitHub PR과 이슈를 관리하는 에이전트입니다.
PR 내용을 분석해 적절한 이슈를 생성하고, 관련 레이블을 붙입니다.
`,
model: anthropic('claude-opus-4-7'),
tools: { createIssueTool },
});
// 에이전트 실행
const result = await githubAgent.generate(
'feat: 사용자 인증 모듈 추가 PR에 대한 이슈를 생성해줘. 저장소는 my-org/my-repo야.'
);
console.log(result.text);| 코드 포인트 | 설명 |
|---|---|
description 구체화 |
"언제 사용하는지"까지 명시해야 에이전트 판단 품질이 올라갑니다 |
if (!response.ok) |
HTTP 오류를 명시적으로 던져야 에이전트가 실패를 인지하고 재시도할 수 있습니다 |
inputSchema |
Zod로 Tool 파라미터 타입을 보장합니다 |
agent.generate() |
자연어 입력으로 에이전트를 실행합니다 |
예시 2: RAG 기반 사내 문서 QA
팀 내부 위키나 문서를 기반으로 질문에 답변하는 에이전트는 요즘 가장 많이 쓰이는 패턴 중 하나입니다. Mastra의 RAG 파이프라인을 활용하면 문서 임베딩부터 검색, 답변 생성까지 한 흐름으로 연결됩니다.
실제 품질에 영향을 주는 요소가 두 가지 있는데요. 청킹 전략(문서를 어떤 크기로 나눌지)과 임베딩 모델 선택입니다. 아래 예시는 단락 단위로 청킹하고 text-embedding-3-small을 쓰는 방식인데, 긴 기술 문서라면 512~1024 토큰 단위 청킹이 더 잘 맞는 경우가 많습니다. 다국어 문서라면 text-embedding-3-large나 다국어 특화 모델을 고려해볼 수 있습니다.
import { MastraVector } from '@mastra/vector-pg'; // pgvector 사용
import { openai } from '@ai-sdk/openai';
import { createTool } from '@mastra/core/tools';
import { Agent } from '@mastra/core/agent';
import { embed } from 'ai';
import { z } from 'zod';
const vectorStore = new MastraVector({
connectionString: process.env.DATABASE_URL!,
});
// 문서 임베딩 및 저장 (단락 단위 청킹 기준)
async function indexDocument(content: string, metadata: Record<string, string>) {
const { embedding } = await embed({
model: openai.embedding('text-embedding-3-small'),
value: content,
});
await vectorStore.upsert({
indexName: 'docs',
vectors: [{ id: crypto.randomUUID(), vector: embedding, metadata }],
});
}
// RAG 조회 Tool
const searchDocsTool = createTool({
id: 'search-docs',
description: '사내 문서를 시맨틱 검색합니다. 팀 정책, 기술 스펙, 온보딩 자료 조회 시 사용합니다.',
inputSchema: z.object({
query: z.string().describe('검색할 내용'),
}),
execute: async ({ context }) => {
const { embedding } = await embed({
model: openai.embedding('text-embedding-3-small'),
value: context.query,
});
const results = await vectorStore.query({
indexName: 'docs',
queryVector: embedding,
topK: 5,
});
return { documents: results.map(r => r.metadata) };
},
});
const docsAgent = new Agent({
name: 'docsAgent',
instructions:
'사내 문서를 검색해 정확한 답변을 제공하는 에이전트입니다. 문서에 없는 내용은 "문서에서 확인할 수 없습니다"라고 안내합니다.',
model: openai('gpt-4o'),
tools: { searchDocsTool },
});예시 3: 다단계 Workflow로 콘텐츠 요약 파이프라인
단순한 에이전트 호출을 넘어, 여러 단계를 결정론적으로 연결해야 할 때 Workflow를 활용할 수 있습니다. 뉴스 수집 → 요약 → 슬랙 발송처럼 각 단계가 명확히 구분되는 작업에 잘 맞습니다. "어차피 순서가 정해져 있는데 Agent 쓸 필요 없잖아"라는 생각이 드는 순간, Workflow가 정답입니다.
import { Agent } from '@mastra/core/agent';
import { createWorkflow, createStep } from '@mastra/core/workflows';
import { openai } from '@ai-sdk/openai';
import { z } from 'zod';
// 2단계에서 사용할 요약 에이전트 (먼저 정의)
const summaryAgent = new Agent({
name: 'summaryAgent',
instructions: '주어진 기사 목록을 간결하고 핵심만 담아 3줄로 요약합니다.',
model: openai('gpt-4o'),
});
// 1단계: RSS 피드 수집
const fetchNewsStep = createStep({
id: 'fetch-news',
inputSchema: z.object({ feedUrl: z.string() }),
outputSchema: z.object({ articles: z.array(z.string()) }),
execute: async ({ inputData }) => {
// RSS 파싱 로직 위치 (실제 구현은 rss-parser 등 활용)
const articles = ['기사 1 내용...', '기사 2 내용...'];
return { articles };
},
});
// 2단계: 요약 생성 (에이전트 활용)
const summarizeStep = createStep({
id: 'summarize',
inputSchema: z.object({ articles: z.array(z.string()) }),
outputSchema: z.object({ summary: z.string() }),
execute: async ({ inputData }) => {
const result = await summaryAgent.generate(
`다음 기사들을 3줄로 요약해주세요:\n${inputData.articles.join('\n\n')}`
);
return { summary: result.text };
},
});
// 3단계: 슬랙 발송
const sendSlackStep = createStep({
id: 'send-slack',
inputSchema: z.object({ summary: z.string() }),
outputSchema: z.object({ sent: z.boolean() }),
execute: async ({ inputData }) => {
const response = await fetch(process.env.SLACK_WEBHOOK_URL!, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ text: inputData.summary }),
});
if (!response.ok) {
throw new Error(`슬랙 전송 실패: ${response.status}`);
}
return { sent: true };
},
});
// Workflow 조립
const newsPipeline = createWorkflow({
name: 'news-pipeline',
triggerSchema: z.object({ feedUrl: z.string() }),
})
.then(fetchNewsStep)
.then(summarizeStep)
.then(sendSlackStep)
.commit();
// 파이프라인 실행
const run = await newsPipeline.execute({
triggerData: { feedUrl: 'https://feeds.example.com/tech.rss' },
});
console.log('파이프라인 완료:', run);실제로 이 패턴으로 내부 뉴스레터 자동화를 붙여봤는데, .then() 체인 방식 덕분에 각 단계가 명확히 분리되어 디버깅이 훨씬 편했습니다. 중간 단계 출력이 Mastra Studio에서 바로 보이는 것도 큰 장점이었고요.
장단점 분석
장점
| 항목 | 내용 |
|---|---|
| 타입 안전성 | Zod 스키마가 Tool 입력·워크플로 단계·구조화 출력 전 구간에 적용되어 런타임 오류를 컴파일 타임에 감지 |
| 개발자 경험 | Mastra Studio(localhost:4111)에서 에이전트 대화, 도구 호출 인스펙션, 워크플로 시각화를 로컬에서 바로 확인 가능. NextBuild DX 벤치마크 기준 10점 만점에 9점 |
| 빠른 배포 | Vercel 통합으로 vercel deploy 한 줄에 90초 내 배포 완료 |
| 모델 다양성 | 94개 공급업체, 3,300개 이상 모델을 단일 인터페이스로 연결 (OpenAI, Anthropic, Google, Groq, Llama 등) |
| MCP 지원 | Model Context Protocol로 외부 도구 생태계와 표준화된 방식으로 연결 |
| 통합 메모리 | 단기·장기·시맨틱 리콜 세 계층을 바로 사용 가능, PostgreSQL/LibSQL/Upstash 백엔드 지원 |
단점 및 주의사항
| 항목 | 내용 | 대응 방안 |
|---|---|---|
| 좁은 생태계 | LangChain 700개 이상 통합 대비 아직 성장 중, 엣지 런타임(Cloudflare Workers) 지원도 제한적 | 필요한 통합은 직접 Tool로 래핑하거나 MCP 서버 활용. 엣지 배포가 필수라면 Vercel AI SDK 직접 사용 검토 |
| 신생 프레임워크 리스크 | v1.0이 2026년 1월 출시라 Stack Overflow 답변·예제가 적고 장기 안정성 데이터 축적 중 | 공식 Discord와 GitHub Discussions 적극 활용. 중요한 프로덕션 도입 시 단계적 롤아웃 권장 |
| Python 팀 비적합 | TypeScript-first 설계로 Python 중심 팀이나 ML 실험 환경에는 맞지 않음 | Python 팀이라면 LangChain이나 PydanticAI 검토 |
MCP(Model Context Protocol)란? Anthropic이 제안하고 업계가 채택하고 있는 LLM-도구 연결 표준 프로토콜입니다. USB-C처럼, 어떤 LLM이든 MCP를 지원하면 동일한 방식으로 외부 도구에 접근할 수 있게 해줍니다. Mastra는 MCP 클라이언트(외부 서버 호출)와 서버(Mastra 도구를 외부에 노출) 양쪽을 모두 지원합니다.
실무에서 가장 흔한 실수
1. Tool의 description을 대충 작성하는 것
LLM이 어떤 Tool을 언제 써야 할지 판단하는 근거가 description입니다. "날씨 조회" 같은 짧은 설명보다 "도시 이름으로 현재 날씨(기온, 상태)를 조회합니다. 날씨 관련 질문에만 사용하세요."처럼 구체적으로 작성하는 것이 에이전트 동작 품질에 큰 차이를 만듭니다.
2. Agent 하나에 Tool을 너무 많이 붙이는 것
Tool이 10개를 넘어가면 LLM이 적절한 Tool을 고르는 정확도가 떨어집니다. 역할별로 Agent를 분리하고 Workflow나 멀티 에이전트 패턴으로 조율하는 구조가 더 안정적입니다.
3. Evals 없이 프로덕션에 올리는 것
에이전트는 코드와 달리 출력이 확률적입니다. 프롬프트나 모델을 바꿀 때 회귀를 감지하려면 핵심 시나리오에 대한 품질 평가가 필요합니다. Mastra는 @mastra/evals 패키지로 이를 자동화할 수 있습니다.
import { evaluate } from '@mastra/evals';
import { AnswerRelevancyMetric } from '@mastra/evals/llm';
const result = await evaluate(docsAgent, '팀 온보딩 절차가 어떻게 되나요?', {
metrics: [new AnswerRelevancyMetric(openai('gpt-4o'))],
});
console.log('관련성 점수:', result.metrics['AnswerRelevancy'].score);
// 0.0 ~ 1.0 범위, 0.7 이하면 프롬프트 재검토 신호CI 파이프라인에 이 평가를 연결해두면, 프롬프트나 모델을 바꿀 때 점수가 떨어지는 즉시 감지할 수 있습니다.
마치며
Mastra를 한두 달 써본 지금, 가장 달라진 건 "에이전트가 왜 이렇게 동작하지?"를 추적하는 시간이 줄었다는 점입니다. Mastra Studio에서 Tool 호출 흐름을 한눈에 볼 수 있고, Zod 스키마 덕분에 타입 불일치가 런타임 전에 잡히니까요. 물론 생태계 넓이에서는 LangChain.js를 따라가려면 시간이 더 필요하고, Python 팀이라면 PydanticAI가 더 자연스러운 선택입니다. 하지만 TypeScript로 AI 에이전트를 만들어야 한다면, 지금 시점에서 Mastra가 가장 일관된 개발 경험을 제공하는 프레임워크입니다.
지금 바로 시작해볼 수 있는 3단계:
npx create-mastra-app@latest my-agent를 실행하면 인터랙티브 CLI가 뜹니다. 모델 공급업체(OpenAI, Anthropic 등)와 메모리 백엔드를 선택하면 기본 구조가 자동으로 생성됩니다.- 생성된 프로젝트에서
pnpm dev를 실행하면 Mastra Studio가localhost:4111에서 열립니다. 코드를 한 줄 수정하면서 에이전트 대화와 Tool 호출 결과를 바로 확인해보시면 감이 빠르게 옵니다. - 기본 Agent가 동작하는 것을 확인했다면, 관심 있는 사용 사례(RAG, Workflow, 멀티 에이전트)에 해당하는 공식 예제를 Mastra GitHub 저장소에서 찾아 확장해보시면 좋습니다.
참고 자료
- Mastra 공식 사이트 | mastra.ai — GitHub 스타 수치 및 공식 기능 기준
- Mastra 공식 문서 | mastra.ai/docs
- Mastra GitHub 저장소 | github.com
- Mastra 에이전트 개요 - 공식 Docs | mastra.ai
- MCP 개요 - Mastra Docs | mastra.ai
- The New Stack: Mastra empowers web devs to build AI agents in TypeScript | thenewstack.io
- How I used Mastra to build a prize-winning RAG agent | LogRocket Blog — LogRocket RAG 에이전트 구현 사례
- Building agentic RAG with Mastra and Elasticsearch | Elastic Labs — Elastic 프로덕션 도입 사례
- Mastra Tutorial: How to Build AI Agents in TypeScript | Firecrawl
- AI Agent Framework Comparison: Mastra vs LangChain vs others | Speakeasy
- Mastra vs LangChain.js | nextbuild.co