쇼핑몰 필수 기능 구현 가이드 - 결제부터 AI 추천까지 개발자 관점의 실무 가이드

발행 읽는 시간 약 2 분

  • 쇼핑몰개발
  • 기능구현
  • 결제시스템
  • AI추천
  • 개발가이드

글 소개

개발자가 반드시 알아야 할 쇼핑몰 핵심 기능 구현법! 결제 시스템, 상품 관리, 주문 처리, AI 추천 엔진까지 - 코드 예제와 함께 완벽 정리

쇼핑몰 핵심 기능 완전 정복! 💻#

"어떤 기능부터 만들어야 할까?" 개발자 관점에서 우선순위별로 완벽 정리했습니다!

🎯 기능 우선순위 매트릭스#

MVP vs 고급 기능 분류#

JavaScript
const featurePriority = {
  P0_MVP: {
    description: "서비스 불가능하면 안 되는 기능",
    features: [
      "사용자 인증 (로그인/회원가입)",
      "상품 목록/상세 조회",
      "장바구니 관리",
      "주문/결제 처리",
      "기본 관리자 기능"
    ],
    developmentTime: "2-3개월"
  },

  P1_Essential: {
    description: "빠른 시일 내 필요한 기능",
    features: [
      "상품 검색/필터링",
      "주문 상태 추적",
      "고객 지원 (Q&A)",
      "기본 마케팅 (쿠폰/이벤트)",
      "모바일 최적화"
    ],
    developmentTime: "1-2개월 추가"
  },

  P2_Advanced: {
    description: "경쟁 우위를 위한 기능",
    features: [
      "AI 기반 상품 추천",
      "실시간 재고 관리",
      "고급 분석 대시보드",
      "소셜 로그인",
      "다국어/다통화 지원"
    ],
    developmentTime: "2-4개월 추가"
  },

  P3_Innovation: {
    description: "차별화를 위한 혁신 기능",
    features: [
      "AR/VR 체험 기능",
      "음성 주문 시스템",
      "블록체인 기반 리워드",
      "메타버스 쇼핑몰",
      "AI 챗봇 상담원"
    ],
    developmentTime: "6개월+ 추가"
  }
};

🔐 1. 사용자 인증 시스템#

회원가입/로그인 구현#

JavaScript
// JWT 기반 인증 시스템
import jwt from 'jsonwebtoken';
import bcrypt from 'bcrypt';
import { User } from '../models/User.js';

class AuthController {
  // 회원가입
  async register(req, res) {
    try {
      const { email, password, name, phone } = req.body;

      // 입력 검증
      if (!this.validateEmail(email)) {
        return res.status(400).json({
          error: '유효하지 않은 이메일 형식입니다.'
        });
      }

      if (!this.validatePassword(password)) {
        return res.status(400).json({
          error: '비밀번호는 8자 이상, 영문+숫자+특수문자 포함해야 합니다.'
        });
      }

      // 중복 체크
      const existingUser = await User.findByEmail(email);
      if (existingUser) {
        return res.status(409).json({
          error: '이미 존재하는 이메일입니다.'
        });
      }

      // 비밀번호 해싱
      const saltRounds = 12;
      const hashedPassword = await bcrypt.hash(password, saltRounds);

      // 사용자 생성
      const user = await User.create({
        email,
        password: hashedPassword,
        name,
        phone,
        status: 'active',
        emailVerified: false
      });

      // 이메일 인증 발송
      await this.sendVerificationEmail(user.email);

      res.status(201).json({
        message: '회원가입이 완료되었습니다. 이메일 인증을 진행해주세요.',
        userId: user.id
      });

    } catch (error) {
      console.error('Registration error:', error);
      res.status(500).json({ error: '서버 오류가 발생했습니다.' });
    }
  }

