Skip to content

필사 모드: 스타트업 기술 스택 선택 가이드 2025: 0→1 단계에서 시리즈 B까지 기술 의사결정

한국어
0%
정확도 0%
💡 왼쪽 원문을 읽으면서 오른쪽에 따라 써보세요. Tab 키로 힌트를 받을 수 있습니다.
원문 렌더가 준비되기 전까지 텍스트 가이드로 표시합니다.

1. 기술 스택 철학

1.1 Boring Technology 원칙

Dan McKinley의 "Choose Boring Technology" 에세이는 스타트업 기술 선택의 바이블입니다. 핵심 주장은 이렇습니다:

**혁신 토큰 (Innovation Tokens):** 모든 팀에는 제한된 수의 "혁신 토큰"이 있습니다. 새롭고 검증되지 않은 기술을 채택할 때마다 토큰을 소비합니다. 비즈니스 자체가 이미 하나의 혁신이므로, 기술 스택에서까지 모험을 할 필요는 없습니다.

┌─────────────────────────────────────────────────────┐

│ 혁신 토큰 분배 예시 │

├─────────────────────────────────────────────────────┤

│ │

│ 보유 혁신 토큰: 3개 │

│ │

│ 좋은 예: │

│ ├── 비즈니스 모델 (혁신) ........... 토큰 1개 사용 │

│ ├── PostgreSQL (지루한 기술) ....... 토큰 0개 │

│ ├── Next.js (검증됨) .............. 토큰 0개 │

│ ├── 독자적 ML 파이프라인 (혁신) .... 토큰 1개 사용 │

│ └── 남은 토큰: 1개 (예비) │

│ │

│ 나쁜 예: │

│ ├── 비즈니스 모델 (혁신) ........... 토큰 1개 사용 │

│ ├── CockroachDB (새로움) .......... 토큰 1개 사용 │

│ ├── Bun Runtime (새로움) .......... 토큰 1개 사용 │

│ ├── 독자적 ML 파이프라인 (혁신) .... 토큰 부족! │

│ └── 남은 토큰: 0개 (위험) │

│ │

└─────────────────────────────────────────────────────┘

1.2 Monolith First

Martin Fowler가 주장한 "Monolith First" 원칙. 마이크로서비스로 시작하지 말고, 모놀리스로 시작하여 필요할 때 분리하세요.

**왜 모놀리스가 먼저인가:**

- 서비스 경계를 미리 올바르게 나누기는 거의 불가능

- 개발 속도가 빠름 (서비스 간 통신 오버헤드 없음)

- 디버깅이 쉬움 (단일 프로세스)

- 트랜잭션 관리가 간단 (분산 트랜잭션 불필요)

- 배포가 단순 (하나의 아티팩트)

┌──────────────────────────────────────────────────┐

│ 아키텍처 진화 경로 │

│ │

│ Stage 0: 모놀리스 (MVP) │

│ ├── 단일 Next.js 앱 │

│ ├── 단일 DB (Supabase) │

│ └── 단일 배포 (Vercel) │

│ │ │

│ ▼ (PMF 달성) │

│ Stage 1: 모듈러 모놀리스 │

│ ├── 도메인별 모듈 분리 │

│ ├── 내부 API 경계 정의 │

│ └── DB 스키마 분리 시작 │

│ │ │

│ ▼ (팀 10명+, 트래픽 급증) │

│ Stage 2: 선택적 서비스 분리 │

│ ├── 병목 서비스만 분리 │

│ ├── 이벤트 기반 비동기 처리 도입 │

│ └── 캐싱 레이어 추가 │

│ │ │

│ ▼ (팀 30명+, 글로벌 확장) │

│ Stage 3: MSA (필요한 경우에만) │

│ ├── 독립 배포 가능한 서비스들 │

│ ├── 서비스 메시 │

│ └── 중앙 집중식 관찰 가능성 │

└──────────────────────────────────────────────────┘

1.3 PaaS-First 전략

인프라 관리에 시간을 쓰지 마세요. PaaS(Platform as a Service)로 시작하고, 필요할 때 IaaS로 이동하세요.

| 접근 방식 | 인프라 관리 시간 | 유연성 | 비용 (초기) | 비용 (성장) |

|-----------|----------------|--------|------------|------------|

| PaaS (Vercel, Railway) | 최소 | 낮음 | 무료-50달러 | 높아질 수 있음 |

| Managed (AWS ECS, GKE) | 중간 | 중간 | 100-500달러 | 중간 |

| Self-managed (K8s on EC2) | 높음 | 높음 | 200-1000달러 | 최적화 가능 |

> **핵심 원칙**: 초기에는 개발 속도 > 비용 최적화. PMF를 찾기 전에 인프라 최적화에 시간을 쓰는 것은 시간 낭비입니다.

2. Stage 0: MVP (월 0-50달러)

2.1 추천 스택

┌───────────────────────────────────────────────┐

│ MVP 기술 스택 (2025) │

├───────────────────────────────────────────────┤

│ │

│ Frontend: Next.js 15 (App Router) │

│ Styling: Tailwind CSS + shadcn/ui │

│ Backend: Next.js API Routes / Server Actions│

│ Database: Supabase (PostgreSQL) │

│ Auth: Supabase Auth / NextAuth.js │

│ Storage: Supabase Storage / Cloudflare R2 │

│ Deploy: Vercel │

│ Analytics: PostHog (self-hosted) / Plausible │

│ Payments: Stripe │

│ Email: Resend │

│ Monitoring: Vercel Analytics + Sentry │

│ │

│ 총 월 비용: 0 ~ 50 USD │

│ (Vercel Free + Supabase Free Tier) │

│ │

└───────────────────────────────────────────────┘

2.2 Next.js + Supabase 프로젝트 셋업

프로젝트 생성

npx create-next-app@latest my-startup --typescript --tailwind --app --src-dir

cd my-startup

핵심 의존성 설치

npm install @supabase/supabase-js @supabase/ssr

npm install stripe @stripe/stripe-js

npm install resend

npm install zod react-hook-form @hookform/resolvers

