쇼핑몰 구축 프로세스 단계별 가이드 - 20주 완성 로드맵

쇼핑몰개발프로세스애자일방법론프로젝트관리개발로드맵단계별가이드

쇼핑몰 개발 20주 완성 로드맵 🗓️

"체계적인 프로세스가 성공의 열쇠다" 15년 경력 PM이 검증한 단계별 가이드를 공개합니다!

🎯 전체 프로젝트 개요

20주 마스터 플랜

🏗️ 쇼핑몰 개발 20주 로드맵

Phase 1: Discovery & Planning (Week 1-4)
├── Week 1-2: 비즈니스 분석 및 요구사항 정의
├── Week 3: 기술 아키텍처 설계
└── Week 4: UI/UX 디자인 기획

Phase 2: Foundation Development (Week 5-8)
├── Week 5-6: 개발 환경 구축 및 기본 프레임워크
├── Week 7: 사용자 인증 시스템
└── Week 8: 상품 관리 시스템

Phase 3: Core Features (Week 9-14)
├── Week 9-10: 장바구니 및 주문 시스템
├── Week 11-12: 결제 시스템 연동
├── Week 13: 관리자 페이지
└── Week 14: 모바일 최적화

Phase 4: Advanced Features (Week 15-17)
├── Week 15: 검색 및 필터링
├── Week 16: 추천 시스템
└── Week 17: 마케팅 도구

Phase 5: Testing & Launch (Week 18-20)
├── Week 18: 통합 테스트 및 QA
├── Week 19: 성능 최적화
└── Week 20: 배포 및 런칭

📋 Phase 1: Discovery & Planning (Week 1-4)

Week 1-2: 비즈니스 분석 및 요구사항 정의

주요 산출물

📊 Week 1-2 Deliverables
├── 비즈니스 요구사항 명세서 (BRS)
├── 기능 요구사항 명세서 (FRS)
├── 사용자 페르소나 (3-5개)
├── 사용자 여정 지도 (Customer Journey Map)
└── 경쟁사 분석 리포트

상세 활동 계획

// Week 1-2 액션 아이템
const week12Activities = {
  stakeholderMeeting: {
    participants: ["CEO", "CTO", "마케팅 팀", "영업 팀"],
    agenda: [
      "비즈니스 목표 및 KPI 정의",
      "타겟 고객 식별",
      "경쟁 우위 요소 파악",
      "예산 및 일정 확정"
    ],
    duration: "4시간",
    outcome: "프로젝트 헌장 작성"
  },

  marketResearch: {
    activities: [
      "경쟁사 5개 업체 심층 분석",
      "타겟 고객 인터뷰 20건",
      "시장 동향 조사",
      "기술 트렌드 분석"
    ],
    tools: ["Google Analytics", "SimilarWeb", "설문조사"],
    output: "시장 분석 리포트"
  },

  requirementAnalysis: {
    methods: ["브레인스토밍", "사용자 스토리 작성", "우선순위 매트릭스"],
    categories: [
      "핵심 기능 (Must-have)",
      "중요 기능 (Should-have)",
      "선택 기능 (Could-have)",
      "미래 기능 (Won't-have)"
    ]
  }
};

핵심 질문 리스트

❓ 비즈니스 핵심 질문들
☐ 주요 타겟 고객층은 누구인가?
☐ 핵심 차별화 포인트는 무엇인가?
☐ 예상 일일/월 방문자 수는?
☐ 주요 수익 모델은?
☐ 필수 연동 시스템은? (ERP, POS, 배송 등)
☐ 글로벌 진출 계획이 있는가?
☐ 모바일 앱이 필요한가?
☐ 예상 상품 개수는?
☐ 고객 서비스 정책은?
☐ 브랜딩 가이드라인은?

Week 3: 기술 아키텍처 설계

시스템 아키텍처 설계

// 추천 기술 스택
const recommendedTechStack = {
  frontend: {
    framework: "React 18 + Next.js 14",
    styling: "Tailwind CSS + Headless UI",
    stateManagement: "Zustand (가벼움) 또는 Redux Toolkit",
    testing: "Jest + React Testing Library",
    tools: ["TypeScript", "ESLint", "Prettier"]
  },

  backend: {
    runtime: "Node.js 20 LTS",
    framework: "Express.js + Helmet (보안)",
    database: "PostgreSQL (주) + Redis (캐시)",
    orm: "Prisma (타입 안전성)",
    authentication: "JWT + Passport.js",
    fileUpload: "AWS S3 + CloudFront",
    search: "Elasticsearch (상품 검색용)"
  },

  infrastructure: {
    hosting: "AWS (EC2 + RDS + S3)",
    cicd: "GitHub Actions",
    monitoring: "DataDog 또는 New Relic",
    security: "AWS WAF + SSL/TLS",
    backup: "자동 백업 (일 1회)"
  },

  thirdParty: {
    payment: "토스페이먼츠 + PayPal (글로벌)",
    shipping: "CJ대한통운 + 로젠택배",
    analytics: "Google Analytics 4",
    email: "SendGrid 또는 AWS SES",
    sms: "NCS (알리고) 또는 Twilio"
  }
};

데이터베이스 설계

-- 핵심 테이블 스키마 예시
-- Users 테이블
CREATE TABLE users (
    id SERIAL PRIMARY KEY,
    email VARCHAR(255) UNIQUE NOT NULL,
    password_hash VARCHAR(255),
    name VARCHAR(100) NOT NULL,
    phone VARCHAR(20),
    birth_date DATE,
    gender CHAR(1),
    status VARCHAR(20) DEFAULT 'active',
    email_verified BOOLEAN DEFAULT false,
    social_provider VARCHAR(50),
    social_id VARCHAR(100),
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    INDEX idx_email (email),
    INDEX idx_status (status)
);

-- Products 테이블
CREATE TABLE products (
    id SERIAL PRIMARY KEY,
    sku VARCHAR(100) UNIQUE NOT NULL,
    name VARCHAR(255) NOT NULL,
    description TEXT,
    category_id INTEGER REFERENCES categories(id),
    brand VARCHAR(100),
    price DECIMAL(10,2) NOT NULL,
    sale_price DECIMAL(10,2),
    cost DECIMAL(10,2),
    stock INTEGER DEFAULT 0,
    reserved_stock INTEGER DEFAULT 0,
    weight DECIMAL(8,2),
    status VARCHAR(20) DEFAULT 'draft',
    featured BOOLEAN DEFAULT false,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    FULLTEXT(name, description),
    INDEX idx_category (category_id),
    INDEX idx_status (status),
    INDEX idx_price (price)
);