  // 로그인
  async login(req, res) {
    try {
      const { email, password } = req.body;

      // 사용자 조회
      const user = await User.findByEmail(email);
      if (!user) {
        return res.status(401).json({
          error: '이메일 또는 비밀번호가 올바르지 않습니다.'
        });
      }

      // 비밀번호 검증
      const isPasswordValid = await bcrypt.compare(password, user.password);
      if (!isPasswordValid) {
        return res.status(401).json({
          error: '이메일 또는 비밀번호가 올바르지 않습니다.'
        });
      }

      // 계정 상태 확인
      if (user.status !== 'active') {
        return res.status(403).json({
          error: '비활성화된 계정입니다. 고객센터에 문의해주세요.'
        });
      }

      // JWT 토큰 생성
      const accessToken = jwt.sign(
        {
          userId: user.id,
          email: user.email,
          role: user.role
        },
        process.env.JWT_SECRET,
        { expiresIn: '1h' }
      );

      const refreshToken = jwt.sign(
        { userId: user.id },
        process.env.JWT_REFRESH_SECRET,
        { expiresIn: '7d' }
      );

      // 리프레시 토큰 저장 (Redis 권장)
      await this.saveRefreshToken(user.id, refreshToken);

      // 로그인 이력 저장
      await this.logUserActivity(user.id, 'login', req.ip);

      res.json({
        message: '로그인 성공',
        user: {
          id: user.id,
          email: user.email,
          name: user.name,
          role: user.role
        },
        accessToken,
        refreshToken
      });

    } catch (error) {
      console.error('Login error:', error);
      res.status(500).json({ error: '서버 오류가 발생했습니다.' });
    }
  }

  // 이메일 형식 검증
  validateEmail(email) {
    const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
    return emailRegex.test(email);
  }

  // 비밀번호 강도 검증
  validatePassword(password) {
    // 최소 8자, 영문 대소문자, 숫자, 특수문자 포함
    const passwordRegex = /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]{8,}$/;
    return passwordRegex.test(password);
  }
}

소셜 로그인 구현 (카카오)#

JavaScript
// 카카오 소셜 로그인
class SocialAuthController {
  async kakaoCallback(req, res) {
    try {
      const { code } = req.query;

      // 카카오 액세스 토큰 획득
      const tokenResponse = await fetch('https://kauth.kakao.com/oauth/token', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/x-www-form-urlencoded',
        },
        body: new URLSearchParams({
          grant_type: 'authorization_code',
          client_id: process.env.KAKAO_CLIENT_ID,
          client_secret: process.env.KAKAO_CLIENT_SECRET,
          redirect_uri: process.env.KAKAO_REDIRECT_URI,
          code
        })
      });

      const tokenData = await tokenResponse.json();

      // 카카오 사용자 정보 조회
      const userResponse = await fetch('https://kapi.kakao.com/v2/user/me', {
        headers: {
          'Authorization': `Bearer ${tokenData.access_token}`,
        }
      });

      const kakaoUser = await userResponse.json();

      // 기존 사용자 확인 또는 신규 생성
      let user = await User.findBySocialId('kakao', kakaoUser.id);

      if (!user) {
        user = await User.create({
          socialProvider: 'kakao',
          socialId: kakaoUser.id,
          email: kakaoUser.kakao_account?.email,
          name: kakaoUser.properties?.nickname,
          profileImage: kakaoUser.properties?.profile_image,
          status: 'active',
          emailVerified: true
        });
      }

      // JWT 토큰 생성 (기존과 동일)
      const accessToken = jwt.sign(
        { userId: user.id, email: user.email },
        process.env.JWT_SECRET,
        { expiresIn: '1h' }
      );

      res.redirect(`${process.env.CLIENT_URL}/auth/success?token=${accessToken}`);

    } catch (error) {
      console.error('Kakao auth error:', error);
      res.redirect(`${process.env.CLIENT_URL}/auth/error`);
    }
  }
}

🛍️ 2. 상품 관리 시스템#

상품 데이터 모델#