UI 라이브러리

npx shadcn@latest init

npx shadcn@latest add button card input form dialog toast

개발 도구

npm install -D prettier eslint-config-prettier

npm install -D @types/node

2.3 Supabase 설정

// src/lib/supabase/server.ts

export async function createClient() {

const cookieStore = await cookies()

return createServerClient(

process.env.NEXT_PUBLIC_SUPABASE_URL!,

process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,

{

cookies: {

getAll() {

return cookieStore.getAll()

},

setAll(cookiesToSet) {

try {

cookiesToSet.forEach(({ name, value, options }) =>

cookieStore.set(name, value, options)

)

} catch {

// Server Component에서는 set 불가 - 무시

}

},

},

}

)

}

// src/lib/supabase/client.ts

export function createClient() {

return createBrowserClient(

process.env.NEXT_PUBLIC_SUPABASE_URL!,

process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!

)

}

2.4 인증 구현

// src/app/auth/login/page.tsx

'use client'

export default function LoginPage() {

const [email, setEmail] = useState('')

const [password, setPassword] = useState('')

const [loading, setLoading] = useState(false)

const router = useRouter()

const supabase = createClient()

const handleLogin = async (e: React.FormEvent) => {

e.preventDefault()

setLoading(true)

const { error } = await supabase.auth.signInWithPassword({

email,

password,

})

if (error) {

alert(error.message)

} else {

router.push('/dashboard')

router.refresh()

}

setLoading(false)

}

const handleGoogleLogin = async () => {

await supabase.auth.signInWithOAuth({

provider: 'google',

options: {

redirectTo: `${window.location.origin}/auth/callback`,

},

})

}

return (

type="email"

placeholder="이메일"

value={email}

onChange={(e) => setEmail(e.target.value)}

className="w-full rounded border p-2"

required

/>

type="password"

placeholder="비밀번호"

value={password}

onChange={(e) => setPassword(e.target.value)}

className="w-full rounded border p-2"

required

/>

type="submit"

disabled={loading}

className="w-full rounded bg-blue-600 p-2 text-white"

>

{loading ? '로그인 중...' : '로그인'}

type="button"

onClick={handleGoogleLogin}

className="w-full rounded border p-2"

>

Google로 로그인

)

}

2.5 Stripe 결제 연동

// src/app/api/stripe/checkout/route.ts

const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!)

export async function POST(req: NextRequest) {

const supabase = await createClient()

const { data: { user } } = await supabase.auth.getUser()

if (!user) {

return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })

}

const { priceId } = await req.json()

const session = await stripe.checkout.sessions.create({

customer_email: user.email,

line_items: [{ price: priceId, quantity: 1 }],

mode: 'subscription',

success_url: `${req.nextUrl.origin}/dashboard?success=true`,

cancel_url: `${req.nextUrl.origin}/pricing?canceled=true`,

metadata: {

userId: user.id,

},

})

return NextResponse.json({ url: session.url })

}

// src/app/api/stripe/webhook/route.ts

const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!)

const supabaseAdmin = createClient(

process.env.NEXT_PUBLIC_SUPABASE_URL!,

process.env.SUPABASE_SERVICE_ROLE_KEY!

)

export async function POST(req: NextRequest) {

const body = await req.text()

const sig = req.headers.get('stripe-signature')!

let event: Stripe.Event

try {

event = stripe.webhooks.constructEvent(

body,

sig,

process.env.STRIPE_WEBHOOK_SECRET!

)

} catch (err) {

return NextResponse.json({ error: 'Invalid signature' }, { status: 400 })

}

switch (event.type) {

case 'checkout.session.completed': {

const session = event.data.object as Stripe.Checkout.Session

const userId = session.metadata?.userId

if (userId) {

await supabaseAdmin

.from('subscriptions')

.upsert({

user_id: userId,

stripe_customer_id: session.customer as string,

stripe_subscription_id: session.subscription as string,

status: 'active',

plan: 'pro',

})

}

break

}

case 'customer.subscription.deleted': {

const subscription = event.data.object as Stripe.Subscription

await supabaseAdmin

.from('subscriptions')

.update({ status: 'canceled' })

.eq('stripe_subscription_id', subscription.id)

break

}

}

return NextResponse.json({ received: true })

}

2.6 MVP 비용 분석

| 서비스 | 무료 티어 | 유료 전환 시점 | 비용 |

|--------|----------|--------------|------|

| Vercel | 100GB 대역폭, 무제한 배포 | 트래픽 증가 시 | Pro: 20달러/월 |

| Supabase | 500MB DB, 1GB 스토리지 | 사용량 증가 시 | Pro: 25달러/월 |

| Stripe | 사용량 기반 | 매출 발생 시 | 2.9% + 30센트/건 |

| Resend | 100통/일 | 전환율 테스트 시 | 20달러/월 |

| Sentry | 5K 이벤트/월 | 에러 증가 시 | 26달러/월 |

| PostHog | 1M 이벤트/월 | 분석 고도화 시 | 0달러 (셀프 호스트) |

**총 MVP 비용: 0-50달러/월** (유료 전환 전까지 거의 무료)

3. Stage 1: Product-Market Fit (월 200-500달러)

3.1 PMF 단계의 기술 변화

PMF를 찾은 후, 다음 영역에서 업그레이드가 필요합니다:

┌───────────────────────────────────────────────┐

│ Stage 1 기술 스택 업그레이드 │

├───────────────────────────────────────────────┤

│ │

│ Database: Supabase Pro → 더 큰 인스턴스 │

│ Cache: + Redis (Upstash) │

│ Queue: + BullMQ (Redis 기반) │

│ Search: + Meilisearch / Typesense │

│ CDN: + Cloudflare │

│ Monitoring: + Better Stack / Grafana Cloud │

│ CI/CD: GitHub Actions 고도화 │

│ Error: Sentry Pro │

│ │

│ 월 비용: 200 ~ 500 USD │

└───────────────────────────────────────────────┘

3.2 Redis 캐싱 추가

// src/lib/cache.ts