-- Orders 테이블
CREATE TABLE orders (
    id SERIAL PRIMARY KEY,
    user_id INTEGER REFERENCES users(id),
    order_number VARCHAR(50) UNIQUE NOT NULL,
    status VARCHAR(30) DEFAULT 'pending',
    subtotal DECIMAL(10,2) NOT NULL,
    shipping_cost DECIMAL(10,2) DEFAULT 0,
    tax_amount DECIMAL(10,2) DEFAULT 0,
    total_amount DECIMAL(10,2) NOT NULL,
    currency VARCHAR(3) DEFAULT 'KRW',
    shipping_address JSONB,
    billing_address JSONB,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    INDEX idx_user_id (user_id),
    INDEX idx_status (status),
    INDEX idx_created_at (created_at)
);

Week 4: UI/UX 디자인 기획

디자인 시스템 구축

/* 디자인 토큰 정의 */
:root {
  /* Colors */
  --primary-50: #eff6ff;
  --primary-100: #dbeafe;
  --primary-500: #3b82f6;
  --primary-600: #2563eb;
  --primary-900: #1e3a8a;

  --gray-50: #f9fafb;
  --gray-100: #f3f4f6;
  --gray-500: #6b7280;
  --gray-900: #111827;

  --success-500: #10b981;
  --warning-500: #f59e0b;
  --error-500: #ef4444;

  /* Typography */
  --font-family-sans: 'Pretendard', -apple-system, system-ui, sans-serif;
  --font-size-xs: 0.75rem;
  --font-size-sm: 0.875rem;
  --font-size-base: 1rem;
  --font-size-lg: 1.125rem;
  --font-size-xl: 1.25rem;
  --font-size-2xl: 1.5rem;
  --font-size-3xl: 1.875rem;

  /* Spacing */
  --space-1: 0.25rem;
  --space-2: 0.5rem;
  --space-3: 0.75rem;
  --space-4: 1rem;
  --space-6: 1.5rem;
  --space-8: 2rem;
  --space-12: 3rem;

  /* Border Radius */
  --radius-sm: 0.25rem;
  --radius-md: 0.375rem;
  --radius-lg: 0.5rem;
  --radius-xl: 0.75rem;

  /* Shadows */
  --shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
  --shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
  --shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1);
}

주요 페이지 와이어프레임

📱 필수 페이지 리스트
├── 홈페이지 (메인)
├── 상품 목록 페이지
├── 상품 상세 페이지
├── 장바구니 페이지
├── 결제 페이지
├── 주문 완료 페이지
├── 마이페이지
├── 로그인/회원가입 페이지
├── 고객센터 페이지
└── 관리자 대시보드 (10+ 페이지)

🛠️ Phase 2: Foundation Development (Week 5-8)

Week 5-6: 개발 환경 구축

프로젝트 초기 설정

# Next.js 프로젝트 생성
npx create-next-app@latest ecommerce-project \
  --typescript \
  --tailwind \
  --eslint \
  --app \
  --src-dir \
  --import-alias "@/*"

cd ecommerce-project

# 필수 의존성 설치
npm install \
  @prisma/client prisma \
  next-auth \
  bcryptjs \
  jsonwebtoken \
  zod \
  react-hook-form \
  @hookform/resolvers \
  zustand \
  date-fns \
  lucide-react

# 개발 도구 설치
npm install -D \
  @types/bcryptjs \
  @types/jsonwebtoken \
  husky \
  lint-staged \
  prettier

프로젝트 구조

src/
├── app/                 # Next.js 13+ App Directory
│   ├── (auth)/         # 인증 관련 페이지 그룹
│   ├── (shop)/         # 쇼핑몰 페이지 그룹
│   ├── admin/          # 관리자 페이지
│   ├── api/            # API 라우트
│   ├── globals.css     # 글로벌 스타일
│   ├── layout.tsx      # 루트 레이아웃
│   └── page.tsx        # 홈페이지
├── components/         # 재사용 가능한 컴포넌트
│   ├── ui/            # 기본 UI 컴포넌트
│   ├── forms/         # 폼 컴포넌트
│   ├── layout/        # 레이아웃 컴포넌트
│   └── features/      # 기능별 컴포넌트
├── lib/               # 유틸리티 및 설정
│   ├── db.ts         # 데이터베이스 연결
│   ├── auth.ts       # 인증 설정
│   ├── utils.ts      # 공통 유틸리티
│   └── validations.ts # 스키마 검증
├── types/             # TypeScript 타입 정의
├── hooks/             # 커스텀 훅
└── stores/            # 상태 관리 스토어

CI/CD 파이프라인 설정

# .github/workflows/ci-cd.yml
name: CI/CD Pipeline

on:
  push:
    branches: [ main, develop ]
  pull_request:
    branches: [ main ]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: '20'
          cache: 'npm'

      - name: Install dependencies
        run: npm ci

      - name: Run linting
        run: npm run lint

      - name: Run type checking
        run: npm run type-check

      - name: Run tests
        run: npm run test

      - name: Build application
        run: npm run build

  deploy:
    needs: test
    runs-on: ubuntu-latest
    if: github.ref == 'refs/heads/main'
    steps:
      - uses: actions/checkout@v4

      - name: Deploy to production
        run: |
          echo "Production deployment script"
          # 실제 배포 스크립트 구현

Week 7: 사용자 인증 시스템

NextAuth.js 설정

// src/lib/auth.ts
import NextAuth from 'next-auth'
import CredentialsProvider from 'next-auth/providers/credentials'
import KakaoProvider from 'next-auth/providers/kakao'
import GoogleProvider from 'next-auth/providers/google'
import { PrismaAdapter } from '@next-auth/prisma-adapter'
import { db } from './db'
import bcrypt from 'bcryptjs'

export const authOptions = {
  adapter: PrismaAdapter(db),
  session: {
    strategy: 'jwt' as const,
  },
  providers: [
    CredentialsProvider({
      name: 'credentials',
      credentials: {
        email: { label: 'Email', type: 'email' },
        password: { label: 'Password', type: 'password' }
      },
      async authorize(credentials) {
        if (!credentials?.email || !credentials?.password) {
          return null
        }

        const user = await db.user.findUnique({
          where: { email: credentials.email }
        })

        if (!user) {
          return null
        }

        const isPasswordValid = await bcrypt.compare(
          credentials.password,
          user.passwordHash
        )

        if (!isPasswordValid) {
          return null
        }

        return {
          id: user.id,
          email: user.email,
          name: user.name,
          role: user.role,
        }
      }
    }),
    KakaoProvider({
      clientId: process.env.KAKAO_CLIENT_ID!,
      clientSecret: process.env.KAKAO_CLIENT_SECRET!,
    }),
    GoogleProvider({
      clientId: process.env.GOOGLE_CLIENT_ID!,
      clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
    })
  ],
  callbacks: {
    async jwt({ token, user }) {
      if (user) {
        token.role = user.role
      }
      return token
    },
    async session({ session, token }) {
      if (token) {
        session.user.id = token.sub!
        session.user.role = token.role as string
      }
      return session
    }
  },
  pages: {
    signIn: '/auth/signin',
    signUp: '/auth/signup',
  }
}

export default NextAuth(authOptions)

Week 8: 상품 관리 시스템

상품 API 개발