JavaScript
// 상품 스키마 (MongoDB 기준)
const productSchema = new mongoose.Schema({
  // 기본 정보
  name: { type: String, required: true, index: true },
  description: { type: String, required: true },
  shortDescription: { type: String, maxlength: 200 },

  // 가격 정보
  price: {
    type: Number,
    required: true,
    min: 0,
    get: v => Math.round(v * 100) / 100  // 소수점 2자리
  },
  salePrice: { type: Number, min: 0 },
  cost: { type: Number, min: 0 },  // 원가

  // 카테고리 및 분류
  category: {
    type: mongoose.Schema.Types.ObjectId,
    ref: 'Category',
    required: true
  },
  tags: [{ type: String, index: true }],
  brand: { type: String, index: true },

  // 재고 관리
  inventory: {
    sku: { type: String, unique: true, required: true },
    stock: { type: Number, default: 0, min: 0 },
    reservedStock: { type: Number, default: 0, min: 0 },
    lowStockThreshold: { type: Number, default: 10 },
    trackInventory: { type: Boolean, default: true }
  },

  // 상품 옵션 (색상, 사이즈 등)
  options: [{
    name: String,
    values: [{
      value: String,
      priceAdjustment: { type: Number, default: 0 },
      stock: { type: Number, default: 0 }
    }]
  }],

  // 이미지
  images: [{
    url: { type: String, required: true },
    alt: String,
    isPrimary: { type: Boolean, default: false },
    order: { type: Number, default: 0 }
  }],

  // SEO
  seo: {
    metaTitle: String,
    metaDescription: String,
    slug: { type: String, unique: true, index: true }
  },

  // 상태 및 표시 옵션
  status: {
    type: String,
    enum: ['draft', 'published', 'archived'],
    default: 'draft'
  },
  featured: { type: Boolean, default: false },

  // 배송 정보
  shipping: {
    weight: { type: Number, min: 0 },
    dimensions: {
      length: Number,
      width: Number,
      height: Number
    },
    freeShippingEligible: { type: Boolean, default: false }
  },

  // 통계
  stats: {
    views: { type: Number, default: 0 },
    sales: { type: Number, default: 0 },
    rating: { type: Number, default: 0, min: 0, max: 5 },
    reviewCount: { type: Number, default: 0 }
  }

}, {
  timestamps: true,
  toJSON: { getters: true }
});

// 인덱스 설정
productSchema.index({ name: 'text', description: 'text', tags: 'text' });
productSchema.index({ category: 1, status: 1 });
productSchema.index({ 'stats.sales': -1 });
productSchema.index({ createdAt: -1 });

상품 CRUD API#

JavaScript
class ProductController {
  // 상품 목록 조회 (필터링, 검색, 페이지네이션)
  async getProducts(req, res) {
    try {
      const {
        page = 1,
        limit = 20,
        category,
        brand,
        minPrice,
        maxPrice,
        search,
        sortBy = 'createdAt',
        sortOrder = 'desc',
        inStock = true
      } = req.query;

      // 쿼리 조건 구성
      const query = { status: 'published' };

      if (category) query.category = category;
      if (brand) query.brand = brand;
      if (minPrice || maxPrice) {
        query.price = {};
        if (minPrice) query.price.$gte = parseFloat(minPrice);
        if (maxPrice) query.price.$lte = parseFloat(maxPrice);
      }

      if (search) {
        query.$text = { $search: search };
      }

      if (inStock === 'true') {
        query['inventory.stock'] = { $gt: 0 };
      }

      // 정렬 조건
      const sort = {};
      sort[sortBy] = sortOrder === 'desc' ? -1 : 1;

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

      // 상품 조회
      const products = await Product
        .find(query)
        .populate('category', 'name slug')
        .sort(sort)
        .skip(skip)
        .limit(parseInt(limit))
        .select('-inventory.cost');  // 원가 정보 제외

      const total = await Product.countDocuments(query);

      res.json({
        products,
        pagination: {
          currentPage: parseInt(page),
          totalPages: Math.ceil(total / parseInt(limit)),
          totalProducts: total,
          hasNext: skip + parseInt(limit) < total,
          hasPrev: parseInt(page) > 1
        }
      });

    } catch (error) {
      console.error('Get products error:', error);
      res.status(500).json({ error: '상품 조회 중 오류가 발생했습니다.' });
    }
  }