const redis = new Redis({

url: process.env.UPSTASH_REDIS_REST_URL!,

token: process.env.UPSTASH_REDIS_REST_TOKEN!,

})

export async function getCached<T>(

key: string,

fetcher: () => Promise<T>,

ttl: number = 3600 // 1시간

): Promise<T> {

// 캐시 확인

const cached = await redis.get<T>(key)

if (cached !== null) {

return cached

}

// 캐시 미스 - 데이터 가져오기

const data = await fetcher()

// 캐시에 저장

await redis.set(key, data, { ex: ttl })

return data

}

export async function invalidateCache(pattern: string) {

const keys = await redis.keys(pattern)

if (keys.length > 0) {

await redis.del(...keys)

}

}

// 사용 예시

// const user = await getCached(

// `user:${userId}`,

// () => db.user.findUnique({ where: { id: userId } }),

// 1800 // 30분

// )

3.3 백그라운드 작업 큐

// src/lib/queue.ts

const connection = new Redis(process.env.REDIS_URL!, {

maxRetriesPerRequest: null,

})

// 큐 정의

export const emailQueue = new Queue('email', { connection })

export const analyticsQueue = new Queue('analytics', { connection })

// 이메일 워커

const emailWorker = new Worker(

'email',

async (job: Job) => {

const { to, subject, template, data } = job.data

// 이메일 전송 로직

console.log(`Sending email to ${to}: ${subject}`)

// await resend.emails.send(...)

},

{

connection,

concurrency: 5,

limiter: {

max: 100,

duration: 60000, // 분당 100건 제한

},

}

)

emailWorker.on('completed', (job) => {

console.log(`Email job ${job.id} completed`)

})

emailWorker.on('failed', (job, err) => {

console.error(`Email job ${job?.id} failed:`, err)

})

// 큐에 작업 추가

// await emailQueue.add('welcome', {

// to: 'user@example.com',

// subject: '환영합니다!',

// template: 'welcome',

// data: { name: '사용자' }

// })

3.4 모니터링 강화

// src/lib/monitoring.ts

// 커스텀 성능 모니터링

export function trackApiPerformance(

name: string,

fn: () => Promise<Response>

): Promise<Response> {

return Sentry.startSpan(

{ name, op: 'http.server' },

async () => {

const start = Date.now()

try {

const response = await fn()

const duration = Date.now() - start

// 느린 API 경고

if (duration > 2000) {

Sentry.captureMessage(`Slow API: ${name} took ${duration}ms`, 'warning')

}

return response

} catch (error) {

Sentry.captureException(error)

throw error

}

}

)

}

// 비즈니스 메트릭 추적

export function trackBusinessEvent(

event: string,

properties?: Record<string, unknown>

) {

// PostHog로 비즈니스 이벤트 전송

// posthog.capture(event, properties)

console.log(`[Business Event] ${event}`, properties)

}

3.5 데이터베이스 최적화

-- 인덱스 최적화

-- 자주 조회되는 쿼리의 인덱스 추가

CREATE INDEX CONCURRENTLY idx_users_email ON users(email);

CREATE INDEX CONCURRENTLY idx_orders_user_created

ON orders(user_id, created_at DESC);

CREATE INDEX CONCURRENTLY idx_products_category_status

ON products(category_id, status) WHERE status = 'active';

-- 파티셔닝 (이벤트 테이블이 커질 때)

CREATE TABLE events (

id BIGSERIAL,

user_id UUID NOT NULL,

event_type TEXT NOT NULL,

payload JSONB,

created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()

) PARTITION BY RANGE (created_at);

-- 월별 파티션 생성

CREATE TABLE events_2025_01 PARTITION OF events

FOR VALUES FROM ('2025-01-01') TO ('2025-02-01');

CREATE TABLE events_2025_02 PARTITION OF events

FOR VALUES FROM ('2025-02-01') TO ('2025-03-01');

-- 쿼리 성능 분석

-- EXPLAIN (ANALYZE, BUFFERS, FORMAT TEXT)

-- SELECT * FROM orders WHERE user_id = 'xxx' ORDER BY created_at DESC LIMIT 20;

4. Stage 2: Growth (월 2K-10K달러)

4.1 성장 단계 기술 스택

┌───────────────────────────────────────────────┐

│ Stage 2 기술 스택 │

├───────────────────────────────────────────────┤

│ │

│ Compute: AWS ECS Fargate / GKE │

│ Database: RDS PostgreSQL Multi-AZ │

│ Cache: ElastiCache Redis Cluster │

│ CDN: CloudFront + S3 │

│ Queue: SQS / RabbitMQ │

│ Search: ElasticSearch / OpenSearch │

│ CI/CD: GitHub Actions + ArgoCD │

│ Monitoring: Datadog / Grafana Stack │

│ Log: CloudWatch / Loki │

│ IaC: Terraform │

│ │

│ 팀 규모: 10-30명 │

│ 월 비용: 2,000 ~ 10,000 USD │

└───────────────────────────────────────────────┘

4.2 Kubernetes 도입

k8s/deployment.yaml

apiVersion: apps/v1

kind: Deployment

metadata:

name: api-server

labels:

app: api-server

spec:

replicas: 3

selector:

matchLabels:

app: api-server

template:

metadata:

labels:

app: api-server

spec:

containers:

- name: api

image: my-registry/api-server:latest

ports:

- containerPort: 3000

resources:

requests:

memory: "256Mi"

cpu: "250m"

limits:

memory: "512Mi"

cpu: "500m"

env:

- name: DATABASE_URL

valueFrom:

secretKeyRef:

name: db-credentials

key: url

- name: REDIS_URL

valueFrom:

secretKeyRef:

name: redis-credentials

key: url

readinessProbe:

httpGet:

path: /health

port: 3000

initialDelaySeconds: 5

periodSeconds: 10

livenessProbe:

httpGet:

path: /health

port: 3000

initialDelaySeconds: 15

periodSeconds: 20

apiVersion: autoscaling/v2

kind: HorizontalPodAutoscaler

metadata:

name: api-server-hpa

spec:

scaleTargetRef:

