future cashcow — fiction 03
Supabase 무료 티어의 500MB
Next.js, Supabase Free, OpenRouter 무료 모델.
총 비용 0달러. 중소기업 개발자가 새벽 2시에 만드는 AI 견적 자동화.
Part I
"요구사항"
2026년 2월 월요일 아침 9시. (주)넥스트비전 3층 사무실. 직원 12명. 도현은 자기 자리에 앉았다. 개발팀장이라는 직함이 있다. 팀원은 0명이다.
카카오톡에 대표 김영수의 메시지가 올라와 있었다. 일요일 밤 11시 32분에 보낸 것이다.
"AI로 뭔가 만들어보자." 도현은 그 문장을 세 번 읽었다. "뭔가"가 문제다. 뭔가는 아무것도 아니면서 동시에 모든 것이다. 대표의 "뭔가"는 대개 유튜브 알고리즘이 그날 밤 추천해준 것에 의해 결정된다.
9시 30분. 대표실 앞 소파. 김영수 대표는 A4 용지 한 장을 들고 있었다. 손글씨로 빼곡히 적혀 있다. 도현은 그 용지를 받아 읽었다.
1. AI 고객 상담 챗봇
- 홈페이지에 넣는 거
- 24시간 자동응답
- 고객이 물어보면 AI가 대답
2. 견적서 자동 생성
- 항목 넣으면 자동으로 견적서 나오게
- PDF로 변환
- 이메일 발송
3. 매출 대시보드
- 실시간 매출 그래프
- 월별/분기별 비교
- 거래처별 분석
4. 그리고 앱도 있으면 좋겠는데
- 영업사원들이 밖에서도 볼 수 있게
- 푸시 알림
도현은 A4를 내려놓았다. 4개 항목. 고객 챗봇, 견적 자동화, 매출 대시보드, 모바일 앱. 이것을 전부 만들려면 개발자 3명이 6개월은 필요하다. 도현은 혼자다. 그리고 이 회사의 개발 예산은 도현의 연봉 3,800만 원이 전부다.
"대표님, 이 네 가지를 전부 만들면 외주 기준으로 최소 1억입니다."
"그래서 AI로 하자는 거 아니야. 요즘 AI로 하면 빠르다며."
"AI로 해도 혼자서 네 개를 동시에 만들 수는 없습니다."
김영수 대표는 의자에 기대며 팔짱을 꼈다. 55세. 제조업 기반 B2B 영업 회사를 20년 운영한 사람이다. 기술에 대한 이해는 얕지만 감은 좋다. 문제는 감과 현실 사이의 거리를 모른다는 것이다.
"그러면 뭘 먼저 해야 돼?"
도현은 A4를 다시 들었다. 네 항목을 위에서부터 짚었다.
"챗봇은 우리 홈페이지 방문자가 하루에 몇 명이나 됩니까."
"... 30명? 40명?"
"하루 30명한테 챗봇은 과잉 투자입니다. 매출 대시보드는 지금 엑셀로 하고 계신 거 데이터 구조가 어떻습니까."
"... 영업팀이 각자 엑셀 파일 따로 쓰고 있어."
"그러면 대시보드 만들기 전에 데이터 통합부터 해야 합니다. 그것만 3개월입니다. 앱은 논외로 하겠습니다."
김영수 대표의 표정이 굳었다.
"그러면 뭘 할 수 있는데?"
"견적서 자동화. 이건 됩니다."
도현은 설명했다. 견적서는 구조가 단순하다. 항목명, 수량, 단가, 비고. 이 네 가지를 입력하면 AI가 표준 문구를 생성하고, PDF로 변환해서 이메일로 보낸다. 지금 영업팀이 견적서 하나 만드는 데 평균 40분 걸린다. 한글 파일 열고, 이전 견적서 복사하고, 항목 수정하고, PDF 변환하고, 메일에 첨부. 이걸 5분으로 줄일 수 있다.
"5분?"
"웹에서 항목 입력하면 AI가 견적서 문구를 자동 생성합니다. 고객사명, 담당자명, 항목별 설명 전부 포함해서. PDF 변환하고 이메일 발송까지 자동으로."
"비용은?"
여기서 0원이라고 하면 믿지 않을 것이다. 0원이라고 하면 품질을 의심할 것이다. 하지만 진짜 0원이다.
"초기 개발 비용은 0원입니다. 제가 만듭니다. 운영 비용은 월 5~6달러 정도. AI API 호출 비용이 소량 발생합니다."
"0원?"
"무료 서비스를 조합합니다."
김영수 대표는 3초 동안 도현을 바라보았다. 그리고 고개를 끄덕였다.
"좋아. 해봐. 근데 앱도 나중에 되는 거지?"
"견적서 시스템이 웹이라 모바일 브라우저에서도 됩니다."
"아, 그래?"
"앱도 있으면 좋겠는데"에 대한 가장 현실적인 답은 반응형 웹이다. 도현은 그것을 말하지 않았다. 대표에게 반응형이라는 단어를 설명하는 것보다 "모바일 브라우저에서도 됩니다"가 빠르다.
Part II
"0원 스택"
그날 오후. 도현은 자리로 돌아와 노트북을 열었다. 브라우저 탭 6개. 각 서비스의 무료 티어 페이지.
기술 스택을 결정해야 한다. 조건은 세 가지. 첫째, 무료. 둘째, 혼자 만들 수 있을 것. 셋째, 프로덕션에 올릴 수 있을 것. "토이 프로젝트"가 아니라 실제로 영업팀이 쓸 수 있는 수준이어야 한다.
총 비용 월 5달러. AI API 호출 비용이 전부다. 나머지는 0원. 2026년의 무료 티어는 놀랍도록 관대하다.
하지만 무료에는 대가가 있다. 도현은 Supabase Free 티어의 조건을 정확히 알고 있었다.
500MB. 넥스트비전의 고객 데이터가 들어갈까. 거래처 120개, 연간 견적서 약 800건. 견적서 하나당 항목 평균 5개, 각 항목에 텍스트 200자. 대충 계산하면 연간 데이터 10MB도 안 될 것이다. 10년 써도 100MB. 500MB면 충분하다.
문제는 7일 미사용 시 일시정지다. 주말 포함 7일 동안 아무도 접속하지 않으면 데이터베이스가 멈춘다. 영업팀이 월요일 아침에 견적서를 만들려는데 DB가 자고 있으면 — 대표한테 불려간다.
도현은 해결책을 메모했다. Supabase 프로젝트에 매일 아침 8시에 헬스체크 쿼리를 보내는 크론잡. GitHub Actions의 scheduled workflow로 무료로 돌릴 수 있다. 매일 아침 SELECT 1 하나 날리면 프로젝트는 영원히 깨어 있다.
AI 모델 선택. OpenRouter를 통해 DeepSeek V3.2를 쓰기로 했다.
견적서 하나 생성 비용 0.1원. 월 800건 기준 80원. 현실적으로 AI API 비용은 월 5달러를 넘기기 어렵다. OpenRouter에 $10 충전하면 2개월은 간다.
Netlify Free는 월 100GB 대역폭. 견적서 시스템 트래픽으로는 1%도 쓰기 어렵다. 상업용 사용도 가능하다.
기술 스택이 결정되었다. 이제 만들어야 한다. 도현은 터미널을 열었다.
Creating a new Next.js app in ./nextvision-quote
$ cd nextvision-quote
$ npm install @supabase/supabase-js @react-pdf/renderer resend
# Supabase 클라이언트 + PDF 생성 + 이메일 발송
added 127 packages in 18s
프로젝트 생성 30초. 패키지 설치 18초. 여기까지는 누구나 할 수 있다. 문제는 다음이다.
도현은 Supabase 대시보드에 접속했다. 새 프로젝트를 생성했다. 리전은 Northeast Asia(Tokyo). 서울에서 가장 가까운 리전이다. 프로젝트 이름: nextvision-quote. 30초 후 프로젝트가 생성되었다.
NEXT_PUBLIC_SUPABASE_URL=https://xxxxxxxxxxxx.supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIs...
SUPABASE_SERVICE_ROLE_KEY=eyJhbGciOiJIUzI1NiIs...
OPENROUTER_API_KEY=sk-or-v1-...
RESEND_API_KEY=re_...
# 5개의 시크릿. 전부 무료 티어에서 발급.
환경변수 5개. URL, 익명 키, 서비스 롤 키, OpenRouter API 키, 이메일 API 키. 이 5줄이 0원짜리 인프라의 전부다.
지난주에 Claude Pro $20을 내 카드로 결제했다. 무료 도구 조합으로 개발 환경을 갖췄다. 이제 무료 인프라 위에 실제 제품을 올린다. 내 돈이 든 건 Claude Pro 월 $20뿐이다. 회사 경비가 아니라 내 월급에서 나간다. 세후 268만 원에서 약 3만 원.
도현은 에디터를 열었다. Claude에게 첫 번째 프롬프트를 보낼 시간이다. 하지만 지금은 오후 5시 47분이다. 퇴근 시간이다.
이 프로젝트는 업무 시간에 하는 것이 아니다. 대표가 "해봐"라고 했지만, 도현의 업무 시간은 기존 시스템 유지보수와 영업팀 요청 처리로 가득 차 있다. AI 견적서 시스템은 퇴근 후에 만든다. 야근 수당은 없다.
Part III
"새벽 2시의 커밋"
그날 밤 11시 14분. 자취방. 모니터 하나, 노트북 하나. 도현은 커피를 내리고 자리에 앉았다.
Claude Pro 구독 덕분에 claude.ai에서 Opus를 쓸 수 있다. 도현은 Claude에게 프로젝트 요구사항을 설명했다. 장문의 프롬프트가 아니라 대화로. 첫 번째 질문은 데이터베이스 스키마였다.
도현은 Claude가 제안한 SQL을 받아서 Supabase SQL Editor에 붙여넣었다.
-- 1. 거래처 테이블
CREATE TABLE clients (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
name TEXT NOT NULL,
contact TEXT,
email TEXT,
phone TEXT,
created_at TIMESTAMPTZ DEFAULT now()
);
-- 2. 견적서 테이블
CREATE TABLE quotes (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
client_id UUID REFERENCES clients(id),
quote_number TEXT NOT NULL UNIQUE,
title TEXT NOT NULL,
total_amount BIGINT DEFAULT 0,
ai_summary TEXT,
status TEXT DEFAULT 'draft',
created_by UUID REFERENCES auth.users(id),
created_at TIMESTAMPTZ DEFAULT now()
);
-- 3. 견적 항목 테이블
CREATE TABLE quote_items (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
quote_id UUID REFERENCES quotes(id) ON DELETE CASCADE,
name TEXT NOT NULL,
description TEXT,
quantity INTEGER DEFAULT 1,
unit_price BIGINT NOT NULL,
ai_desc TEXT -- AI가 생성한 표준 문구
);
-- 4. RLS 정책
ALTER TABLE clients ENABLE ROW LEVEL SECURITY;
ALTER TABLE quotes ENABLE ROW LEVEL SECURITY;
ALTER TABLE quote_items ENABLE ROW LEVEL SECURITY;
-- 인증된 사용자만 접근 가능
CREATE POLICY "authenticated_access" ON clients
FOR ALL USING (auth.role() = 'authenticated');
CREATE POLICY "authenticated_access" ON quotes
FOR ALL USING (auth.role() = 'authenticated');
CREATE POLICY "authenticated_access" ON quote_items
FOR ALL USING (
quote_id IN (SELECT id FROM quotes)
);
테이블 3개, 컬럼 20개, RLS 정책 3개. 500MB 중 이 스키마가 차지하는 공간은 수 킬로바이트. 도현은 SQL을 실행했다. 초록색 성공 메시지가 떴다.
다음은 핵심이다. AI 견적 문구 생성 API. 도현은 Next.js의 Route Handler로 API를 만들기 시작했다.
import { NextRequest, NextResponse } from 'next/server';
import { createClient } from '@supabase/supabase-js';
const supabase = createClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.SUPABASE_SERVICE_ROLE_KEY!
);
export async function POST(req: NextRequest) {
const { items, clientName, title } = await req.json();
// 1. OpenRouter API로 AI 견적 문구 생성
const response = await fetch(
"https://openrouter.ai/api/v1/chat/completions",
{
method: "POST",
headers: {
"Content-Type": "application/json",
"Authorization": `Bearer ${process.env.OPENROUTER_API_KEY}`,
},
body: JSON.stringify({
model: "deepseek/deepseek-chat",
messages: [
{
role: "system",
content: `당신은 B2B 견적서 작성 전문가입니다.
각 항목에 대해 전문적이고 정중한 설명을
2-3문장으로 작성하세요. 기술 사양이 있으면
포함하고, 납품 조건과 품질 보증을 언급하세요.
JSON 배열로 응답하세요.`,
},
{
role: "user",
content: `고객사: ${clientName}\n제목: ${title}\n\n항목:\n${
items.map((item: any, i: number) =>
`${i + 1}. ${item.name} (수량: ${item.quantity},`
+ ` 단가: ${item.unitPrice.toLocaleString()}원)`
).join('\n')
}`,
},
],
temperature: 0.3,
response_format: { type: "json_object" },
}),
}
);
const data = await response.json();
const descriptions = JSON.parse(
data.choices[0].message.content
);
// 2. Supabase에 견적서 저장
const quoteNumber = `NV-${Date.now()}`;
const totalAmount = items.reduce(
(sum: number, item: any) =>
sum + item.quantity * item.unitPrice, 0
);
const { data: quote, error } = await supabase
.from('quotes')
.insert({
quote_number: quoteNumber,
client_id: items[0].clientId,
title,
total_amount: totalAmount,
ai_summary: data.choices[0].message.content,
})
.select()
.single();
if (error) return NextResponse.json(
{ error: error.message }, { status: 500 }
);
// 3. 견적 항목 저장 (AI 생성 설명 포함)
const itemsWithAI = items.map(
(item: any, i: number) => ({
quote_id: quote.id,
name: item.name,
quantity: item.quantity,
unit_price: item.unitPrice,
ai_desc: descriptions.items?.[i]?.description
?? '',
})
);
await supabase.from('quote_items').insert(itemsWithAI);
return NextResponse.json({
quoteId: quote.id,
quoteNumber,
totalAmount,
descriptions,
});
}
70줄. 이 70줄이 하는 일은 세 가지다. 첫째, OpenRouter API를 통해 DeepSeek에게 견적 항목을 보내서 전문적인 설명을 생성한다. 둘째, Supabase에 견적서를 저장한다. 셋째, 각 항목에 AI 생성 설명을 붙여서 저장한다.
도현은 코드를 한 줄씩 읽었다. Claude가 생성한 코드와 자신이 이해한 코드 사이의 차이를 확인하는 습관이다. temperature: 0.3. 낮은 온도는 일관된 출력을 의미한다. 견적서에는 창의적 문구가 아니라 정확한 문구가 필요하다. 이건 맞다.
하지만 한 가지 문제를 발견했다. response_format: { type: "json_object" }. DeepSeek이 이 파라미터를 지원하는지 확인해야 한다. OpenRouter 문서를 열었다. 지원한다. 계속 진행.
자정을 넘겼다. 다음은 프론트엔드다. 견적서 입력 폼.
"use client";
import { useState } from "react";
interface QuoteItem {
name: string;
quantity: number;
unitPrice: number;
}
export default function NewQuotePage() {
const [clientName, setClientName] = useState("");
const [title, setTitle] = useState("");
const [items, setItems] = useState<QuoteItem[]>([
{ name: "", quantity: 1, unitPrice: 0 },
]);
const [loading, setLoading] = useState(false);
const [result, setResult] = useState<any>(null);
const addItem = () =>
setItems([...items,
{ name: "", quantity: 1, unitPrice: 0 }]);
const removeItem = (index: number) =>
setItems(items.filter((_, i) => i !== index));
const updateItem = (
index: number,
field: keyof QuoteItem,
value: string | number
) => {
const updated = [...items];
updated[index] = { ...updated[index], [field]: value };
setItems(updated);
};
const handleSubmit = async () => {
setLoading(true);
try {
const res = await fetch("/api/generate-quote", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ items, clientName, title }),
});
setResult(await res.json());
} catch (err) {
console.error(err);
} finally {
setLoading(false);
}
};
const total = items.reduce(
(sum, item) => sum + item.quantity * item.unitPrice, 0
);
return (
<main className="max-w-2xl mx-auto p-8">
<h1 className="text-2xl font-bold mb-8">
새 견적서
</h1>
{/* 고객사 + 제목 입력 */}
<input placeholder="고객사명" value={clientName}
onChange={e => setClientName(e.target.value)}
className="w-full border p-3 mb-4 rounded" />
<input placeholder="견적서 제목" value={title}
onChange={e => setTitle(e.target.value)}
className="w-full border p-3 mb-8 rounded" />
{/* 견적 항목 동적 폼 */}
{items.map((item, i) => (
<div key={i} className="flex gap-3 mb-3">
<input placeholder="항목명"
value={item.name}
onChange={e =>
updateItem(i, "name", e.target.value)}
className="flex-1 border p-2 rounded" />
<input type="number" placeholder="수량"
value={item.quantity}
onChange={e =>
updateItem(i, "quantity",
Number(e.target.value))}
className="w-20 border p-2 rounded" />
<input type="number" placeholder="단가"
value={item.unitPrice}
onChange={e =>
updateItem(i, "unitPrice",
Number(e.target.value))}
className="w-32 border p-2 rounded" />
<button onClick={() => removeItem(i)}
className="text-red-500">X</button>
</div>
))}
<button onClick={addItem}
className="text-blue-600 mb-8">
+ 항목 추가</button>
{/* 합계 + 생성 버튼 */}
<div className="border-t pt-4 flex justify-between">
<span className="font-bold">
합계: {total.toLocaleString()}원
</span>
<button onClick={handleSubmit}
disabled={loading}
className="bg-black text-white px-6 py-2
rounded disabled:opacity-50">
{loading ? "AI 생성 중..." : "견적서 생성"}
</button>
</div>
</main>
);
}
컴포넌트 하나. 상태 5개. 함수 4개. 도현은 이 코드를 Claude가 생성하기를 기다리는 동안 라면을 끓였다. 돌아왔을 때 코드는 이미 완성되어 있었다. 도현은 라면을 먹으며 코드를 읽었다.
새벽 1시 12분. PDF 변환 로직. 이것이 이 프로젝트에서 가장 까다로운 부분이다. 브라우저에서 견적서 PDF를 생성해야 한다. @react-pdf/renderer를 쓰기로 했다.
import {
Document, Page, Text, View, StyleSheet,
renderToBuffer
} from "@react-pdf/renderer";
const styles = StyleSheet.create({
page: { padding: 40, fontSize: 10,
fontFamily: "Helvetica" },
header: { fontSize: 18, fontWeight: "bold",
marginBottom: 20, textAlign: "center" },
meta: { flexDirection: "row",
justifyContent: "space-between",
marginBottom: 20, fontSize: 9,
color: "#666" },
table: { marginTop: 10 },
row: { flexDirection: "row",
borderBottom: "1px solid #ddd",
padding: 8 },
headerRow: { backgroundColor: "#f5f5f5",
fontWeight: "bold" },
col1: { width: "30%" },
col2: { width: "35%", fontSize: 8,
color: "#444" },
col3: { width: "10%", textAlign: "center" },
col4: { width: "25%", textAlign: "right" },
total: { marginTop: 20, textAlign: "right",
fontSize: 14, fontWeight: "bold" },
});
export async function generateQuotePDF(
quote: QuoteData
): Promise<Buffer> {
const doc = (
<Document>
<Page size="A4" style={styles.page}>
<Text style={styles.header}>
견 적 서
</Text>
<View style={styles.meta}>
<Text>{quote.quoteNumber}</Text>
<Text>{quote.clientName} 귀중</Text>
</View>
<View style={styles.table}>
<View style={[styles.row, styles.headerRow]}>
<Text style={styles.col1}>항목</Text>
<Text style={styles.col2}>설명</Text>
<Text style={styles.col3}>수량</Text>
<Text style={styles.col4}>금액</Text>
</View>
{quote.items.map((item, i) => (
<View key={i} style={styles.row}>
<Text style={styles.col1}>
{item.name}</Text>
<Text style={styles.col2}>
{item.aiDesc}</Text>
<Text style={styles.col3}>
{item.quantity}</Text>
<Text style={styles.col4}>
{(item.quantity * item.unitPrice
).toLocaleString()}원</Text>
</View>
))}
</View>
<Text style={styles.total}>
합계: {quote.totalAmount.toLocaleString()}원
</Text>
</Page>
</Document>
);
return renderToBuffer(doc);
}
새벽 1시 47분. 도현은 PDF 코드를 저장하고 터미널에서 개발 서버를 돌렸다.
- Local: http://localhost:3000
- Ready in 2.3s
브라우저에서 localhost:3000/quotes/new를 열었다. 입력 폼이 떴다. 고객사명: "테스트주식회사". 제목: "2026년 1분기 기자재 납품 견적". 항목 1: "서버 랙 42U" / 수량 2 / 단가 1,800,000. 항목 2: "UPS 3kVA" / 수량 1 / 단가 2,500,000.
"견적서 생성" 버튼을 눌렸다. 로딩 스피너가 돌았다. 3초. DeepSeek이 응답했다.
{
"quoteNumber": "NV-1707523847291",
"totalAmount": 6100000,
"descriptions": {
"items": [
{
"name": "서버 랙 42U",
"description": "42U 표준 규격(19인치) 서버 랙
캐비넷. 전면/후면 메시 도어, 케이블
관리 시스템, 접지 키트 포함. 내하중
800kg. 납품 후 1년 무상 보증."
},
{
"name": "UPS 3kVA",
"description": "3kVA 온라인 무정전
전원장치(UPS). 이중 변환 방식, 배터리
런타임 15분(풀로드 기준). SNMP 카드
내장, 원격 모니터링 가능. 설치 및 초기
설정 포함."
}
]
}
}
도현은 응답을 읽었다. "42U 표준 규격(19인치) 서버 랙 캐비넷. 전면/후면 메시 도어, 케이블 관리 시스템, 접지 키트 포함." 영업팀이 한글 파일에서 40분 동안 끙끙대며 쓰던 그 문구를 AI가 3초 만에 생성했다. 그리고 더 전문적이다.
Supabase 대시보드를 열었다. quotes 테이블에 데이터 1건. quote_items 테이블에 2건. 정상 저장.
새벽 2시 3분. 도현은 커밋했다.
$ git commit -m "feat: AI quote generation with OpenRouter + Supabase"
[main 4a7f2c1] feat: AI quote generation with OpenRouter + Supabase
12 files changed, 487 insertions(+)
487줄. 이 중 도현이 직접 타이핑한 것은 환경변수 5줄과 프롬프트 수정 20줄, 그리고 디버깅 과정에서 고친 것들을 합쳐 약 60줄이다. 나머지 400줄은 Claude가 생성했다. 하지만 도현은 그 400줄을 전부 읽었다. 이해하지 못하는 줄이 나오면 Claude에게 물었다. "이 줄이 왜 필요해?" Claude가 설명하면 도현이 이해한 뒤에 수용했다.
Y Combinator 2025 겨울 배치의 25%가 코드베이스의 95%를 AI로 생성했다고 한다. 나도 비슷하다. 하지만 그 25%의 팀에 CTO가 없고 개발자가 없다면 — 누가 코드를 읽는가. 적어도 나는 읽는다. 이해하지 못해도 읽는다. 이해가 될 때까지 읽는다.
새벽 2시 11분. 도현은 노트북을 덮었다. 내일도 출근이다. 아침 9시. 영업팀의 엑셀 요청이 기다리고 있을 것이다. 그리고 내일 밤에도 이 자리에 앉아서 이메일 발송 기능을 만들 것이다.
자취방의 불을 껐다. 모니터의 잔상이 눈에 남았다. 도현은 생각했다. 연봉 3,800만 원. 세후 268만 원. Claude Pro 월 $20은 내 돈이다. 새벽 2시 코딩은 야근 수당이 없다. 회사의 "미래먹거리"를 내 시간과 내 돈으로 만들고 있다.
하지만 그래도 만든다. 이유는 단순하다. 이것이 동작하면 도현의 가치가 올라간다. 팀원 0명의 개발팀장이 AI 견적서 시스템을 만든 개발팀장이 된다. 포트폴리오에 한 줄이 추가된다. 그리고 어쩌면 — 연봉 협상 카드가 하나 생긴다.
Part IV
"It works"
2주 뒤. 2026년 2월 금요일 오후 3시.
도현은 대표실 소파에 앉아 노트북을 펼쳤다. 김영수 대표가 맞은편에 앉았다. 영업 팀장 박정호가 옆에 섰다.
"보여드리겠습니다."
도현은 브라우저를 열었다. URL은 nextvision-quote.netlify.app. Netlify 무료 배포. 커스텀 도메인은 나중에 연결하면 된다.
로그인 화면. Supabase Auth가 처리하는 이메일 로그인. 도현이 테스트 계정으로 들어갔다. 대시보드가 떴다. 최근 견적서 목록, 거래처 수, 이번 달 견적 금액. Tailwind CSS로 만든 깔끔한 UI.
"새 견적서를 만들어보겠습니다."
도현이 입력했다. 고객사: "(주)한솔테크". 제목: "2026년 2월 IT 인프라 견적". 항목 3개: 서버 랙, UPS, 네트워크 스위치. 수량과 단가를 입력하고 "견적서 생성" 버튼을 눌렀다.
로딩 스피너. 3초.
화면이 바뀌었다. 견적서 상세 페이지. 상단에 견적 번호, 고객사명, 날짜. 테이블에 항목명, AI가 생성한 설명, 수량, 금액. 하단에 합계와 부가세. 각 항목의 설명란에는 DeepSeek이 생성한 전문 문구가 들어가 있었다.
박정호 영업팀장이 화면에 눈을 가까이 대고 읽었다.
"'48포트 기가비트 이더넷 스위치. L3 관리형, PoE+ 지원, VLAN/QoS 설정 가능.' 이거 AI가 쓴 거야?"
"네."
"우리 팀 김주임보다 잘 쓰는데."
도현은 "PDF 다운로드" 버튼을 눌렀다. 2초 후 PDF가 생성되었다. A4 형식. 상단에 "(주)넥스트비전 견적서". 깔끔한 테이블. AI가 생성한 설명이 각 항목 아래에 정렬되어 있었다.
"이메일 발송도 됩니다."
도현이 "이메일 발송" 버튼을 눌렀다. 수신자에 박정호 팀장의 이메일을 입력하고 전송. 5초 후 박정호의 폰이 울렸다. 이메일. 제목: "[넥스트비전] 견적서 NV-1708065432". 첨부 파일: 견적서 PDF.
박정호가 폰으로 PDF를 열어보았다. 그리고 말했다.
"이거 괜찮다. 진짜 괜찮다. 우리 팀이 견적서 하나 만드는 데 40분 걸리는데, 이거 5분이면 되겠네."
김영수 대표가 물었다.
"비용이 얼마라고 했지?"
"월 5~6달러입니다. 한화 약 7,000원."
"7천 원?"
"AI API 호출 비용입니다. 나머지는 전부 무료입니다."
대표가 고개를 끄덕였다. 그리고 도현이 예상한 바로 그 질문이 나왔다.
"오 이거 괜찮은데? 근데 앱은?"
도현은 폰을 꺼내 같은 URL을 열었다. nextvision-quote.netlify.app. 모바일 브라우저에서 동일한 화면이 반응형으로 표시되었다. 견적서 입력, AI 생성, PDF 다운로드. 전부 모바일에서 된다.
"앱 설치 없이 이대로 쓸 수 있습니다. 홈 화면에 추가하면 앱처럼 동작합니다."
"아, 홈 화면에 추가하는 거?"
"네. 크롬에서 '홈 화면에 추가'를 누르면 아이콘이 생깁니다."
대표는 만족한 표정이었다. 7,000원짜리 견적서 시스템. 40분이 5분이 되는 마법. 도현은 노트북을 닫으며 생각했다.
여기까지 2주. 밤 11시부터 새벽 2시까지, 주 5일. 총 약 30시간. 코드 487줄. 무료 서비스 5개. 결과물: 실제로 동작하는 AI 견적서 시스템.
하지만 이것은 MVP다. 최소한으로 동작하는 제품이다. 견적서 승인 워크플로우가 없다. 거래처 관리가 원시적이다. 견적서 템플릿 커스터마이징이 안 된다. 매출 연동이 없다. 대표가 원래 요구한 네 가지 중 하나를 겨우 만든 것이다.
도현은 대표실을 나와 자리로 돌아왔다. 다음 단계를 정리했다.
[완료] AI 견적 문구 자동 생성
[완료] PDF 변환 + 이메일 발송
[완료] Netlify 배포
[다음] 거래처 관리 CRUD
[다음] 견적서 수정/삭제
[다음] 견적서 승인 워크플로우
[다음] 한글 폰트 PDF 지원
[나중] 매출 대시보드
[나중] 거래처별 분석
[보류] AI 고객 상담 챗봇
[보류] 네이티브 앱
"보류" 항목 두 개. 대표의 원래 A4 메모에 있던 것들이다. 도현은 그것들이 언제 "보류"에서 벗어날지 모른다. 아마 다음 달에 대표가 유튜브에서 또 다른 영상을 보기 전까지는 보류일 것이다.
오후 5시 47분. 퇴근. 도현은 사무실을 나서며 Netlify 대시보드를 한 번 더 확인했다.
Site: nextvision-quote
Status: Published
Last deploy: 2 hours ago
Bandwidth used: 12 MB / 100 GB
# 100GB 중 12MB 사용. 0.012%.
100GB 대역폭의 0.012%. Supabase 500MB의 0.1%. Netlify 빌드 300분의 3분. 무료 티어의 벽은 한참 멀었다. 넥스트비전의 규모로는 아마 평생 무료 티어를 벗어나지 않을 것이다.
하지만 도현은 알고 있었다. 무료 티어의 진짜 비용은 돈이 아니다. 시간이다. 새벽 2시의 시간. 야근 수당 없는 시간. 자기 돈으로 결제한 Claude Pro의 시간. 0원 스택의 대가는 도현의 수면 시간이었다.
버스 정류장에서 버스를 기다리며 도현은 폰으로 Supabase 대시보드를 열었다. 데이터베이스 사용량: 3.2MB / 500MB. 견적서 테스트 데이터 7건. 월요일부터 영업팀이 실제로 쓰기 시작하면 데이터가 쌓일 것이다. 거래처 120개, 월 80건의 견적서. 500MB가 가득 차려면 수십 년이 걸릴 것이다.
500MB. 넥스트비전의 모든 비즈니스 데이터가 들어갈 수 있는 공간. 반도체 하나에 들어가는 용량보다 작다. 스마트폰 사진 한 장보다 작을 수 있는 공간에 중소기업 하나의 견적 데이터가 전부 들어간다. 데이터가 적다는 것은 회사가 작다는 뜻이고, 회사가 작다는 것은 무료 티어로 충분하다는 뜻이다.
그래서 0원이 가능한 것이다. 우리가 작으니까. 작아서 무료이고, 무료여서 시작할 수 있고, 시작할 수 있어서 만들었다.
버스가 왔다. 도현은 탔다. 창밖으로 도시의 저녁이 지나갔다. 다음 주에는 견적서 수정/삭제 기능을 만들어야 한다. 그다음 주에는 승인 워크플로우. 매일 밤 3시간씩. 월급과 별개로.
도현의 폰에 카카오톡 알림이 울렸다.
도현은 메시지를 읽고 폰을 주머니에 넣었다. 답장은 하지 않았다. 거래처 포털. 새로운 요구사항. A4 메모에 다섯 번째 항목이 추가된 것이다.
버스가 집 앞 정류장에 섰다. 도현은 내렸다. 자취방 계단을 올라가며 생각했다.
오늘 밤에는 코딩하지 않는다. 자야 한다. 내일 밤에 한다.
하지만 자취방 문을 열고 노트북을 보는 순간 — 도현은 알고 있었다. 아마 1시간 뒤에 다시 에디터를 열 것이다. 그리고 새벽 2시에 커밋할 것이다. 거래처 포털이 아니라 견적서 수정 기능이겠지만.
3,800만 원짜리 개발팀장. 팀원 0명. Claude Pro $20은 자비. 새벽 2시의 커밋.
Supabase 무료 티어의 500MB 안에 넥스트비전의 미래먹거리가 들어가 있다.
500MB에 담긴
3,800만 원의 야근
Next.js + Supabase Free + OpenRouter. 총 비용 월 7,000원. 대가는 새벽 2시의 수면 시간이다.