  // 상품 상세 조회
  async getProduct(req, res) {
    try {
      const { id } = req.params;

      const product = await Product
        .findById(id)
        .populate('category', 'name slug parentCategory')
        .select('-inventory.cost');

      if (!product) {
        return res.status(404).json({ error: '상품을 찾을 수 없습니다.' });
      }

      if (product.status !== 'published') {
        return res.status(404).json({ error: '상품을 찾을 수 없습니다.' });
      }

      // 조회수 증가 (비동기로 처리)
      Product.updateOne(
        { _id: id },
        { $inc: { 'stats.views': 1 } }
      ).exec();

      // 연관 상품 추천 (같은 카테고리, 비슷한 가격대)
      const relatedProducts = await Product
        .find({
          _id: { $ne: id },
          category: product.category,
          status: 'published',
          price: {
            $gte: product.price * 0.7,
            $lte: product.price * 1.3
          }
        })
        .limit(4)
        .select('name price images slug');

      res.json({
        product,
        relatedProducts
      });

    } catch (error) {
      console.error('Get product error:', error);
      res.status(500).json({ error: '상품 조회 중 오류가 발생했습니다.' });
    }
  }

  // 상품 등록 (관리자)
  async createProduct(req, res) {
    try {
      // 권한 확인
      if (req.user.role !== 'admin') {
        return res.status(403).json({ error: '권한이 없습니다.' });
      }

      const productData = req.body;

      // SKU 중복 확인
      const existingProduct = await Product.findOne({
        'inventory.sku': productData.inventory.sku
      });

      if (existingProduct) {
        return res.status(409).json({ error: 'SKU가 이미 존재합니다.' });
      }

      // Slug 자동 생성 (URL 친화적)
      if (!productData.seo?.slug) {
        productData.seo = {
          ...productData.seo,
          slug: this.generateSlug(productData.name)
        };
      }

      const product = new Product(productData);
      await product.save();

      res.status(201).json({
        message: '상품이 성공적으로 등록되었습니다.',
        product
      });

    } catch (error) {
      console.error('Create product error:', error);
      res.status(500).json({ error: '상품 등록 중 오류가 발생했습니다.' });
    }
  }

  // Slug 생성 헬퍼
  generateSlug(name) {
    return name
      .toLowerCase()
      .replace(/[^a-z0-9가-힣\s-]/g, '')
      .replace(/\s+/g, '-')
      .replace(/-+/g, '-')
      .trim('-');
  }
}

🛒 3. 장바구니 시스템#

장바구니 관리#

JavaScript
class CartController {
  // 장바구니 조회
  async getCart(req, res) {
    try {
      const userId = req.user?.id;
      const sessionId = req.session.id;

      // 로그인한 사용자는 DB에서, 비로그인은 세션에서
      let cart;
      if (userId) {
        cart = await Cart.findOne({ userId }).populate({
          path: 'items.productId',
          select: 'name price images inventory status'
        });
      } else {
        cart = req.session.cart || { items: [] };
      }

      if (!cart) {
        cart = { items: [] };
      }

      // 상품 정보 업데이트 및 유효성 검사
      const validatedCart = await this.validateCartItems(cart);

      res.json(validatedCart);

    } catch (error) {
      console.error('Get cart error:', error);
      res.status(500).json({ error: '장바구니 조회 중 오류가 발생했습니다.' });
    }
  }

  // 장바구니에 상품 추가
  async addToCart(req, res) {
    try {
      const { productId, quantity = 1, options = {} } = req.body;
      const userId = req.user?.id;

      // 상품 존재 및 재고 확인
      const product = await Product.findById(productId);
      if (!product || product.status !== 'published') {
        return res.status(404).json({ error: '상품을 찾을 수 없습니다.' });
      }

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

      // 옵션 가격 계산
      let optionPriceAdjustment = 0;
      if (product.options?.length > 0) {
        for (const option of product.options) {
          if (options[option.name]) {
            const selectedValue = option.values.find(
              v => v.value === options[option.name]
            );
            if (selectedValue) {
              optionPriceAdjustment += selectedValue.priceAdjustment;
            }
          }
        }
      }

      const finalPrice = product.salePrice || product.price;
      const totalPrice = (finalPrice + optionPriceAdjustment) * quantity;

      // 장바구니 아이템 생성
      const cartItem = {
        productId,
        quantity,
        options,
        price: finalPrice + optionPriceAdjustment,
        totalPrice
      };

      // 장바구니 업데이트
      if (userId) {
        await this.updateUserCart(userId, cartItem);
      } else {
        this.updateSessionCart(req, cartItem);
      }

      // 임시 재고 예약 (15분간)
      if (product.inventory.trackInventory) {
        await this.reserveInventory(productId, quantity, 15);
      }

      res.json({
        message: '장바구니에 상품이 추가되었습니다.',
        cartItem
      });

    } catch (error) {
      console.error('Add to cart error:', error);
      res.status(500).json({ error: '장바구니 추가 중 오류가 발생했습니다.' });
    }
  }