apiVersion: apps/v1

kind: Deployment

name: api-server

minReplicas: 3

maxReplicas: 20

metrics:

- type: Resource

resource:

name: cpu

target:

type: Utilization

averageUtilization: 70

- type: Resource

resource:

name: memory

target:

type: Utilization

averageUtilization: 80

4.3 CI/CD 파이프라인

.github/workflows/deploy.yml

name: Deploy to Production

on:

push:

branches: [main]

env:

AWS_REGION: ap-northeast-2

ECR_REPOSITORY: my-startup/api

ECS_SERVICE: api-service

ECS_CLUSTER: production

jobs:

test:

runs-on: ubuntu-latest

steps:

- uses: actions/checkout@v4

- uses: actions/setup-node@v4

with:

node-version: '22'

cache: 'npm'

- run: npm ci

- run: npm run lint

- run: npm run type-check

- run: npm run test

build-and-deploy:

needs: test

runs-on: ubuntu-latest

steps:

- uses: actions/checkout@v4

- name: Configure AWS credentials

uses: aws-actions/configure-aws-credentials@v4

with:

aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}

aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}

aws-region: ${{ env.AWS_REGION }}

- name: Login to Amazon ECR

id: login-ecr

uses: aws-actions/amazon-ecr-login@v2

- name: Build, tag, and push image

env:

ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }}

IMAGE_TAG: ${{ github.sha }}

run: |

docker build -t $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG .

docker push $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG

- name: Deploy to ECS

uses: aws-actions/amazon-ecs-deploy-task-definition@v2

with:

task-definition: task-definition.json

service: ${{ env.ECS_SERVICE }}

cluster: ${{ env.ECS_CLUSTER }}

wait-for-service-stability: true

4.4 Terraform 인프라 코드

terraform/main.tf

terraform {

required_providers {

aws = {

source = "hashicorp/aws"

version = "~> 5.0"

}

}

backend "s3" {

bucket = "my-startup-terraform-state"

key = "production/terraform.tfstate"

region = "ap-northeast-2"

}

}

RDS PostgreSQL

resource "aws_db_instance" "main" {

identifier = "my-startup-production"

engine = "postgres"

engine_version = "16.1"

instance_class = "db.r6g.large"

allocated_storage = 100

max_allocated_storage = 500

storage_encrypted = true

multi_az = true

db_subnet_group_name = aws_db_subnet_group.main.name

vpc_security_group_ids = [aws_security_group.rds.id]

backup_retention_period = 14

backup_window = "03:00-04:00"

maintenance_window = "Mon:04:00-Mon:05:00"

performance_insights_enabled = true

monitoring_interval = 60

tags = {

Environment = "production"

Service = "database"

}

}

ElastiCache Redis

resource "aws_elasticache_replication_group" "main" {

replication_group_id = "my-startup-cache"

description = "Redis cache cluster"

node_type = "cache.r6g.large"

num_cache_clusters = 2

automatic_failover_enabled = true

multi_az_enabled = true

engine_version = "7.0"

port = 6379

subnet_group_name = aws_elasticache_subnet_group.main.name

security_group_ids = [aws_security_group.redis.id]

at_rest_encryption_enabled = true

transit_encryption_enabled = true

}

5. Stage 3: Scale (월 10K달러 이상)

5.1 스케일 단계 아키텍처

┌─────────────────────────────────────────────────────────┐

│ Stage 3 아키텍처 │

├─────────────────────────────────────────────────────────┤

│ │

│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │

│ │CloudFront│───▶│ ALB/NLB │───▶│ K8s │ │

│ │ + WAF │ │ │ │ Cluster │ │

│ └──────────┘ └──────────┘ └──────────┘ │

│ │ │

│ ┌─────────────────┼──────────┐ │

│ │ │ │ │

│ ▼ ▼ ▼ │

│ ┌───────────┐ ┌───────────┐ ┌────────┐ │

│ │API Gateway│ │ Worker │ │ Cron │ │

│ │ Service │ │ Service │ │Service │ │

│ └─────┬─────┘ └─────┬─────┘ └───┬────┘ │

│ │ │ │ │

│ ┌─────────────┼───────────────┼────────────┤ │

│ │ │ │ │ │

│ ▼ ▼ ▼ ▼ │

│ ┌────────┐ ┌─────────┐ ┌──────────┐ ┌────────┐ │

│ │RDS │ │Redis │ │ Kafka │ │ S3 │ │

│ │Multi-AZ│ │Cluster │ │ Cluster │ │ │ │

│ └────────┘ └─────────┘ └──────────┘ └────────┘ │

│ │ │

│ ┌─────┴─────┐ │

│ │ElasticSearch│ │

│ │ Cluster │ │

│ └───────────┘ │

│ │

│ 팀 규모: 30명+ │

│ 월 비용: 10,000 USD+ │

└─────────────────────────────────────────────────────────┘

5.2 이벤트 기반 아키텍처 (Kafka)

// src/events/producer.ts

const kafka = new Kafka({

clientId: 'my-startup',

brokers: (process.env.KAFKA_BROKERS || '').split(','),

ssl: true,

sasl: {

mechanism: 'scram-sha-256',

username: process.env.KAFKA_USERNAME!,

password: process.env.KAFKA_PASSWORD!,

},

})

const producer = kafka.producer({

createPartitioner: Partitioners.DefaultPartitioner,

idempotent: true,

})

export async function publishEvent(topic: string, event: {

type: string

payload: Record<string, unknown>

metadata?: Record<string, unknown>

}) {

await producer.connect()

const message = {

key: event.payload.id as string || crypto.randomUUID(),

value: JSON.stringify({

...event,

timestamp: new Date().toISOString(),

version: '1.0',

}),

headers: {

'event-type': event.type,

'content-type': 'application/json',

},

}

await producer.send({

topic,

messages: [message],

})

}

// 사용 예시

// await publishEvent('orders', {

// type: 'order.created',

// payload: { id: orderId, userId, items, total },

// })

5.3 ElasticSearch 통합

// src/lib/search.ts