// src/app/api/products/route.ts
import { NextRequest, NextResponse } from 'next/server'
import { db } from '@/lib/db'
import { z } from 'zod'

const productQuerySchema = z.object({
  page: z.coerce.number().min(1).default(1),
  limit: z.coerce.number().min(1).max(100).default(20),
  category: z.string().optional(),
  search: z.string().optional(),
  sortBy: z.enum(['name', 'price', 'createdAt', 'sales']).default('createdAt'),
  sortOrder: z.enum(['asc', 'desc']).default('desc'),
  minPrice: z.coerce.number().min(0).optional(),
  maxPrice: z.coerce.number().min(0).optional(),
})

export async function GET(request: NextRequest) {
  try {
    const { searchParams } = new URL(request.url)
    const params = Object.fromEntries(searchParams)

    const validatedParams = productQuerySchema.parse(params)

    const {
      page,
      limit,
      category,
      search,
      sortBy,
      sortOrder,
      minPrice,
      maxPrice
    } = validatedParams

    // 쿼리 조건 구성
    const where: any = {
      status: 'published',
    }

    if (category) {
      where.categoryId = category
    }

    if (search) {
      where.OR = [
        { name: { contains: search, mode: 'insensitive' } },
        { description: { contains: search, mode: 'insensitive' } },
      ]
    }

    if (minPrice !== undefined || maxPrice !== undefined) {
      where.price = {}
      if (minPrice !== undefined) where.price.gte = minPrice
      if (maxPrice !== undefined) where.price.lte = maxPrice
    }

    // 정렬 조건
    const orderBy: any = {}
    orderBy[sortBy] = sortOrder

    // 페이지네이션
    const skip = (page - 1) * limit

    // 상품 조회
    const [products, total] = await Promise.all([
      db.product.findMany({
        where,
        orderBy,
        skip,
        take: limit,
        include: {
          category: {
            select: {
              id: true,
              name: true,
              slug: true,
            }
          },
          images: {
            where: { isPrimary: true },
            take: 1,
          }
        }
      }),
      db.product.count({ where })
    ])

    const totalPages = Math.ceil(total / limit)

    return NextResponse.json({
      products,
      pagination: {
        currentPage: page,
        totalPages,
        totalProducts: total,
        hasNext: page < totalPages,
        hasPrev: page > 1,
      }
    })

  } catch (error) {
    console.error('Products API error:', error)

    if (error instanceof z.ZodError) {
      return NextResponse.json(
        { error: '잘못된 요청 매개변수입니다.', details: error.errors },
        { status: 400 }
      )
    }

    return NextResponse.json(
      { error: '상품 조회 중 오류가 발생했습니다.' },
      { status: 500 }
    )
  }
}

⚙️ Phase 3: Core Features (Week 9-14)

Week 9-10: 장바구니 및 주문 시스템

장바구니 상태 관리

// src/stores/cartStore.ts
import { create } from 'zustand'
import { persist } from 'zustand/middleware'

interface CartItem {
  productId: string
  name: string
  price: number
  quantity: number
  options?: Record<string, string>
  image?: string
}

interface CartState {
  items: CartItem[]
  isOpen: boolean
  addItem: (item: CartItem) => void
  removeItem: (productId: string, options?: Record<string, string>) => void
  updateQuantity: (productId: string, quantity: number, options?: Record<string, string>) => void
  clearCart: () => void
  toggleCart: () => void
  getTotalItems: () => number
  getTotalPrice: () => number
}

export const useCartStore = create<CartState>()(
  persist(
    (set, get) => ({
      items: [],
      isOpen: false,

      addItem: (newItem) =>
        set((state) => {
          const existingItemIndex = state.items.findIndex(
            (item) =>
              item.productId === newItem.productId &&
              JSON.stringify(item.options) === JSON.stringify(newItem.options)
          )

          if (existingItemIndex >= 0) {
            // 기존 아이템 수량 증가
            const updatedItems = [...state.items]
            updatedItems[existingItemIndex].quantity += newItem.quantity
            return { items: updatedItems }
          } else {
            // 새 아이템 추가
            return { items: [...state.items, newItem] }
          }
        }),

      removeItem: (productId, options) =>
        set((state) => ({
          items: state.items.filter(
            (item) =>
              !(item.productId === productId &&
                JSON.stringify(item.options) === JSON.stringify(options))
          ),
        })),

      updateQuantity: (productId, quantity, options) =>
        set((state) => ({
          items: state.items.map((item) =>
            item.productId === productId &&
            JSON.stringify(item.options) === JSON.stringify(options)
              ? { ...item, quantity }
              : item
          ),
        })),

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

      toggleCart: () => set((state) => ({ isOpen: !state.isOpen })),

      getTotalItems: () => {
        return get().items.reduce((total, item) => total + item.quantity, 0)
      },

      getTotalPrice: () => {
        return get().items.reduce((total, item) => total + (item.price * item.quantity), 0)
      },
    }),
    {
      name: 'cart-storage',
      partialize: (state) => ({ items: state.items }),
    }
  )
)

주문 처리 시스템

// src/app/api/orders/route.ts
import { NextRequest, NextResponse } from 'next/server'
import { getServerSession } from 'next-auth'
import { authOptions } from '@/lib/auth'
import { db } from '@/lib/db'
import { z } from 'zod'

const createOrderSchema = z.object({
  items: z.array(z.object({
    productId: z.string(),
    quantity: z.number().min(1),
    options: z.record(z.string()).optional(),
  })),
  shippingAddress: z.object({
    name: z.string(),
    phone: z.string(),
    address: z.string(),
    addressDetail: z.string().optional(),
    zipCode: z.string(),
  }),
  billingAddress: z.object({
    name: z.string(),
    phone: z.string(),
    address: z.string(),
    addressDetail: z.string().optional(),
    zipCode: z.string(),
  }).optional(),
  deliveryMemo: z.string().optional(),
})