  // 사용자 장바구니 업데이트 (DB)
  async updateUserCart(userId, newItem) {
    const cart = await Cart.findOne({ userId });

    if (!cart) {
      // 새 장바구니 생성
      await Cart.create({
        userId,
        items: [newItem]
      });
    } else {
      // 기존 아이템 확인 (같은 상품 + 같은 옵션)
      const existingItemIndex = cart.items.findIndex(item =>
        item.productId.toString() === newItem.productId &&
        JSON.stringify(item.options) === JSON.stringify(newItem.options)
      );

      if (existingItemIndex >= 0) {
        // 기존 아이템 수량 증가
        cart.items[existingItemIndex].quantity += newItem.quantity;
        cart.items[existingItemIndex].totalPrice += newItem.totalPrice;
      } else {
        // 새 아이템 추가
        cart.items.push(newItem);
      }

      cart.updatedAt = new Date();
      await cart.save();
    }
  }

  // 세션 장바구니 업데이트
  updateSessionCart(req, newItem) {
    if (!req.session.cart) {
      req.session.cart = { items: [] };
    }

    const existingItemIndex = req.session.cart.items.findIndex(item =>
      item.productId === newItem.productId &&
      JSON.stringify(item.options) === JSON.stringify(newItem.options)
    );

    if (existingItemIndex >= 0) {
      req.session.cart.items[existingItemIndex].quantity += newItem.quantity;
      req.session.cart.items[existingItemIndex].totalPrice += newItem.totalPrice;
    } else {
      req.session.cart.items.push(newItem);
    }
  }

  // 재고 임시 예약
  async reserveInventory(productId, quantity, minutes) {
    const expiredAt = new Date(Date.now() + minutes * 60 * 1000);

    await InventoryReservation.create({
      productId,
      quantity,
      expiredAt
    });

    // 예약 재고 업데이트
    await Product.updateOne(
      { _id: productId },
      { $inc: { 'inventory.reservedStock': quantity } }
    );
  }
}

💳 4. 결제 시스템#

토스페이먼츠 연동#

JavaScript
class PaymentController {
  // 결제 준비 (결제 링크 생성)
  async preparePayment(req, res) {
    try {
      const { orderId, amount, paymentMethod } = req.body;
      const userId = req.user.id;

      // 주문 정보 검증
      const order = await Order.findOne({
        _id: orderId,
        userId,
        status: 'pending'
      });

      if (!order) {
        return res.status(404).json({ error: '주문을 찾을 수 없습니다.' });
      }

      // 금액 일치 확인
      if (order.totalAmount !== amount) {
        return res.status(400).json({ error: '주문 금액이 일치하지 않습니다.' });
      }

      // 토스페이먼츠 결제 생성
      const paymentData = {
        amount,
        orderId: order._id.toString(),
        orderName: `${order.items[0].productName} 외 ${order.items.length - 1}건`,
        customerEmail: req.user.email,
        customerName: req.user.name,
        returnUrl: `${process.env.CLIENT_URL}/payment/success`,
        failUrl: `${process.env.CLIENT_URL}/payment/fail`,
        flowMode: 'DIRECT',
        easyPay: paymentMethod === 'EASY_PAY' ? '토스페이' : undefined
      };

      const response = await fetch('https://api.tosspayments.com/v1/payments', {
        method: 'POST',
        headers: {
          'Authorization': `Basic ${Buffer.from(process.env.TOSS_SECRET_KEY + ':').toString('base64')}`,
          'Content-Type': 'application/json'
        },
        body: JSON.stringify(paymentData)
      });

      const payment = await response.json();

      if (!response.ok) {
        throw new Error(payment.message || '결제 생성 실패');
      }

      // 결제 정보 저장
      await Payment.create({
        orderId,
        userId,
        paymentKey: payment.paymentKey,
        amount,
        method: paymentMethod,
        status: 'PENDING',
        tossPaymentId: payment.id
      });

      res.json({
        success: true,
        paymentUrl: payment.checkout.url,
        paymentKey: payment.paymentKey
      });

    } catch (error) {
      console.error('Payment preparation error:', error);
      res.status(500).json({ error: '결제 준비 중 오류가 발생했습니다.' });
    }
  }