const esClient = new Client({

node: process.env.ELASTICSEARCH_URL!,

auth: {

apiKey: process.env.ELASTICSEARCH_API_KEY!,

},

})

// 인덱스 생성

export async function createProductIndex() {

await esClient.indices.create({

index: 'products',

body: {

settings: {

analysis: {

analyzer: {

korean: {

type: 'custom',

tokenizer: 'nori_tokenizer',

filter: ['nori_readingform', 'lowercase'],

},

},

},

},

mappings: {

properties: {

name: {

type: 'text',

analyzer: 'korean',

fields: { keyword: { type: 'keyword' } },

},

description: { type: 'text', analyzer: 'korean' },

category: { type: 'keyword' },

price: { type: 'integer' },

tags: { type: 'keyword' },

created_at: { type: 'date' },

},

},

},

})

}

// 검색

export async function searchProducts(query: string, filters?: {

category?: string

minPrice?: number

maxPrice?: number

page?: number

size?: number

}) {

const must: Record<string, unknown>[] = []

const filter: Record<string, unknown>[] = []

if (query) {

must.push({

multi_match: {

query,

fields: ['name^3', 'description', 'tags^2'],

type: 'best_fields',

fuzziness: 'AUTO',

},

})

}

if (filters?.category) {

filter.push({ term: { category: filters.category } })

}

if (filters?.minPrice || filters?.maxPrice) {

const range: Record<string, number> = {}

if (filters.minPrice) range.gte = filters.minPrice

if (filters.maxPrice) range.lte = filters.maxPrice

filter.push({ range: { price: range } })

}

const result = await esClient.search({

index: 'products',

body: {

query: {

bool: { must, filter },

},

from: ((filters?.page || 1) - 1) * (filters?.size || 20),

size: filters?.size || 20,

highlight: {

fields: { name: {}, description: {} },

},

},

})

return {

total: (result.hits.total as { value: number }).value,

hits: result.hits.hits.map((hit) => ({

...hit._source,

score: hit._score,

highlights: hit.highlight,

})),

}

}

6. 프론트엔드 스택

6.1 프레임워크 비교

| 항목 | Next.js 15 | Remix | SvelteKit |

|------|-----------|-------|-----------|

| 렌더링 | SSR/SSG/ISR/RSC | SSR/CSR | SSR/SSG/CSR |

| 서버 컴포넌트 | 있음 (RSC) | 없음 | 없음 (rune 사용) |

| 라우팅 | 파일 기반 (App Router) | 파일 기반 | 파일 기반 |

| 데이터 페칭 | Server Components, fetch | Loader/Action | Load 함수 |

| 배포 | Vercel 최적화, 어디서나 | 어디서나 | 어디서나 |

| 생태계 | 매우 큼 | 중간 | 성장 중 |

| 학습 곡선 | 중간-높음 | 중간 | 낮음-중간 |

| 스타트업 추천 | 강력 추천 | 추천 | 소규모 팀 추천 |

**스타트업 결론:** 2025년 기준 Next.js가 가장 안전한 선택. 생태계, 채용 시장, 배포 인프라 모든 면에서 우위.

6.2 UI 컴포넌트 전략

추천 스택:

├── Tailwind CSS ........... 유틸리티 퍼스트 CSS

├── shadcn/ui .............. 복사-붙여넣기 컴포넌트 (소유권)

├── Radix UI ............... 접근성 기반 헤드리스 컴포넌트

├── Framer Motion .......... 애니메이션

└── Lucide Icons ........... 아이콘 세트

대안:

├── Mantine ................ 올인원 UI 라이브러리

├── Ark UI ................. Chakra UI 팀의 헤드리스 컴포넌트

└── Park UI ................ Ark UI + Tailwind

6.3 상태 관리

| 라이브러리 | 복잡도 | 용도 | 스타트업 추천 |

|-----------|--------|------|-------------|

| React useState/useContext | 최소 | 로컬 상태 | 기본 |

| Zustand | 낮음 | 글로벌 상태 | 강력 추천 |

| Jotai | 낮음 | 원자적 상태 | 추천 |

| TanStack Query | 중간 | 서버 상태 | 필수 |

| Redux Toolkit | 높음 | 복잡한 상태 | 비추천 (초기) |

// Zustand 예시 - 간단한 스토어

interface CartStore {

items: Array<{ id: string; name: string; price: number; quantity: number }>

addItem: (item: { id: string; name: string; price: number }) => void

removeItem: (id: string) => void

clearCart: () => void

total: () => number

}

export const useCartStore = create<CartStore>()(

persist(

(set, get) => ({

items: [],

addItem: (item) =>

set((state) => {

const existing = state.items.find((i) => i.id === item.id)

if (existing) {

return {

items: state.items.map((i) =>

i.id === item.id ? { ...i, quantity: i.quantity + 1 } : i

),

}

}

return { items: [...state.items, { ...item, quantity: 1 }] }

}),

removeItem: (id) =>

set((state) => ({

items: state.items.filter((i) => i.id !== id),

})),

clearCart: () => set({ items: [] }),

total: () =>

get().items.reduce((sum, item) => sum + item.price * item.quantity, 0),

}),

{ name: 'cart-storage' }

)

)

7. 백엔드 스택

7.1 언어 비교 (스타트업 관점)

| 항목 | Node.js (TS) | Go | Python | Java/Kotlin |

|------|-------------|-----|--------|-------------|

| 개발 속도 | 빠름 | 중간 | 빠름 | 느림 |

| 성능 | 중간 | 매우 높음 | 낮음 | 높음 |

| 채용 난이도 | 쉬움 | 중간 | 쉬움 | 중간 |

| 풀스택 가능 | 예 (Next.js) | 아니요 | 제한적 | 아니요 |

| 생태계 | 매우 큼 | 큼 | 매우 큼 (ML) | 매우 큼 |

| 스타트업 추천도 | 최고 | 성능 중요 시 | ML/데이터 시 | 엔터프라이즈 |

7.2 스타트업별 추천

B2C SaaS → Node.js (TypeScript) + Next.js

