MVP 빠른 구축 - 한국 스타트업 스타일로 최소 기능만 빠르게 만들기
한국 스타트업 스타일로 필요한 기능만 빠르게 만들어 시장에 출시합니다.
# 예시: 맛집 추천 앱
## ✅ 포함할 것 (MVP)
- [ ] 카카오 로그인
- [ ] 맛집 목록 보기
- [ ] 맛집 상세 정보
- [ ] 찜하기
## ❌ 나중에 (v2)
- 회원가입/로그인 (카카오 로그인으로 충분)
- 리뷰 작성 (일단 정보만)
- 추천 알고리즘 (수동 큐레이션으로 시작)
- 관리자 페이지 (Notion/구글 시트로 대체)
- 푸시 알림
- 포인트/쿠폰
# 30분 안에 인증 + DB 완성
npx create-next-app@latest my-mvp
cd my-mvp
npm install @supabase/supabase-js @supabase/auth-helpers-nextjs
장점:
// lib/supabase.ts
import { createClient } from '@supabase/supabase-js';
export const supabase = createClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
);
// 로그인 (1줄)
await supabase.auth.signInWithOAuth({ provider: 'kakao' });
// 데이터 조회 (1줄)
const { data } = await supabase.from('restaurants').select('*');
// 데이터 추가 (1줄)
await supabase.from('restaurants').insert({ name: '맛집' });
// ❌ 이메일 회원가입 구현 (시간 낭비)
// - 이메일 인증
// - 비밀번호 재설정
// - 유효성 검증
// - 스팸 방지
// ✅ 카카오 로그인 하나로 끝
<button onClick={() => supabase.auth.signInWithOAuth({ provider: 'kakao' })}>
카카오로 3초 시작
</button>
// ❌ 관리자 페이지 개발
// - 권한 관리
// - UI/UX 디자인
// - CRUD 화면
// ✅ Supabase Studio 사용
// 브라우저에서 바로 데이터 수정
// 별도 개발 불필요
// ❌ 여러 결제 수단
// - 카드, 계좌이체, 휴대폰
// - 정기 결제
// - 환불 처리
// ✅ 토스페이먼츠 간편결제만
import { loadTossPayments } from '@tosspayments/payment-sdk';
const tossPayments = await loadTossPayments(clientKey);
await tossPayments.requestPayment('카드', {
amount: 10000,
orderId: generateId(),
orderName: '서비스 이용권',
successUrl: '/payment/success',
failUrl: '/payment/fail',
});
// ❌ 자체 이미지 서버 구축
// - S3 설정
// - CDN 연동
// - 리사이징
// ✅ Cloudinary 무료 플랜
import { CldImage } from 'next-cloudinary';
<CldImage src="my-image" width={300} height={300} crop="fill" />
// ❌ SMTP 서버 설정
// ✅ Resend (무료 100통/일)
import { Resend } from 'resend';
const resend = new Resend(process.env.RESEND_API_KEY);
await resend.emails.send({
from: 'noreply@myapp.com',
to: user.email,
subject: '환영합니다!',
html: '<p>가입을 환영합니다!</p>',
});
npx create-next-app@latest foodie --typescript --tailwind --app
cd foodie
npm install @supabase/supabase-js
-- restaurants 테이블
create table restaurants (
id uuid default gen_random_uuid() primary key,
name text not null,
address text,
image_url text,
rating numeric default 0,
created_at timestamp default now()
);
-- favorites 테이블
create table favorites (
user_id uuid references auth.users not null,
restaurant_id uuid references restaurants not null,
created_at timestamp default now(),
primary key (user_id, restaurant_id)
);
// app/page.tsx
import { createServerComponentClient } from '@supabase/auth-helpers-nextjs';
import { cookies } from 'next/headers';
export default async function Home() {
const supabase = createServerComponentClient({ cookies });
const { data: restaurants } = await supabase
.from('restaurants')
.select('*')
.order('rating', { ascending: false })
.limit(20);
return (
<div className="container mx-auto p-4">
<h1 className="text-3xl font-bold mb-8">오늘의 맛집</h1>
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
{restaurants?.map((restaurant) => (
<div key={restaurant.id} className="border rounded-lg p-4">
<img
src={restaurant.image_url || '/placeholder.jpg'}
alt={restaurant.name}
className="w-full h-40 object-cover rounded"
/>
<h2 className="font-bold mt-2">{restaurant.name}</h2>
<p className="text-sm text-gray-600">{restaurant.address}</p>
<div className="flex items-center mt-2">
<span className="text-yellow-500">⭐</span>
<span className="ml-1">{restaurant.rating.toFixed(1)}</span>
</div>
</div>
))}
</div>
</div>
);
}
// app/login/page.tsx
'use client';
import { createClientComponentClient } from '@supabase/auth-helpers-nextjs';
export default function Login() {
const supabase = createClientComponentClient();
const handleLogin = async () => {
await supabase.auth.signInWithOAuth({
provider: 'kakao',
options: {
redirectTo: `${location.origin}/auth/callback`,
},
});
};
return (
<div className="flex items-center justify-center min-h-screen">
<button
onClick={handleLogin}
className="bg-yellow-400 text-black px-8 py-3 rounded-lg font-bold"
>
카카오로 3초 시작
</button>
</div>
);
}
// app/auth/callback/route.ts
import { createRouteHandlerClient } from '@supabase/auth-helpers-nextjs';
import { cookies } from 'next/headers';
import { NextResponse } from 'next/server';
export async function GET(request: Request) {
const requestUrl = new URL(request.url);
const code = requestUrl.searchParams.get('code');
if (code) {
const supabase = createRouteHandlerClient({ cookies });
await supabase.auth.exchangeCodeForSession(code);
}
return NextResponse.redirect(new URL('/', requestUrl.origin));
}
// components/FavoriteButton.tsx
'use client';
import { createClientComponentClient } from '@supabase/auth-helpers-nextjs';
import { useState } from 'react';
export function FavoriteButton({ restaurantId }: { restaurantId: string }) {
const [isFavorite, setIsFavorite] = useState(false);
const supabase = createClientComponentClient();
const toggleFavorite = async () => {
const { data: { user } } = await supabase.auth.getUser();
if (!user) {
alert('로그인이 필요합니다!');
return;
}
if (isFavorite) {
await supabase
.from('favorites')
.delete()
.eq('user_id', user.id)
.eq('restaurant_id', restaurantId);
} else {
await supabase.from('favorites').insert({
user_id: user.id,
restaurant_id: restaurantId,
});
}
setIsFavorite(!isFavorite);
};
return (
<button onClick={toggleFavorite}>
{isFavorite ? '❤️' : '🤍'}
</button>
);
}
# GitHub에 푸시
git add .
git commit -m "MVP 완성"
git push
# Vercel 배포
npx vercel --prod
완성! 🎉
// 간단한 이벤트 추적
'use client';
import { useEffect } from 'react';
export function TrackView({ pageName }: { pageName: string }) {
useEffect(() => {
if (typeof window !== 'undefined' && window.gtag) {
window.gtag('event', 'page_view', {
page_name: pageName,
});
}
}, [pageName]);
return null;
}
<!-- 간단한 피드백 버튼 -->
<button onclick="window.open('https://tally.so/r/your-form', '_blank')">
피드백 주기
</button>
npx shadcn-ui@latest init
npx shadcn-ui@latest add button card
import { Heart, Star, MapPin } from 'lucide-react';
<Heart className="w-5 h-5" />
// app/layout.tsx
import { Inter } from 'next/font/google';
const inter = Inter({ subsets: ['latin'] });
// ❌ 커스텀 색상 정의
// ✅ Tailwind 기본 색상 사용
<div className="bg-blue-500 text-white">
// MVP 단계에서 불필요:
// - 이미지 최적화 (나중에)
// - 번들 사이즈 최적화 (나중에)
// - SSR/SSG 최적화 (나중에)
// - 캐싱 전략 (나중에)
// MVP 단계에서:
// - Figma 디자인 (불필요)
// - 브랜드 가이드라인 (불필요)
// - 반응형 완벽 대응 (모바일만)
// - 애니메이션 (불필요)
// MVP 단계에서:
// - 마이크로서비스 (불필요)
// - 복잡한 상태 관리 (불필요)
// - 추상화/인터페이스 (불필요)
// - 디자인 패턴 (불필요)
### Week 1
- [ ] Day 1-2: 핵심 기능 정의 (1가지만!)
- [ ] Day 3: DB 스키마 설계
- [ ] Day 4-5: 프론트엔드 개발
- [ ] Day 6-7: 백엔드 API 개발
### Week 2
- [ ] Day 8-9: 통합 테스트
- [ ] Day 10: 버그 수정
- [ ] Day 11: 배포
- [ ] Day 12: 베타 테스터 모집
- [ ] Day 13-14: 피드백 수집 및 개선
"빠르게 실패하고, 빠르게 배우고, 빠르게 피봇하기"