  // 결제 승인
  async confirmPayment(req, res) {
    try {
      const { paymentKey, orderId, amount } = req.body;

      // 결제 승인 요청
      const response = await fetch(`https://api.tosspayments.com/v1/payments/${paymentKey}`, {
        method: 'POST',
        headers: {
          'Authorization': `Basic ${Buffer.from(process.env.TOSS_SECRET_KEY + ':').toString('base64')}`,
          'Content-Type': 'application/json'
        },
        body: JSON.stringify({
          amount,
          orderId
        })
      });

      const payment = await response.json();

      if (!response.ok) {
        throw new Error(payment.message || '결제 승인 실패');
      }

      // 결제 정보 업데이트
      await Payment.updateOne(
        { paymentKey },
        {
          status: 'COMPLETED',
          approvedAt: new Date(),
          receipt: payment.receipt,
          tossPaymentData: payment
        }
      );

      // 주문 상태 업데이트
      await Order.updateOne(
        { _id: orderId },
        {
          status: 'paid',
          paidAt: new Date()
        }
      );

      // 재고 차감
      await this.deductInventory(orderId);

      // 결제 완료 이메일 발송
      await this.sendPaymentConfirmationEmail(orderId);

      res.json({
        success: true,
        message: '결제가 완료되었습니다.',
        payment
      });

    } catch (error) {
      console.error('Payment confirmation error:', error);

      // 결제 실패 처리
      await Payment.updateOne(
        { paymentKey: req.body.paymentKey },
        { status: 'FAILED', failReason: error.message }
      );

      res.status(400).json({
        error: error.message || '결제 승인 중 오류가 발생했습니다.'
      });
    }
  }

  // 재고 차감
  async deductInventory(orderId) {
    const order = await Order.findById(orderId).populate('items.productId');

    for (const item of order.items) {
      await Product.updateOne(
        { _id: item.productId._id },
        {
          $inc: {
            'inventory.stock': -item.quantity,
            'inventory.reservedStock': -item.quantity,
            'stats.sales': item.quantity
          }
        }
      );
    }
  }
}

🤖 5. AI 기반 상품 추천 시스템#

추천 엔진 구현#

Python
# Python으로 구현한 추천 알고리즘
import numpy as np
import pandas as pd
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.metrics.pairwise import cosine_similarity
from sklearn.decomposition import NMF