├── 풀스택 한 명이 모든 것을 할 수 있음

├── Vercel 배포로 인프라 고민 최소화

└── 프론트엔드-백엔드 타입 공유

B2B SaaS → Node.js or Go

├── API 성능이 중요하면 Go

├── 빠른 기능 개발이 중요하면 Node.js

└── 둘 다 좋은 선택

AI/ML 스타트업 → Python + TypeScript

├── ML 파이프라인은 Python

├── API 서버는 FastAPI (Python) 또는 Next.js (TS)

└── 프론트엔드는 Next.js

핀테크 → Go or Java/Kotlin

├── 높은 성능과 안정성 필요

├── 정적 타입 시스템의 안전성

└── 금융 규제 대응에 강한 생태계

게이밍/리얼타임 → Go or Rust

├── 높은 동시성 처리

├── WebSocket 서버 성능

└── 낮은 지연시간

7.3 API 설계 패턴

// src/app/api/v1/products/route.ts

// 요청 검증 스키마

const createProductSchema = z.object({

name: z.string().min(1).max(200),

description: z.string().max(5000).optional(),

price: z.number().positive(),

category: z.string(),

tags: z.array(z.string()).max(10).optional(),

})

// GET /api/v1/products

export async function GET(req: NextRequest) {

const searchParams = req.nextUrl.searchParams

const page = parseInt(searchParams.get('page') || '1')

const limit = Math.min(parseInt(searchParams.get('limit') || '20'), 100)

const category = searchParams.get('category')

const supabase = await createClient()

let query = supabase

.from('products')

.select('*', { count: 'exact' })

.eq('status', 'active')

.range((page - 1) * limit, page * limit - 1)

.order('created_at', { ascending: false })

if (category) {

query = query.eq('category', category)

}

const { data, count, error } = await query

if (error) {

return NextResponse.json({ error: error.message }, { status: 500 })

}

return NextResponse.json({

data,

pagination: {

page,

limit,

total: count || 0,

totalPages: Math.ceil((count || 0) / limit),

},

})

}

// POST /api/v1/products

export async function POST(req: NextRequest) {

const supabase = await createClient()

const { data: { user } } = await supabase.auth.getUser()

if (!user) {

return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })

}

const body = await req.json()

const parsed = createProductSchema.safeParse(body)

if (!parsed.success) {

return NextResponse.json(

{ error: 'Validation failed', details: parsed.error.flatten() },

{ status: 400 }

)

}

const { data, error } = await supabase

.from('products')

.insert({ ...parsed.data, user_id: user.id })

.select()

.single()

if (error) {

return NextResponse.json({ error: error.message }, { status: 500 })

}

return NextResponse.json({ data }, { status: 201 })

}

8. 데이터베이스

8.1 PostgreSQL vs MySQL

| 항목 | PostgreSQL | MySQL |

|------|-----------|-------|

| JSON 지원 | JSONB (인덱싱 가능) | JSON (제한적) |

| 전문 검색 | 내장 (tsvector) | 내장 (FULLTEXT) |

| 확장성 | 확장 풍부 (PostGIS 등) | 제한적 |

| 복제 | 논리적/물리적 | 물리적 (기본) |

| 성능 | 읽기/쓰기 균형 | 읽기 최적화 |

| 스타트업 추천 | 강력 추천 | MySQL 경험 팀이면 OK |

8.2 BaaS 비교

| 서비스 | DB 엔진 | 가격 (시작) | 특징 | 스타트업 추천 |

|--------|---------|------------|------|-------------|

| Supabase | PostgreSQL | 무료 | Auth, Storage, Realtime 포함 | MVP 최고 |

| Neon | PostgreSQL | 무료 | 서버리스, 브랜칭 | 좋음 |

| PlanetScale | MySQL (Vitess) | 무료 | 브랜칭, 무중단 스키마 변경 | 좋음 |

| Turso | SQLite (libSQL) | 무료 | 에지, 매우 빠름 | 소규모 앱 |

| CockroachDB | PostgreSQL 호환 | 유료 | 분산 SQL, 글로벌 | 대규모 앱 |

8.3 Redis 도입 시점

Redis를 도입해야 하는 시점:

├── 동일 쿼리가 초당 100회 이상 실행될 때

├── API 응답 시간이 500ms를 넘길 때

├── 세션 관리가 필요할 때

├── 실시간 기능 (리더보드, 카운터 등)이 필요할 때

├── Rate Limiting이 필요할 때

└── 분산 잠금(Distributed Lock)이 필요할 때

Redis를 아직 도입하지 않아도 되는 시점:

├── DAU 1,000 이하

├── DB 쿼리가 20ms 이내

├── Next.js의 내장 캐시로 충분할 때

└── 비용이 제약이 될 때

9. 인프라

9.1 PaaS에서 IaaS로의 전환 시점

Vercel/Railway에서 AWS/GCP로 이동해야 하는 시점:

1. 비용 임계점

├── Vercel Pro 비용이 월 500달러를 넘길 때

├── 대역폭 비용이 컴퓨팅보다 클 때

└── 함수 실행 시간 제한에 걸릴 때

2. 기술적 요구사항

├── Long-running 프로세스가 필요할 때 (5분+ 이상)

├── WebSocket이 핵심 기능일 때

├── GPU 컴퓨팅이 필요할 때

└── 커스텀 네트워크 구성이 필요할 때

3. 규제/컴플라이언스

├── 데이터 저장 위치 규제

├── SOC 2, HIPAA 등 인증 필요

└── VPC 격리가 필요할 때

이동하지 않아도 되는 경우:

├── 월 비용 500달러 이하

├── 트래픽이 예측 가능하고 안정적

├── 서버리스 아키텍처로 충분

└── DevOps 전담 인력이 없음

9.2 클라우드 비교

| 항목 | AWS | GCP | Azure | Vercel |

|------|-----|-----|-------|--------|

| 스타트업 크레딧 | 10K-100K | 200K (구글 포 스타트업) | 150K | 없음 (무료 티어) |

| 서비스 다양성 | 최고 | 높음 | 높음 | 프론트엔드 특화 |