export async function POST(request: NextRequest) {
  try {
    const session = await getServerSession(authOptions)
    if (!session?.user?.id) {
      return NextResponse.json(
        { error: '로그인이 필요합니다.' },
        { status: 401 }
      )
    }

    const body = await request.json()
    const validatedData = createOrderSchema.parse(body)

    // 상품 정보 및 재고 확인
    const productIds = validatedData.items.map(item => item.productId)
    const products = await db.product.findMany({
      where: {
        id: { in: productIds },
        status: 'published',
      }
    })

    if (products.length !== productIds.length) {
      return NextResponse.json(
        { error: '일부 상품을 찾을 수 없습니다.' },
        { status: 400 }
      )
    }

    // 주문 아이템 생성 및 총액 계산
    let subtotal = 0
    const orderItems = []

    for (const item of validatedData.items) {
      const product = products.find(p => p.id === item.productId)!

      // 재고 확인
      const availableStock = product.stock - product.reservedStock
      if (product.trackInventory && availableStock < item.quantity) {
        return NextResponse.json(
          { error: `${product.name}의 재고가 부족합니다. 현재 재고: ${availableStock}개` },
          { status: 400 }
        )
      }

      const itemPrice = product.salePrice || product.price
      const itemTotal = itemPrice * item.quantity

      orderItems.push({
        productId: item.productId,
        productName: product.name,
        productSku: product.sku,
        quantity: item.quantity,
        price: itemPrice,
        total: itemTotal,
        options: item.options || {},
      })

      subtotal += itemTotal
    }

    // 배송비 계산 (예: 50,000원 이상 무료배송)
    const shippingCost = subtotal >= 50000 ? 0 : 3000

    // 세금 계산 (부가세 10%)
    const taxAmount = Math.floor(subtotal * 0.1)

    const totalAmount = subtotal + shippingCost + taxAmount

    // 주문번호 생성
    const orderNumber = `ORD-${Date.now()}-${Math.random().toString(36).substr(2, 9).toUpperCase()}`

    // 데이터베이스 트랜잭션으로 주문 생성
    const order = await db.$transaction(async (tx) => {
      // 주문 생성
      const createdOrder = await tx.order.create({
        data: {
          userId: session.user.id,
          orderNumber,
          status: 'pending',
          subtotal,
          shippingCost,
          taxAmount,
          totalAmount,
          currency: 'KRW',
          shippingAddress: validatedData.shippingAddress,
          billingAddress: validatedData.billingAddress || validatedData.shippingAddress,
          deliveryMemo: validatedData.deliveryMemo,
          items: {
            create: orderItems
          }
        },
        include: {
          items: true
        }
      })

      // 재고 예약
      for (const item of validatedData.items) {
        await tx.product.update({
          where: { id: item.productId },
          data: {
            reservedStock: {
              increment: item.quantity
            }
          }
        })
      }

      return createdOrder
    })

    // 주문 확인 이메일 발송 (비동기)
    sendOrderConfirmationEmail(order.id).catch(console.error)

    return NextResponse.json({
      success: true,
      order: {
        id: order.id,
        orderNumber: order.orderNumber,
        totalAmount: order.totalAmount,
        status: order.status,
      }
    }, { status: 201 })

  } catch (error) {
    console.error('Create order error:', error)

    if (error instanceof z.ZodError) {
      return NextResponse.json(
        { error: '잘못된 주문 데이터입니다.', details: error.errors },
        { status: 400 }
      )
    }

    return NextResponse.json(
      { error: '주문 생성 중 오류가 발생했습니다.' },
      { status: 500 }
    )
  }
}

// 주문 확인 이메일 발송
async function sendOrderConfirmationEmail(orderId: string) {
  // 이메일 발송 로직 구현
  console.log(`Sending order confirmation email for order: ${orderId}`)
}

Week 11-12: 결제 시스템 연동

토스페이먼츠 결제 위젯

// src/components/payment/TossPaymentWidget.tsx
'use client'

import { useEffect, useRef } from 'react'
import { loadTossPayments } from '@tosspayments/payment-sdk'

interface TossPaymentWidgetProps {
  clientKey: string
  amount: number
  orderId: string
  orderName: string
  customerEmail: string
  customerName: string
  onPaymentSuccess: (data: any) => void
  onPaymentFail: (data: any) => void
}

export default function TossPaymentWidget({
  clientKey,
  amount,
  orderId,
  orderName,
  customerEmail,
  customerName,
  onPaymentSuccess,
  onPaymentFail,
}: TossPaymentWidgetProps) {
  const paymentRef = useRef<HTMLDivElement>(null)
  const tossPayments = useRef<any>(null)

  useEffect(() => {
    async function initializeTossPayments() {
      try {
        tossPayments.current = await loadTossPayments(clientKey)

        // 결제 위젯 렌더링
        tossPayments.current.renderPaymentMethods({
          selector: paymentRef.current,
          variantKey: 'DEFAULT',
          options: {
            amount,
            currency: 'KRW',
            country: 'KR',
          }
        })

        // 약관 동의 위젯 렌더링
        tossPayments.current.renderAgreement({
          selector: '#agreement',
          variantKey: 'AGREEMENT'
        })

      } catch (error) {
        console.error('토스페이먼츠 초기화 실패:', error)
      }
    }

    initializeTossPayments()
  }, [clientKey, amount])

  const handlePayment = async () => {
    if (!tossPayments.current) {
      alert('결제 시스템이 준비되지 않았습니다.')
      return
    }

    try {
      await tossPayments.current.requestPayment({
        amount,
        orderId,
        orderName,
        customerName,
        customerEmail,
        successUrl: `${window.location.origin}/payment/success`,
        failUrl: `${window.location.origin}/payment/fail`,
      })
    } catch (error) {
      console.error('결제 요청 실패:', error)
      onPaymentFail(error)
    }
  }

  return (
    <div className="max-w-md mx-auto">
      {/* 결제 수단 위젯 */}
      <div ref={paymentRef} className="mb-6" />

      {/* 약관 동의 위젯 */}
      <div id="agreement" className="mb-6" />

      {/* 결제 버튼 */}
      <button
        onClick={handlePayment}
        className="w-full bg-blue-600 text-white py-3 rounded-lg font-medium hover:bg-blue-700 transition-colors"
      >
        {amount.toLocaleString()}원 결제하기
      </button>
    </div>
  )
}

Week 13: 관리자 페이지

관리자 대시보드

// src/app/admin/dashboard/page.tsx
import { Suspense } from 'react'
import { getServerSession } from 'next-auth'
import { authOptions } from '@/lib/auth'
import { redirect } from 'next/navigation'
import { db } from '@/lib/db'
import DashboardStats from '@/components/admin/DashboardStats'
import RecentOrders from '@/components/admin/RecentOrders'
import SalesChart from '@/components/admin/SalesChart'

async function getDashboardData() {
  const today = new Date()
  const startOfMonth = new Date(today.getFullYear(), today.getMonth(), 1)
  const startOfLastMonth = new Date(today.getFullYear(), today.getMonth() - 1, 1)
  const endOfLastMonth = new Date(today.getFullYear(), today.getMonth(), 0)

  const [
    totalOrders,
    totalRevenue,
    totalUsers,
    totalProducts,
    monthlyOrders,
    lastMonthOrders,
    recentOrders,
    topProducts
  ] = await Promise.all([
    // 전체 주문 수
    db.order.count({
      where: { status: { not: 'cancelled' } }
    }),

    // 전체 매출
    db.order.aggregate({
      where: { status: 'completed' },
      _sum: { totalAmount: true }
    }),

    // 전체 사용자 수
    db.user.count({
      where: { status: 'active' }
    }),

    // 전체 상품 수
    db.product.count({
      where: { status: 'published' }
    }),

    // 이번 달 주문 수
    db.order.count({
      where: {
        createdAt: { gte: startOfMonth },
        status: { not: 'cancelled' }
      }
    }),

    // 지난 달 주문 수
    db.order.count({
      where: {
        createdAt: {
          gte: startOfLastMonth,
          lte: endOfLastMonth
        },
        status: { not: 'cancelled' }
      }
    }),

    // 최근 주문 10개
    db.order.findMany({
      take: 10,
      orderBy: { createdAt: 'desc' },
      include: {
        user: {
          select: { name: true, email: true }
        },
        items: {
          take: 1,
          select: { productName: true, quantity: true }
        }
      }
    }),

    // 인기 상품 Top 10
    db.product.findMany({
      take: 10,
      where: { status: 'published' },
      orderBy: { sales: 'desc' },
      select: {
        id: true,
        name: true,
        sales: true,
        price: true,
        images: {
          where: { isPrimary: true },
          take: 1,
          select: { url: true }
        }
      }
    })
  ])

  const monthlyGrowth = lastMonthOrders === 0
    ? 100
    : ((monthlyOrders - lastMonthOrders) / lastMonthOrders) * 100

  return {
    stats: {
      totalOrders,
      totalRevenue: totalRevenue._sum.totalAmount || 0,
      totalUsers,
      totalProducts,
      monthlyOrders,
      monthlyGrowth
    },
    recentOrders,
    topProducts
  }
}