class RecommendationEngine:
    def __init__(self):
        self.user_item_matrix = None
        self.item_features = None
        self.model = None

    def collaborative_filtering_recommendations(self, user_id, num_recommendations=5):
        """협업 필터링 기반 추천"""
        try:
            # 사용자 구매 이력 조회
            user_purchases = self.get_user_purchase_history(user_id)

            if len(user_purchases) == 0:
                return self.popular_items_recommendation(num_recommendations)

            # 사용자-상품 매트릭스 생성
            user_item_matrix = self.create_user_item_matrix()

            # NMF (Non-negative Matrix Factorization) 모델 학습
            model = NMF(n_components=50, random_state=42)
            W = model.fit_transform(user_item_matrix)
            H = model.components_

            # 해당 사용자의 잠재 요인 벡터
            user_idx = self.get_user_index(user_id)
            if user_idx is None:
                return self.content_based_recommendations(user_purchases[0], num_recommendations)

            user_vector = W[user_idx]

            # 모든 상품에 대한 예측 점수 계산
            predicted_scores = np.dot(user_vector, H)

            # 이미 구매한 상품 제외
            purchased_items = set([p['product_id'] for p in user_purchases])

            # 추천 상품 선별
            recommendations = []
            for idx, score in enumerate(predicted_scores):
                product_id = self.get_product_id_by_index(idx)
                if product_id not in purchased_items:
                    recommendations.append({
                        'product_id': product_id,
                        'score': float(score),
                        'reason': 'similar_users'
                    })

            # 점수순 정렬
            recommendations.sort(key=lambda x: x['score'], reverse=True)
            return recommendations[:num_recommendations]

        except Exception as e:
            print(f"Collaborative filtering error: {e}")
            return self.popular_items_recommendation(num_recommendations)

    def content_based_recommendations(self, reference_product_id, num_recommendations=5):
        """콘텐츠 기반 필터링 추천"""
        try:
            # 상품 특성 데이터 조회
            products_df = self.get_products_dataframe()

            # 텍스트 특성 벡터화
            tfidf = TfidfVectorizer(
                max_features=1000,
                stop_words='english',
                ngram_range=(1, 2)
            )

            # 상품명 + 설명 + 카테고리 결합
            text_features = products_df['name'] + ' ' + \
                          products_df['description'] + ' ' + \
                          products_df['category']

            tfidf_matrix = tfidf.fit_transform(text_features)

            # 코사인 유사도 계산
            similarity_matrix = cosine_similarity(tfidf_matrix)

            # 기준 상품의 인덱스 찾기
            ref_idx = products_df[products_df['id'] == reference_product_id].index[0]

            # 유사한 상품 찾기
            similarity_scores = list(enumerate(similarity_matrix[ref_idx]))
            similarity_scores = sorted(similarity_scores, key=lambda x: x[1], reverse=True)

            # 기준 상품 제외하고 추천
            recommendations = []
            for idx, score in similarity_scores[1:num_recommendations+1]:
                product = products_df.iloc[idx]
                recommendations.append({
                    'product_id': product['id'],
                    'score': float(score),
                    'reason': 'similar_content'
                })

            return recommendations

        except Exception as e:
            print(f"Content-based filtering error: {e}")
            return self.popular_items_recommendation(num_recommendations)

    def hybrid_recommendations(self, user_id, num_recommendations=5):
        """하이브리드 추천 (협업 + 콘텐츠 기반)"""
        try:
            # 협업 필터링 추천
            cf_recs = self.collaborative_filtering_recommendations(
                user_id, num_recommendations * 2
            )

            # 사용자의 최근 구매 상품 기반 콘텐츠 추천
            recent_purchases = self.get_recent_purchases(user_id, limit=3)
            cb_recs = []

            for purchase in recent_purchases:
                cb_recs.extend(
                    self.content_based_recommendations(
                        purchase['product_id'],
                        num_recommendations
                    )
                )

            # 점수 정규화 및 가중 평균
            cf_weight = 0.7
            cb_weight = 0.3

            # 추천 결과 병합
            combined_recs = {}

            for rec in cf_recs:
                product_id = rec['product_id']
                combined_recs[product_id] = {
                    'product_id': product_id,
                    'score': rec['score'] * cf_weight,
                    'reasons': ['similar_users']
                }

            for rec in cb_recs:
                product_id = rec['product_id']
                if product_id in combined_recs:
                    combined_recs[product_id]['score'] += rec['score'] * cb_weight
                    combined_recs[product_id]['reasons'].append('similar_content')
                else:
                    combined_recs[product_id] = {
                        'product_id': product_id,
                        'score': rec['score'] * cb_weight,
                        'reasons': ['similar_content']
                    }

            # 최종 추천 결과
            final_recs = list(combined_recs.values())
            final_recs.sort(key=lambda x: x['score'], reverse=True)

            return final_recs[:num_recommendations]

        except Exception as e:
            print(f"Hybrid recommendation error: {e}")
            return self.popular_items_recommendation(num_recommendations)

    def popular_items_recommendation(self, num_recommendations=5):
        """인기 상품 추천 (기본값)"""
        try:
            popular_products = self.get_popular_products(num_recommendations)
            return [{
                'product_id': product['id'],
                'score': product['popularity_score'],
                'reason': 'popular_item'
            } for product in popular_products]
        except Exception as e:
            print(f"Popular items recommendation error: {e}")
            return []

Node.js에서 추천 시스템 연동#