| 학습 곡선 | 높음 | 중간 | 높음 | 낮음 |

| 한국 리전 | 서울 | 서울 | 부산 | 인천 (Edge) |

| 스타트업 추천 | 시리즈 A+ | ML 중심 | 엔터프라이즈 | MVP-PMF |

9.3 비용 최적화 체크리스트

1. **Spot/Preemptible 인스턴스** 활용 (최대 90% 절약)

2. **Reserved Instances** 1년 약정 (최대 40% 절약)

3. **Right-sizing**: 실제 사용량 기반 인스턴스 크기 조정

4. **Auto-scaling**: 트래픽에 따른 자동 스케일링

5. **S3 수명 주기 정책**: 오래된 데이터를 Glacier로 이동

6. **CloudFront 캐싱**: 오리진 요청 최소화

7. **NAT Gateway 비용 주의**: 데이터 전송 비용이 의외로 큼

8. **개발/스테이징 환경**: 야간/주말 자동 중단

10. 채용과 기술 스택

10.1 기술 스택이 채용에 미치는 영향

기술 스택 선택은 곧 채용 풀을 결정합니다:

| 기술 | 한국 개발자 풀 | 주니어 비율 | 시니어 채용 난이도 |

|------|--------------|-----------|-----------------|

| React/Next.js | 매우 큼 | 높음 | 중간 |

| TypeScript | 큼 | 중간 | 중간 |

| Python | 매우 큼 | 높음 | ML 분야 어려움 |

| Go | 작음 | 낮음 | 높음 |

| Rust | 매우 작음 | 매우 낮음 | 매우 높음 |

| Java/Kotlin | 매우 큼 | 높음 | 중간 |

| PostgreSQL | 큼 | 중간 | 중간 |

| Kubernetes | 중간 | 낮음 | 높음 |

10.2 채용 친화적 기술 스택

채용이 쉬운 스택 (한국 기준):

├── Next.js + TypeScript (프론트)

├── Node.js + Express/Nest.js (백엔드)

├── PostgreSQL / MySQL (DB)

├── Redis (캐시)

├── Docker + GitHub Actions (DevOps)

└── AWS (인프라)

채용이 어려운 스택:

├── Svelte / SolidJS (프론트)

├── Rust / Elixir (백엔드)

├── CockroachDB / ScyllaDB (DB)

├── Nomad / Consul (오케스트레이션)

└── Pulumi / CDK (IaC)

10.3 기술 면접에서 스택 설명하기

채용 과정에서 기술 스택 선택 이유를 설명하는 것이 중요합니다:

- **왜 이 기술을 선택했는지** (비즈니스 맥락)

- **트레이드오프는 무엇이었는지** (선택하지 않은 기술)

- **언제 변경할 계획인지** (기술 로드맵)

11. 흔한 실수

11.1 이력서 주도 개발 (Resume-Driven Development)

잘못된 의사결정 패턴:

├── "K8s 경험을 쌓고 싶어서" → K8s 도입 (DAU 100)

├── "Rust가 트렌드라서" → Rust로 API 서버 작성 (팀 2명)

├── "MSA가 정석이라서" → 3명이 10개 서비스 운영

├── "GraphQL이 좋다고 해서" → REST로 충분한 CRUD에 GraphQL

└── "모노레포가 구글에서 쓰니까" → 3명 팀에 Turborepo 셋업

올바른 의사결정:

├── "이 문제를 해결하는 가장 간단한 방법은?"

├── "6개월 뒤에도 유지보수할 수 있는가?"

├── "팀원들이 이미 익숙한 기술인가?"

├── "채용이 용이한 기술인가?"

└── "전환 비용이 감당 가능한가?"

11.2 조기 최적화

- DB 인덱스 40개 미리 생성 (쿼리 패턴도 모르는 상태에서)

- 캐싱 레이어 3단계 구축 (트래픽 100명/일)

- 글로벌 CDN 설정 (사용자 모두 한국)

- 로드 밸런서 + Auto Scaling (CPU 사용률 5%)

11.3 조기 마이크로서비스

3명의 팀이 마이크로서비스를 도입하면:

- 서비스 간 통신 디버깅에 하루 종일

- 분산 트랜잭션 문제로 데이터 불일치

- 배포 파이프라인 10개 관리

- 모니터링 대시보드 세팅만 2주

- 결과: 개발 속도 50% 감소, 인프라 비용 300% 증가

11.4 기술 부채를 무시하는 것

반대로, 기술 부채를 너무 쌓는 것도 문제입니다:

- 테스트 없이 6개월 개발 후 리팩토링 불가능

- 타입 없이 JavaScript로 만 줄 코드 작성

- 하드코딩된 설정값이 곳곳에 산재

- 로깅/모니터링 없이 운영하다 장애 대응 불가

> **균형**: MVP 단계에서는 80/20 법칙 적용. 20%의 핵심 코드에 80%의 품질을 투자하세요.

12. 실제 사례

12.1 Vercel 스택

Vercel (Next.js 개발사):

├── Frontend: Next.js (자사 제품)

├── Backend: Go + Node.js

├── Database: PlanetScale (MySQL) → Neon (PostgreSQL)

├── Cache: Redis (Upstash)

├── Search: Turborepo 기반 모노레포

├── Deploy: 자사 플랫폼

├── Monitoring: 자체 구축 + Datadog

└── 특징: 자사 제품을 적극 활용 (dogfooding)

12.2 Linear 스택

Linear (프로젝트 관리 도구):

├── Frontend: React + TypeScript

├── Backend: Node.js + TypeScript

├── Database: PostgreSQL

├── Cache: Redis

├── Sync: CRDT 기반 실시간 동기화

├── Deploy: Google Cloud

├── Monitoring: 자체 구축

└── 특징: 오프라인 퍼스트, 로컬 데이터 우선

12.3 Cal.com 스택

Cal.com (오픈소스 일정 관리):

├── Frontend: Next.js + TypeScript

├── Backend: tRPC + Prisma

├── Database: PostgreSQL

├── Auth: NextAuth.js