export default async function AdminDashboard() {
  const session = await getServerSession(authOptions)

  if (!session || session.user.role !== 'admin') {
    redirect('/auth/signin')
  }

  const data = await getDashboardData()

  return (
    <div className="space-y-8">
      <div>
        <h1 className="text-3xl font-bold text-gray-900">대시보드</h1>
        <p className="text-gray-600">전체 현황을 한눈에 확인하세요</p>
      </div>

      {/* 통계 카드 */}
      <Suspense fallback={<div>통계 로딩 중...</div>}>
        <DashboardStats stats={data.stats} />
      </Suspense>

      <div className="grid grid-cols-1 lg:grid-cols-2 gap-8">
        {/* 매출 차트 */}
        <Suspense fallback={<div>차트 로딩 중...</div>}>
          <SalesChart />
        </Suspense>

        {/* 최근 주문 */}
        <Suspense fallback={<div>주문 목록 로딩 중...</div>}>
          <RecentOrders orders={data.recentOrders} />
        </Suspense>
      </div>
    </div>
  )
}

Week 14: 모바일 최적화

반응형 디자인 시스템

/* src/app/globals.css */

/* 모바일 우선 반응형 디자인 */
.container {
  @apply w-full mx-auto px-4;
}

@media (min-width: 640px) {
  .container {
    @apply max-w-screen-sm px-6;
  }
}

@media (min-width: 768px) {
  .container {
    @apply max-w-screen-md px-8;
  }
}

@media (min-width: 1024px) {
  .container {
    @apply max-w-screen-lg;
  }
}

@media (min-width: 1280px) {
  .container {
    @apply max-w-screen-xl;
  }
}

/* 모바일 터치 최적화 */
.touch-target {
  @apply min-h-[44px] min-w-[44px];
}

/* 모바일 네비게이션 */
.mobile-nav {
  @apply fixed bottom-0 left-0 right-0 bg-white border-t z-50;
}

.mobile-nav-item {
  @apply flex flex-col items-center py-2 px-1 text-xs;
}

/* 스와이프 제스처 지원 */
.swipeable {
  touch-action: pan-x;
}

/* 모바일 최적화된 상품 그리드 */
.product-grid {
  @apply grid gap-4;
  grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
}

@media (min-width: 640px) {
  .product-grid {
    grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
  }
}

@media (min-width: 768px) {
  .product-grid {
    grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
  }
}

🔍 Phase 4: Advanced Features (Week 15-17)

Week 15: 검색 및 필터링

Elasticsearch 통합

// src/lib/elasticsearch.ts
import { Client } from '@elastic/elasticsearch'

const client = new Client({
  node: process.env.ELASTICSEARCH_URL || 'http://localhost:9200'
})

export async function indexProduct(product: any) {
  try {
    await client.index({
      index: 'products',
      id: product.id,
      body: {
        name: product.name,
        description: product.description,
        category: product.category?.name,
        categoryId: product.categoryId,
        brand: product.brand,
        price: product.price,
        salePrice: product.salePrice,
        tags: product.tags,
        status: product.status,
        createdAt: product.createdAt,
        sales: product.sales
      }
    })
  } catch (error) {
    console.error('Product indexing error:', error)
  }
}

export async function searchProducts(query: string, filters: any = {}, page = 1, size = 20) {
  try {
    const searchBody: any = {
      query: {
        bool: {
          must: [],
          filter: []
        }
      },
      sort: [],
      from: (page - 1) * size,
      size
    }

    // 검색어가 있는 경우
    if (query) {
      searchBody.query.bool.must.push({
        multi_match: {
          query,
          fields: [
            'name^3',      // 상품명에 3배 가중치
            'description^1',
            'brand^2',     // 브랜드에 2배 가중치
            'tags^2'       // 태그에 2배 가중치
          ],
          fuzziness: 'AUTO',  // 오타 허용
          operator: 'and'
        }
      })

      // 검색어 하이라이트
      searchBody.highlight = {
        fields: {
          name: {},
          description: {}
        }
      }
    } else {
      // 검색어가 없으면 모든 문서 매치
      searchBody.query.bool.must.push({
        match_all: {}
      })
    }

    // 필터 적용
    if (filters.category) {
      searchBody.query.bool.filter.push({
        term: { categoryId: filters.category }
      })
    }

    if (filters.brand) {
      searchBody.query.bool.filter.push({
        term: { 'brand.keyword': filters.brand }
      })
    }

    if (filters.minPrice || filters.maxPrice) {
      const priceRange: any = {}
      if (filters.minPrice) priceRange.gte = filters.minPrice
      if (filters.maxPrice) priceRange.lte = filters.maxPrice

      searchBody.query.bool.filter.push({
        range: { price: priceRange }
      })
    }

    // 정렬
    switch (filters.sortBy) {
      case 'price_asc':
        searchBody.sort.push({ price: { order: 'asc' } })
        break
      case 'price_desc':
        searchBody.sort.push({ price: { order: 'desc' } })
        break
      case 'sales':
        searchBody.sort.push({ sales: { order: 'desc' } })
        break
      case 'newest':
      default:
        searchBody.sort.push({ createdAt: { order: 'desc' } })
        break
    }

    // 상품 상태 필터 (항상 적용)
    searchBody.query.bool.filter.push({
      term: { status: 'published' }
    })

    const response = await client.search({
      index: 'products',
      body: searchBody
    })

    return {
      products: response.body.hits.hits.map((hit: any) => ({
        ...hit._source,
        id: hit._id,
        score: hit._score,
        highlights: hit.highlight
      })),
      total: response.body.hits.total.value,
      page,
      totalPages: Math.ceil(response.body.hits.total.value / size)
    }

  } catch (error) {
    console.error('Search error:', error)
    throw error
  }
}