JavaScript
class RecommendationService {
  // 사용자 맞춤 추천
  async getUserRecommendations(userId, limit = 10) {
    try {
      // Python 추천 엔진 호출
      const response = await fetch(`${process.env.ML_SERVICE_URL}/recommendations/user`, {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
          'Authorization': `Bearer ${process.env.ML_SERVICE_TOKEN}`
        },
        body: JSON.stringify({
          user_id: userId,
          num_recommendations: limit
        })
      });

      const recommendations = await response.json();

      // 상품 정보 조회
      const productIds = recommendations.map(rec => rec.product_id);
      const products = await Product
        .find({ _id: { $in: productIds }, status: 'published' })
        .select('name price images slug stats.rating stats.reviewCount');

      // 추천 결과와 상품 정보 결합
      const enrichedRecommendations = recommendations
        .map(rec => {
          const product = products.find(p => p._id.toString() === rec.product_id);
          return product ? {
            ...rec,
            product
          } : null;
        })
        .filter(Boolean);

      return enrichedRecommendations;

    } catch (error) {
      console.error('Get user recommendations error:', error);

      // 실패 시 인기 상품으로 폴백
      return this.getPopularProducts(limit);
    }
  }

  // 연관 상품 추천
  async getRelatedProducts(productId, limit = 4) {
    try {
      const response = await fetch(`${process.env.ML_SERVICE_URL}/recommendations/similar`, {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
          'Authorization': `Bearer ${process.env.ML_SERVICE_TOKEN}`
        },
        body: JSON.stringify({
          product_id: productId,
          num_recommendations: limit
        })
      });

      const recommendations = await response.json();

      const productIds = recommendations.map(rec => rec.product_id);
      const products = await Product
        .find({ _id: { $in: productIds }, status: 'published' })
        .select('name price images slug');

      return recommendations
        .map(rec => {
          const product = products.find(p => p._id.toString() === rec.product_id);
          return product ? { ...rec, product } : null;
        })
        .filter(Boolean);

    } catch (error) {
      console.error('Get related products error:', error);

      // 실패 시 같은 카테고리 상품으로 폴백
      const originalProduct = await Product.findById(productId);
      if (originalProduct) {
        return this.getCategoryProducts(originalProduct.category, limit);
      }

      return [];
    }
  }

  // 실시간 추천 업데이트
  async updateUserPreferences(userId, productId, action) {
    try {
      // 사용자 행동 로그 저장
      await UserActivity.create({
        userId,
        productId,
        action, // 'view', 'add_to_cart', 'purchase', 'like'
        timestamp: new Date()
      });

      // ML 서비스에 실시간 업데이트 전송
      await fetch(`${process.env.ML_SERVICE_URL}/user-activity`, {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
          'Authorization': `Bearer ${process.env.ML_SERVICE_TOKEN}`
        },
        body: JSON.stringify({
          user_id: userId,
          product_id: productId,
          action,
          timestamp: new Date().toISOString()
        })
      });

    } catch (error) {
      console.error('Update user preferences error:', error);
      // 실패해도 메인 프로세스에 영향 주지 않음
    }
  }
}

🚀 마무리#

쇼핑몰의 핵심 기능들을 성공적으로 구현하려면:

개발 우선순위 원칙#

  1. MVP 먼저: 기본 기능으로 빠르게 론칭
  2. 사용자 피드백: 실제 사용 데이터 기반 개선
  3. 점진적 고도화: 단계별 기능 확장
  4. 성능 최적화: 사용자 증가에 맞춘 스케일링

핵심 개발 팁#

  • 보안 우선: 모든 입력 검증, SQL 인젝션 방지
  • 에러 처리: 사용자 친화적 에러 메시지
  • 로깅: 디버깅을 위한 상세 로그 기록
  • 테스트: 단위/통합 테스트 필수

다음 편에서는 20주 개발 프로세스를 자세히 다룰 예정입니다!


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

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

💡 어떤 기능 구현이 가장 궁금하신가요? 댓글로 알려주시면 더 상세한 코드 예제와 구현 팁을 제공해드릴게요!

Tags: #쇼핑몰개발 #기능구현 #결제시스템 #AI추천 #개발가이드