├── Email: 다양한 프로바이더 지원

├── Deploy: Vercel

├── Monorepo: Turborepo

└── 특징: 오픈소스, 모노레포, tRPC로 타입 안전

12.4 시사점

이 사례들의 공통점:

1. **검증된 기술** 위주 (PostgreSQL, Redis, TypeScript)

2. **모놀리스 또는 모듈러 모놀리스**로 시작

3. **TypeScript**를 기본으로 채택

4. **PaaS 우선** (Vercel, Google Cloud 매니지드)

5. 핵심 차별화 포인트에만 혁신 투자

13. 퀴즈

**A:** 혁신 토큰은 Dan McKinley가 제안한 개념으로, 팀이 감당할 수 있는 새로운/검증되지 않은 기술의 수를 의미합니다. 모든 팀에는 제한된 수(보통 3개 정도)의 혁신 토큰이 있으며, 검증되지 않은 기술을 도입할 때마다 하나씩 소비됩니다.

아껴야 하는 이유: 스타트업의 비즈니스 모델 자체가 이미 하나의 혁신이므로 토큰 1개를 사용합니다. 나머지 토큰을 기술 스택에서 모두 소진하면, 문제 발생 시 대응할 여력이 없습니다. 검증된(지루한) 기술은 예측 가능한 문제만 발생하고, 해결 방법이 이미 알려져 있어 운영 부담을 줄여줍니다.

**A:** 이 조합이 추천되는 이유:

1. **개발 속도**: Next.js의 풀스택 기능으로 프론트/백 분리 없이 개발 가능

2. **비용**: 세 서비스 모두 무료 티어가 충분히 관대 (월 0-50달러)

3. **인프라 관리 제로**: Vercel이 배포/CDN/SSL 모두 처리, Supabase가 DB/Auth/Storage 처리

4. **타입 안전성**: TypeScript로 풀스택 타입 공유

5. **확장성**: Supabase는 PostgreSQL 기반이라 추후 마이그레이션 용이

6. **생태계**: shadcn/ui, Stripe, Resend 등 통합이 매우 쉬움

7. **채용**: React/Next.js 개발자 풀이 가장 큼

핵심은 PMF를 찾기 전에 인프라에 시간을 쓰지 않는 것입니다.

**A:** 전환 시점 판단 기준:

1. **비용**: Vercel 비용이 월 500달러 이상일 때 (동일 워크로드를 AWS에서 더 저렴하게 운영 가능)

2. **기술 제약**: 5분 이상의 Long-running 프로세스, WebSocket 기반 실시간 기능, GPU 컴퓨팅 필요 시

3. **규제**: 데이터 저장 위치 규제, SOC 2/HIPAA 인증, VPC 격리 필요 시

4. **팀**: DevOps/인프라 전담 인력이 확보되었을 때

전환하지 않아도 되는 경우: 월 비용 500달러 이하, 서버리스 아키텍처로 충분, DevOps 전담자 없음. 핵심은 "필요할 때" 전환하는 것이지, 미리 준비하는 것이 아닙니다.

**A:** 처음부터 마이크로서비스를 도입하면 안 되는 이유:

1. **서비스 경계 실패**: PMF 전에는 도메인이 계속 변하므로 서비스 경계를 올바르게 정하기 불가능

2. **운영 오버헤드**: 서비스 간 통신, 분산 트랜잭션, 서비스 디스커버리 등 복잡도 폭발

3. **디버깅 어려움**: 분산 시스템 디버깅은 모놀리스보다 10배 어려움

4. **인프라 비용**: 각 서비스별 배포 파이프라인, 모니터링, 로깅 필요 (비용 3배 이상)

5. **팀 규모 미스매치**: 3-5명 팀이 10개 서비스를 관리하는 것은 비현실적

올바른 접근: 모놀리스로 시작, 도메인 이해가 충분해진 후 병목 지점만 선택적으로 서비스 분리.

**A:** 기술 스택은 채용에 직접적인 영향을 미칩니다:

1. **채용 풀 크기**: React/TypeScript 개발자는 매우 많지만, Rust/Elixir 개발자는 극소수

2. **채용 속도**: 인기 기술 스택은 지원자가 많아 빠르게 채용 가능

3. **채용 비용**: 희귀 기술은 프리미엄 연봉 필요 (Go 시니어 > Node.js 시니어 by 20-30%)

4. **온보딩**: 팀원이 이미 아는 기술이면 온보딩 1주 이내, 새로운 기술이면 1개월 이상

5. **이직률**: 시장에서 인기 없는 기술은 커리어 불안으로 이직률 상승

추천: 한국 기준으로 TypeScript + React + PostgreSQL + AWS는 가장 채용이 용이한 조합입니다.

참고 자료

1. McKinley, D. "Choose Boring Technology." https://boringtechnology.club/

2. Fowler, M. "Monolith First." Martin Fowler's blog.

3. Next.js Documentation (2025). https://nextjs.org/docs

4. Supabase Documentation. https://supabase.com/docs

5. Vercel Documentation. https://vercel.com/docs

6. Stripe Documentation. https://stripe.com/docs

7. Terraform AWS Provider Documentation.

8. "The Twelve-Factor App." https://12factor.net/

9. Kleppmann, M. "Designing Data-Intensive Applications." O'Reilly.

10. Newman, S. "Building Microservices, 2nd Edition." O'Reilly.

11. Basecamp. "Getting Real." https://basecamp.com/gettingreal

12. Linear Engineering Blog. https://linear.app/blog

13. Cal.com GitHub Repository. https://github.com/calcom/cal.com

14. AWS Startup Guides. https://aws.amazon.com/startups/

15. Google for Startups Cloud Program. https://cloud.google.com/startup

16. Y Combinator Library - Technical Decisions. https://www.ycombinator.com/library

현재 단락 (1/1109)

Dan McKinley의 "Choose Boring Technology" 에세이는 스타트업 기술 선택의 바이블입니다. 핵심 주장은 이렇습니다:

작성 글자: 0원문 글자: 27,088작성 단락: 0/1109