export async function getSearchSuggestions(query: string, size = 5) {
  try {
    const response = await client.search({
      index: 'products',
      body: {
        suggest: {
          name_suggest: {
            prefix: query,
            completion: {
              field: 'name.suggest',
              size
            }
          }
        }
      }
    })

    return response.body.suggest.name_suggest[0].options.map((option: any) => ({
      text: option.text,
      score: option._score
    }))

  } catch (error) {
    console.error('Suggestion error:', error)
    return []
  }
}

Week 16: AI 추천 시스템

실시간 추천 API

// src/app/api/recommendations/route.ts
import { NextRequest, NextResponse } from 'next/server'
import { getServerSession } from 'next-auth'
import { authOptions } from '@/lib/auth'
import { getPersonalizedRecommendations } from '@/lib/ml/recommendations'

export async function GET(request: NextRequest) {
  try {
    const { searchParams } = new URL(request.url)
    const type = searchParams.get('type') || 'personalized'
    const productId = searchParams.get('productId')
    const limit = parseInt(searchParams.get('limit') || '10')

    const session = await getServerSession(authOptions)
    const userId = session?.user?.id

    let recommendations = []

    switch (type) {
      case 'personalized':
        if (userId) {
          recommendations = await getPersonalizedRecommendations(userId, limit)
        } else {
          // 비로그인 사용자는 인기 상품 추천
          recommendations = await getPopularRecommendations(limit)
        }
        break

      case 'similar':
        if (productId) {
          recommendations = await getSimilarProducts(productId, limit)
        }
        break

      case 'trending':
        recommendations = await getTrendingProducts(limit)
        break

      case 'recently_viewed':
        if (userId) {
          recommendations = await getRecentlyViewedRecommendations(userId, limit)
        }
        break

      default:
        recommendations = await getPopularRecommendations(limit)
    }

    return NextResponse.json({
      recommendations,
      type,
      generatedAt: new Date().toISOString()
    })

  } catch (error) {
    console.error('Recommendations API error:', error)
    return NextResponse.json(
      { error: '추천 상품을 가져오는 중 오류가 발생했습니다.' },
      { status: 500 }
    )
  }
}

async function getPersonalizedRecommendations(userId: string, limit: number) {
  // Python ML 서비스 호출
  const response = await fetch(`${process.env.ML_SERVICE_URL}/recommendations/personalized`, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'Authorization': `Bearer ${process.env.ML_SERVICE_TOKEN}`
    },
    body: JSON.stringify({
      user_id: userId,
      limit
    })
  })

  if (!response.ok) {
    throw new Error('ML service error')
  }

  const mlRecommendations = await response.json()

  // 상품 정보 조회
  const productIds = mlRecommendations.map((rec: any) => rec.product_id)
  const products = await db.product.findMany({
    where: {
      id: { in: productIds },
      status: 'published'
    },
    include: {
      images: {
        where: { isPrimary: true },
        take: 1
      },
      category: {
        select: { name: true }
      }
    }
  })

  return mlRecommendations.map((rec: any) => {
    const product = products.find(p => p.id === rec.product_id)
    return {
      ...rec,
      product
    }
  }).filter((rec: any) => rec.product)
}

Week 17: 마케팅 도구

쿠폰 시스템

// src/lib/coupon.ts
interface Coupon {
  id: string
  code: string
  name: string
  type: 'fixed' | 'percentage'
  value: number
  minOrderAmount?: number
  maxDiscountAmount?: number
  usageLimit: number
  usedCount: number
  validFrom: Date
  validUntil: Date
  isActive: boolean
  applicableProducts?: string[]
  applicableCategories?: string[]
}

export class CouponService {
  static async validateCoupon(couponCode: string, orderAmount: number, userId: string) {
    const coupon = await db.coupon.findUnique({
      where: { code: couponCode }
    })

    if (!coupon) {
      throw new Error('존재하지 않는 쿠폰입니다.')
    }

    if (!coupon.isActive) {
      throw new Error('비활성화된 쿠폰입니다.')
    }

    if (new Date() < coupon.validFrom) {
      throw new Error('아직 사용할 수 없는 쿠폰입니다.')
    }

    if (new Date() > coupon.validUntil) {
      throw new Error('만료된 쿠폰입니다.')
    }

    if (coupon.usedCount >= coupon.usageLimit) {
      throw new Error('사용 한도가 초과된 쿠폰입니다.')
    }

    if (coupon.minOrderAmount && orderAmount < coupon.minOrderAmount) {
      throw new Error(`${coupon.minOrderAmount.toLocaleString()}원 이상 주문 시 사용 가능합니다.`)
    }

    // 사용자 쿠폰 사용 이력 확인
    const userUsage = await db.couponUsage.count({
      where: {
        couponId: coupon.id,
        userId
      }
    })

    if (userUsage > 0) {
      throw new Error('이미 사용한 쿠폰입니다.')
    }

    return coupon
  }

  static calculateDiscount(coupon: Coupon, orderAmount: number) {
    let discountAmount = 0

    if (coupon.type === 'fixed') {
      discountAmount = coupon.value
    } else if (coupon.type === 'percentage') {
      discountAmount = Math.floor(orderAmount * (coupon.value / 100))
    }

    // 최대 할인 금액 제한
    if (coupon.maxDiscountAmount && discountAmount > coupon.maxDiscountAmount) {
      discountAmount = coupon.maxDiscountAmount
    }

    // 주문 금액을 초과할 수 없음
    if (discountAmount > orderAmount) {
      discountAmount = orderAmount
    }

    return discountAmount
  }

  static async applyCoupon(couponCode: string, orderId: string, userId: string) {
    const order = await db.order.findUnique({
      where: { id: orderId },
      include: { items: true }
    })

    if (!order || order.userId !== userId) {
      throw new Error('주문을 찾을 수 없습니다.')
    }

    const coupon = await this.validateCoupon(couponCode, order.subtotal, userId)
    const discountAmount = this.calculateDiscount(coupon, order.subtotal)

    // 트랜잭션으로 쿠폰 적용
    await db.$transaction(async (tx) => {
      // 주문에 쿠폰 할인 적용
      await tx.order.update({
        where: { id: orderId },
        data: {
          discountAmount,
          totalAmount: order.totalAmount - discountAmount,
          couponId: coupon.id
        }
      })

      // 쿠폰 사용 기록 생성
      await tx.couponUsage.create({
        data: {
          couponId: coupon.id,
          userId,
          orderId,
          discountAmount
        }
      })

      // 쿠폰 사용 횟수 증가
      await tx.coupon.update({
        where: { id: coupon.id },
        data: {
          usedCount: { increment: 1 }
        }
      })
    })

    return discountAmount
  }
}

🧪 Phase 5: Testing & Launch (Week 18-20)

Week 18: 통합 테스트 및 QA

자동화 테스트 스위트

// tests/e2e/shopping-flow.test.ts
import { test, expect } from '@playwright/test'

test.describe('쇼핑 플로우 E2E 테스트', () => {
  test.beforeEach(async ({ page }) => {
    // 테스트 데이터 설정
    await page.goto('/')
  })

  test('상품 검색부터 주문 완료까지 전체 플로우', async ({ page }) => {
    // 1. 상품 검색
    await page.fill('[data-testid=search-input]', '스마트폰')
    await page.press('[data-testid=search-input]', 'Enter')

    // 검색 결과 확인
    await expect(page.locator('[data-testid=product-item]')).toHaveCount.greaterThan(0)

    // 2. 상품 상세 페이지 이동
    await page.click('[data-testid=product-item]:first-child')
    await expect(page.locator('[data-testid=product-name]')).toBeVisible()

    // 3. 장바구니 추가
    await page.click('[data-testid=add-to-cart-btn]')
    await expect(page.locator('[data-testid=cart-count]')).toHaveText('1')

    // 4. 장바구니 페이지 이동
    await page.click('[data-testid=cart-icon]')
    await expect(page.locator('[data-testid=cart-item]')).toHaveCount(1)

    // 5. 결제 페이지 이동
    await page.click('[data-testid=checkout-btn]')

    // 6. 로그인 (테스트 계정)
    await page.fill('[data-testid=email-input]', 'test@example.com')
    await page.fill('[data-testid=password-input]', 'testpassword123')
    await page.click('[data-testid=login-btn]')

    // 7. 배송 정보 입력
    await page.fill('[data-testid=shipping-name]', '홍길동')
    await page.fill('[data-testid=shipping-phone]', '010-1234-5678')
    await page.fill('[data-testid=shipping-address]', '서울시 강남구 테헤란로 123')

    // 8. 결제 수단 선택 (테스트 결제)
    await page.click('[data-testid=payment-method-test]')

    // 9. 주문 완료
    await page.click('[data-testid=place-order-btn]')
    await expect(page.locator('[data-testid=order-success]')).toBeVisible()

    // 주문번호 확인
    const orderNumber = await page.textContent('[data-testid=order-number]')
    expect(orderNumber).toMatch(/^ORD-\d+/)
  })

  test('모바일 반응형 디자인 테스트', async ({ page }) => {
    // 모바일 뷰포트 설정
    await page.setViewportSize({ width: 375, height: 667 })

    // 모바일 네비게이션 확인
    await expect(page.locator('[data-testid=mobile-nav]')).toBeVisible()

    // 햄버거 메뉴 동작 확인
    await page.click('[data-testid=menu-toggle]')
    await expect(page.locator('[data-testid=mobile-menu]')).toBeVisible()

    // 상품 그리드 모바일 최적화 확인
    await page.goto('/products')
    const productItems = page.locator('[data-testid=product-item]')
    const itemWidth = await productItems.first().boundingBox()
    expect(itemWidth?.width).toBeLessThan(200)
  })
})

성능 테스트

// tests/performance/load-test.js
import http from 'k6/http'
import { check, sleep } from 'k6'

export let options = {
  stages: [
    { duration: '5m', target: 100 },   // 5분간 100명까지 증가
    { duration: '10m', target: 100 },  // 10분간 100명 유지
    { duration: '5m', target: 200 },   // 5분간 200명까지 증가
    { duration: '10m', target: 200 },  // 10분간 200명 유지
    { duration: '5m', target: 0 },     // 5분간 0명까지 감소
  ],
  thresholds: {
    http_req_duration: ['p(95)<2000'], // 95%의 요청이 2초 이내
    http_req_failed: ['rate<0.01'],    // 에러율 1% 미만
  },
}

export default function() {
  // 홈페이지 로드 테스트
  let response = http.get('https://your-ecommerce-site.com/')
  check(response, {
    '홈페이지 상태 코드 200': (r) => r.status === 200,
    '홈페이지 응답 시간 < 2초': (r) => r.timings.duration < 2000,
  })

  // 상품 목록 페이지 테스트
  response = http.get('https://your-ecommerce-site.com/api/products?page=1&limit=20')
  check(response, {
    '상품 API 상태 코드 200': (r) => r.status === 200,
    '상품 API 응답 시간 < 1초': (r) => r.timings.duration < 1000,
    '상품 데이터 존재': (r) => JSON.parse(r.body).products.length > 0,
  })

  // 검색 API 테스트
  response = http.get('https://your-ecommerce-site.com/api/search?q=스마트폰')
  check(response, {
    '검색 API 상태 코드 200': (r) => r.status === 200,
    '검색 API 응답 시간 < 1.5초': (r) => r.timings.duration < 1500,
  })

  sleep(1)
}

Week 19: 성능 최적화

Next.js 최적화

// next.config.js
/** @type {import('next').NextConfig} */
const nextConfig = {
  // 실험적 기능
  experimental: {
    appDir: true,
    serverComponentsExternalPackages: ['prisma']
  },

  // 이미지 최적화
  images: {
    domains: ['your-cdn-domain.com', 's3.amazonaws.com'],
    deviceSizes: [640, 750, 828, 1080, 1200, 1920, 2048, 3840],
    imageSizes: [16, 32, 48, 64, 96, 128, 256, 384],
    formats: ['image/webp', 'image/avif'],
    minimumCacheTTL: 31536000, // 1년 캐싱
  },

  // 번들 분석
  webpack: (config, { buildId, dev, isServer, defaultLoaders, webpack }) => {
    // 번들 크기 최적화
    if (!dev && !isServer) {
      config.optimization.splitChunks = {
        ...config.optimization.splitChunks,
        cacheGroups: {
          ...config.optimization.splitChunks.cacheGroups,
          vendor: {
            test: /[\\/]node_modules[\\/]/,
            name: 'vendors',
            chunks: 'all',
            enforce: true,
          },
        },
      }
    }

    // 불필요한 모듈 제거
    config.resolve.alias = {
      ...config.resolve.alias,
      '@': path.resolve(__dirname, 'src'),
    }

    return config
  },

  // 압축 설정
  compress: true,

  // 파워드 바이 Next.js 헤더 제거
  poweredByHeader: false,

  // 보안 헤더
  async headers() {
    return [
      {
        source: '/(.*)',
        headers: [
          {
            key: 'X-Frame-Options',
            value: 'DENY',
          },
          {
            key: 'X-Content-Type-Options',
            value: 'nosniff',
          },
          {
            key: 'Referrer-Policy',
            value: 'strict-origin-when-cross-origin',
          },
          {
            key: 'Content-Security-Policy',
            value: "default-src 'self'; script-src 'self' 'unsafe-eval' 'unsafe-inline'; style-src 'self' 'unsafe-inline';",
          },
        ],
      },
    ]
  },

  // 리다이렉트 설정
  async redirects() {
    return [
      {
        source: '/old-product/:path*',
        destination: '/products/:path*',
        permanent: true,
      },
    ]
  },
}

module.exports = nextConfig

데이터베이스 최적화

-- 성능 최적화를 위한 인덱스 생성
-- 상품 검색 최적화
CREATE INDEX idx_products_search ON products(name, category_id, status, price);
CREATE INDEX idx_products_category_price ON products(category_id, price) WHERE status = 'published';

-- 주문 관련 최적화
CREATE INDEX idx_orders_user_status ON orders(user_id, status, created_at);
CREATE INDEX idx_order_items_product ON order_items(product_id, created_at);

-- 사용자 활동 로그 최적화
CREATE INDEX idx_user_activities_user_date ON user_activities(user_id, created_at DESC);

-- 통계 쿼리 최적화
CREATE INDEX idx_products_sales_stats ON products(category_id, sales DESC, created_at DESC);

-- 파티셔닝 설정 (대용량 데이터 처리)
-- 주문 테이블을 월별로 파티셔닝
CREATE TABLE orders_2024_01 PARTITION OF orders
    FOR VALUES FROM ('2024-01-01') TO ('2024-02-01');

CREATE TABLE orders_2024_02 PARTITION OF orders
    FOR VALUES FROM ('2024-02-01') TO ('2024-03-01');

-- 사용자 활동 로그 테이블을 일별로 파티셔닝
CREATE TABLE user_activities_2024_01_01 PARTITION OF user_activities
    FOR VALUES FROM ('2024-01-01 00:00:00') TO ('2024-01-02 00:00:00');

Week 20: 배포 및 런칭

프로덕션 배포 스크립트

#!/bin/bash
# deploy.sh

set -e  # 에러 발생 시 스크립트 중단

echo "🚀 프로덕션 배포 시작..."

# 1. 환경 변수 확인
if [ -z "$NODE_ENV" ]; then
    export NODE_ENV=production
fi

echo "📋 환경: $NODE_ENV"

# 2. 의존성 설치
echo "📦 의존성 설치 중..."
npm ci --only=production

# 3. 타입 체크
echo "🔍 타입 체크 중..."
npm run type-check

# 4. 린트 체크
echo "🧹 린트 체크 중..."
npm run lint

# 5. 테스트 실행
echo "🧪 테스트 실행 중..."
npm run test

# 6. 빌드
echo "🔨 빌드 중..."
npm run build

# 7. 데이터베이스 마이그레이션
echo "🗄️  데이터베이스 마이그레이션 중..."
npx prisma migrate deploy

# 8. 프로덕션 서버 재시작
echo "🔄 서버 재시작 중..."
pm2 restart ecommerce-app || pm2 start ecosystem.config.js

# 9. 헬스 체크
echo "🏥 헬스 체크 중..."
sleep 10
curl -f http://localhost:3000/api/health || exit 1

# 10. 캐시 웜업
echo "🔥 캐시 웜업 중..."
curl -f http://localhost:3000/ > /dev/null
curl -f http://localhost:3000/products > /dev/null

echo "✅ 배포 완료!"

# 11. Slack 알림 (선택사항)
if [ ! -z "$SLACK_WEBHOOK_URL" ]; then
    curl -X POST -H 'Content-type: application/json' \
        --data '{"text":"🚀 쇼핑몰 배포 완료! '$(date)'"}' \
        $SLACK_WEBHOOK_URL
fi

모니터링 설정

// src/lib/monitoring.ts
import { createLogger, format, transports } from 'winston'

// 구조화된 로깅 설정
export const logger = createLogger({
  level: process.env.LOG_LEVEL || 'info',
  format: format.combine(
    format.timestamp(),
    format.errors({ stack: true }),
    format.json()
  ),
  defaultMeta: { service: 'ecommerce-api' },
  transports: [
    new transports.File({ filename: 'logs/error.log', level: 'error' }),
    new transports.File({ filename: 'logs/combined.log' }),
  ],
})

if (process.env.NODE_ENV !== 'production') {
  logger.add(new transports.Console({
    format: format.combine(
      format.colorize(),
      format.simple()
    )
  }))
}

// 성능 메트릭 수집
export function trackPerformance(operation: string, startTime: number) {
  const duration = Date.now() - startTime

  logger.info('Performance metric', {
    operation,
    duration,
    timestamp: new Date().toISOString()
  })

  // Prometheus 메트릭 (선택사항)
  if (process.env.NODE_ENV === 'production') {
    // histogram.observe({ operation }, duration / 1000)
  }
}

// 에러 추적
export function trackError(error: Error, context: any = {}) {
  logger.error('Application error', {
    message: error.message,
    stack: error.stack,
    context,
    timestamp: new Date().toISOString()
  })

  // Sentry 에러 리포팅 (선택사항)
  if (process.env.NODE_ENV === 'production') {
    // Sentry.captureException(error, { contexts: context })
  }
}

🎯 프로젝트 성공 지표

런칭 후 30일 목표

const launchTargets = {
  technical: {
    uptime: '99.9%',
    pageLoadTime: '< 3초 (95th percentile)',
    apiResponseTime: '< 500ms (95th percentile)',
    errorRate: '< 0.1%',
    mobileScore: '> 90 (Lighthouse)',
  },

  business: {
    dailyActiveUsers: '1,000명',
    conversionRate: '2%',
    averageOrderValue: '75,000원',
    monthlyRevenue: '1억원',
    customerSatisfaction: '4.5/5.0',
  },

  operational: {
    supportResponseTime: '< 2시간',
    orderProcessingTime: '< 30분',
    returnRate: '< 5%',
    stockAccuracy: '99%',
  }
}

🚀 마무리

20주 완성 로드맵의 핵심은 단계별 검증과 지속적 개선입니다.

성공을 위한 5가지 원칙

  1. 사용자 중심: 기술보다 사용자 경험 우선
  2. 데이터 기반: 모든 결정은 데이터로 검증
  3. 점진적 개선: 완벽보다는 지속적 발전
  4. 팀워크: 소통과 협업이 성공의 열쇠
  5. 품질 유지: 속도보다 안정성과 보안 우선

이 가이드를 따라 여러분만의 성공적인 쇼핑몰을 만들어보세요!


📚 쇼핑몰 구축 완벽 가이드 시리즈

  1. 2026년 쇼핑몰 시장 동향과 성공 전략
  2. 쇼핑몰 구축 방법론 완전 비교
  3. 쇼핑몰 구축 비용 완전 분석
  4. 예산별 쇼핑몰 구축 전략
  5. 쇼핑몰 필수 기능 구현 가이드
  6. 쇼핑몰 구축 프로세스 단계별 가이드 ← 현재 글
  7. 쇼핑몰 성공/실패 사례 분석
  8. 2026년 이커머스 트렌드와 미래 전망
  9. 쇼핑몰 구축 실행 체크리스트
  10. 쇼핑몰 개발 도구 및 리소스 추천

💡 어떤 개발 단계에서 가장 궁금한 점이 있으신가요? 댓글로 알려주시면 더 상세한 가이드와 코드 예제를 제공해드릴게요!

Tags: #쇼핑몰개발프로세스 #애자일방법론 #프로젝트관리 #개발로드맵 #단계별가이드

궁금한 점이 있으신가요?

문의사항이 있으시면 언제든지 연락주세요.