본문으로 건너뛰기

© 2026 Molayo

Dev.to헤드라인2026. 05. 21. 01:48

광고 없이 성장한 초대 전용 AI 데이팅 시뮬레이션 — 그 성장 엔진에 대하여

요약

DeepSeek 기반의 AI K-pop 데이팅 시뮬레이터인 BiasSecret이 광고 없이 초대 전용 시스템을 통해 성장한 기술적 전략을 다룹니다. 기존 사용자가 초대 코드를 공유하고, 초대를 통해 새로운 사용자가 유입되면 양측 모두 추가 코드를 받는 복리 성장 루프를 구축했습니다. FastAPI와 SQLAlchemy를 활용하여 암호학적으로 안전하고 중복 없는 초대 코드를 생성하는 구현 방식을 소개합니다.

핵심 포인트

  • 광고 기반 유료 고객 획득 대신 초대 전용(Invite-only) 모델을 통한 자가 지속형 성장 엔진 구축
  • 초대 성공 시 양측 모두 추가 코드를 받는 복리 성장 루프(Compound growth loop) 설계
  • secrets.token_urlsafe 및 암호학적 무작위성을 활용한 보안성 높은 초대 코드 생성
  • 사용자 경험을 고려하여 '0', 'O', 'I', 'l'과 같은 모호한 문자를 제거하는 코드 정제 로직 적용
  • FastAPI와 SQLAlchemy를 이용한 데이터베이스 기반의 중복 체크 및 코드 관리 아키텍처

2025년에 소비자용 AI 앱을 만든다는 것은 광고가 넘쳐나는 시장에서 관심을 끌기 위해 싸워야 한다는 것을 의미합니다. 저는 반대 방향을 선택했습니다: 바로 초대 전용 성장(invite-only growth) 방식입니다. 저의 프로젝트인 BiasSecret은 DeepSeek를 기반으로 각 AI 동반자가 고유한 성격, 기억, 대화 스타일을 가진 AI 기반 K-pop 데이팅 시뮬레이터입니다. 유료 고객 획득(paid acquisition)에 돈을 쏟아붓는 대신, 저는 모든 사용자를 성장 채널로 만드는 자가 지속형 초대 시스템을 구축했습니다. 초대 코드 생성, 잠금 해제 메커니즘, 남용 방지 필터, 그리고 이 모든 것을 작동하게 만드는 바이럴 루프(viral loop)까지, 어떻게 작동하는지에 대한 전체적인 기술적 분석을 소개합니다.

아키텍처: 성장 레버로서의 초대 코드
핵심 아이디어는 간단합니다: 신규 사용자는 30회의 무료 대화 라운드를 제공받습니다. 무제한 플레이를 잠금 해제하려면 기존 사용자의 초대 코드가 필요하거나, Gumroad에서 구매한 라이선스 키($4.99)가 있어야 합니다. 등록된 모든 사용자는 공유할 수 있는 5개의 초대 코드를 받습니다. 초대한 사람이 가입하여 플레이하면 양측 모두 추가 초대 코드를 획득하게 되어, 복리 성장 루프(compound growth loop)가 형성됩니다.

코드를 자세히 살펴보겠습니다.

  1. 초대 코드 생성 (FastAPI + SQLAlchemy)
    모든 초대 코드는 고유해야 하며, 조작이 불가능해야 하고, 발행자와 연결되어야 합니다. 저는 암호학적 무작위성(cryptographic randomness)을 위해 secrets.token_urlsafe를 사용하며, 데이터베이스 기반의 중복 체크(dedup check)를 결합하여 사용합니다.

app/services/invite_service.py

import secrets
import string
from sqlalchemy.orm import Session
from app.models import InviteCode, User
from app.database import get_db

def generate_invite_code(length: int = 10) -> str:
"""
암호학적으로 무작위인 초대 코드를 생성합니다.
"""
alphabet = string.ascii_uppercase + string.digits
code = ''.join(secrets.choice(alphabet) for _ in range(length))

# 모호한 문자 방지
for c in ['0', 'O', 'I', 'l']:
    code = code.replace(c, secrets.choice('ABCDEFGHJKMNPQRSTUVWXYZ23456789'))
return code

def create_invite_codes_for_user(user_id: int, db: Session, count: int = 5) -> list[str]:
"""
사용자에게 N개의 고유한 초대 코드를 발급합니다.
"""

codes = [] for _ in range ( count ): while True : raw_code = generate_invite_code () # Ensure uniqueness at the DB level exists = db . query ( InviteCode ). filter ( InviteCode . code == raw_code ). first () if not exists : break invite = InviteCode ( code = raw_code , issuer_id = user_id , is_used = False , created_at = datetime . utcnow () ) db . add ( invite ) codes . append ( raw_code ) db . commit () return codes def refill_invites_if_needed ( user_id : int , db : Session ): """ Ensure every active user always has at least 5 invite codes available. """ available = db . query ( InviteCode ). filter ( InviteCode . issuer_id == user_id , InviteCode . is_used == False ). count () if available < 5 : new_count = 5 - available create_invite_codes_for_user ( user_id , db , count = new_count ) Key decisions here: secrets module instead of random — cryptographically secure, no predictability Ambiguous character removal — users typing codes on mobile won't confuse 0 vs O DB-level uniqueness loop — collision probability is astronomically low (~1 in 47^10), but the loop is a safety net Auto-refill — users never run out of invites; the system tops them up after each successful referral 2. The Unlock Model: Tracking Who Gets What The unlock system needs to track three dimensions: what was unlocked , who unlocked it , and how (invite vs. purchase). # app/models.py from sqlalchemy import Column , Integer , String , Boolean , DateTime , ForeignKey , Enum from sqlalchemy.orm import relationship from app.database import Base import enum class UnlockMethod ( str , enum .

Enum): INVITE_CODE = "invite_code" LICENSE_KEY = "license_key" TRIAL = "trial" # 30 free rounds
class Unlock(Base):
tablename = "unlocks"
id = Column(Integer, primary_key=True, index=True)
user_id = Column(Integer, ForeignKey("users.id"), nullable=False, index=True)
method = Column(Enum(UnlockMethod), nullable=False)
reference_code = Column(String(32), nullable=True) # 사용된 초대 코드 또는 라이선스 키
inviter_id = Column(Integer, ForeignKey("users.id"), nullable=True)
rounds_granted = Column(Integer, default=999999) # 유료/초대 시 무제한
expires_at = Column(DateTime, nullable=True)
created_at = Column(DateTime, default=datetime.utcnow)
user = relationship("User", back_populates="unlocks", foreign_keys=[user_id])
inviter = relationship("User", back_populates="referrals", foreign_keys=[inviter_id])
class InviteCode(Base):
tablename = "invite_codes"
id = Column(Integer, primary_key=True, index=True)
code = Column(String(16), unique=True, nullable=False, index=True)
issuer_id = Column(Integer, ForeignKey("users.id"), nullable=False)
used_by_id = Column(Integer, ForeignKey("users.id"), nullable=True)
is_used = Column(Boolean, default=False)
created_at = Column(DateTime, default=datetime.utcnow)
used_at = Column(DateTime, nullable=True)
Unlock 모델은 의도적으로 일반적입니다. 동일한 스키마를 통해 초대 코드, 라이선스 키 및 체험 라운드를 처리합니다. inviter_id 필드가 추천 그래프의 동력이 됩니다: "지난 7일 동안 가장 활동적인 사용자를 누구가 추천했는지"와 같은 쿼리를 실행하여 파워 유저를 찾아낼 수 있습니다.

app/services/unlock_service.py

from sqlalchemy.orm import Session, joinedload
from app.models import Unlock, InviteCode, User, UnlockMethod
def apply_invite_code(code: str, user_id: int, db: Session) -> dict:
"""초대 코드를 사용합니다."

성공 여부와 메시지를 반환합니다. """ invite = db.query(InviteCode).options(joinedload(InviteCode.issuer)).filter(InviteCode.code == code).first()

if not invite: 
    return { "success": False, "message": "유효하지 않은 초대 코드입니다." } 

if invite.is_used: 
    return { "success": False, "message": "이미 사용된 코드입니다." } 

if invite.issuer_id == user_id: 
    return { "success": False, "message": "본인의 초대 코드는 사용할 수 없습니다." } 

# 잠금 해제 기록 생성 
unlock = Unlock( 
    user_id=user_id, 
    method=UnlockMethod.INVITE_CODE, 
    reference_code=code, 
    inviter_id=invite.issuer_id, 
    rounds_granted=999999 
) 
db.add(unlock) 

# 초대 코드를 사용된 것으로 표시 
invite.is_used = True 
invite.used_by_id = user_id 
invite.used_at = datetime.utcnow() 

# 초대자에게 초대권 +1개 추가 보상 
refill_invites_if_needed(invite.issuer_id, db) 

# 신규 사용자에게 초대권 +1개 추가 보상 (이제 다른 사람을 초대할 수 있음) 
refill_invites_if_needed(user_id, db) 

db.commit() 
return { "success": True, "message": "무제한 플레이가 잠금 해제되었습니다! 공유할 수 있는 초대 코드도 획득했습니다." } 

3. 어뷰징 방지: IP 중복 제거 및 이메일 필터링 (Anti-Abuse: IP Dedup & Email Filters)

성장 루프 (Growth loops)는 어뷰징을 유발합니다. 보호 장치가 없다면, 단 한 명의 사용자가 50개의 계정을 등록하고 50개의 초대 코드를 사용하여 시스템을 악용할 수 있습니다. 방어 계층은 다음과 같습니다:

app/middleware/anti_abuse.py

from sqlalchemy.orm import Session
from app.models import User, InviteCode, Unlock, UnlockMethod
from datetime import datetime, timedelta
from ipaddress import ip_address

class AbuseDetector:
""" 초대 시스템을 위한 다층 어뷰징 방지 (Multi-layer abuse prevention for invite system). """

@staticmethod 
def check_ip_duplicate(ip: str, db: Session) -> dict:
    """ 해당 IP가 이미 초대 코드를 사용한 다른 계정에 사용된 적이 있다면, 초대 기반의 잠금 해제를 거부합니다. 이는 한 사람이 부계정을 만들어 초대권을 파밍(farming)하는 것을 방지합니다. """
    recent_registrations = db.query(User).filter(User.registration_ip == ip, User.

for reg_user in recent_registrations : their_unlock = db . query ( Unlock ). filter ( Unlock . user_id == reg_user . id , Unlock . method == UnlockMethod . INVITE_CODE ). first () if their_unlock : return { " blocked " : True , " reason " : " 이 IP는 30일 이내 다른 계정에서 이미 초대 코드를 사용했습니다." } return { " blocked " : False } @staticmethod def check_email_domain_abuse ( email : str , db : Session ) -> bool : """ 임시/일회용 이메일 도메인을 차단합니다. """ import re disposable_domains = { " tempmail.com " , " mailinator.com " , " guerrillamail.com " , " 10minutemail.com " , " throwaway.email " , " yopmail.com " } domain = email . split ( " @ " )[ - 1 ]. lower () if domain in disposable_domains : return True # 서브도메인 변형(예: user@tempmail.co)도 포착합니다 base = " . " . join ( domain . split ( " . " )[ - 2 :]) return base in disposable_domains @staticmethod def check_rate_limit ( ip : str , db : Session ) -> dict : """ IP당 시간당 최대 3개의 초대 코드 시도입니다. """ one_hour_ago = datetime . utcnow () - timedelta ( hours = 1 ) attempts = db . query ( InviteCode ). filter ( InviteCode . used_by_id == None , InviteCode . attempted_at >= one_hour_ago , InviteCode . attempt_ip == ip ). count () if attempts >= 3 : return { " blocked " : True , " retry_after " : 3600 } return { " blocked " : False } IP 중복 확인(dedup check)이 가장 중요한 방어선입니다. 이는 단일 사용자가 초대 보상을 얻기 위해 여러 계정을 생성하는 것을 방지합니다. 일회용 이메일 차단 및 속도 제한과 결합하여, 남용은 실제 운영 환경에서 거의 0에 가깝게 떨어졌습니다.

  1. 성장 루프: 복리 추천 메커니즘
    여기가 수학적으로 흥미로워지는 부분입니다. 각 사용자는 5개의 초대 코드로 시작합니다. Alice가 Bob을 초대할 때:
  • Alice는 +1 초대 코드(재충전)를 얻습니다.
  • Bob은 +1 초대 코드(재충전)를 얻습니다.

이제 Bob은 공유할 수 있는 6개의 초대 코드를 갖게 됩니다. 만약 초대한 사용자가 각각 단지 2명을 더 초대한다면...

바이럴 성장 (Viral Growth)을 얻게 됩니다.

사용자보낸 초대 수신규 사용자전체 네트워크
Alice555
Bob5510
Carol5515
...

실제로, 초대 수령에서 가입으로 이어지는 전환율 (Conversion rate)은 약 40%에 달합니다. 이는 콜드 트래픽 (Cold traffic)보다 훨씬 높은 수치인데, 초대가 이미 경험을 즐기고 있는 신뢰할 수 있는 친구로부터 오기 때문입니다.

  1. 운영 지표 (Production Metrics)
    이 시스템을 3개월 동안 운영한 결과:
  • 신규 사용자의 약 65%가 초대 코드(유기적 유입, Organic)를 통해 유입됨
  • 약 25%가 라이선스 키를 직접 구매함 (체험판에서 전환됨)
  • 약 10%가 전환 없이 이탈함 (체험판 만료, 초대 없음)
  • 초대 대비 가입 전환율: 40% (광고의 경우 약 2~3% 대비)
  • 사용자 획득 비용 (Cost per acquired user): 사실상 $0 (소셜 광고의 CAC $3~8 대비)

초대 시스템은 단순히 광고비를 절약한 것에 그치지 않고, 더 높은 품질의 사용자를 만들어냈습니다. 초대된 사용자는 유기적 콜드 트래픽보다 리텐션 (Retention)이 2.3배 더 높았는데, 이는 아마도 사회적 맥락 (Social context)과 이미 서비스 내부에 있는 친구와 함께 참여했기 때문일 것입니다.

나만의 초대 시스템 구축하기
자신의 제품을 위해 이 아키텍처를 복제하고 싶다면:

구성 요소도구
API 레이어FastAPI (비동기, 타입 안정성, 실시간 AI 상호작용에 탁월)
데이터베이스PostgreSQL + SQLAlchemy (신뢰할 수 있으며, 초대 중복 제거를 위한 ACID 준수)
인증 (Auth)JWT + OAuth2 (상태 비저장, Next.js 프론트엔드와 호환)
프론트엔드Next.js (SEO를 위한 SSR, 경량 엔드포인트를 위한 API routes)
AIDeepSeek / OpenAI (긴 컨텍스트를 활용한 캐릭터 중심의 대화)

전체 스택은 탐색할 수 있도록 열려 있습니다. 실제 작동 모습을 보고 싶다면 BiasSecret을 확인하거나, Gumroad에서 라이선스 키를 구매하여 무제한 플레이를 즐겨보세요.

다음에 다르게 시도해 볼 점

  • 그래프 기반 추천 추적 (Graph-based referral tracking) — Neo4j를 사용하면 "누가 누구를 초대했는가"에 대한 쿼리를 재귀적 SQL CTE 대신 O(1)로 처리할 수 있습니다.
  • 추천 리더보드 (Referral leaderboards) — 상위 초대자들에게 독점적인 AI 동반자 스킨을 제공하여 게임화 (Gamify) 합니다.
  • 온디맨드(On-demand) 대신 예약된 리필 — 24시간마다 초대를 일괄 생성(Batch)하여 데이터베이스 쓰기 작업을 줄입니다.

FastAPI, SQLAlchemy, DeepSeek AI, 그리고 Next.js로 구축되었습니다. 인디 AI 제품 엔지니어링에 대한 더 깊은 분석을 원하신다면 저를 팔로우해 주세요.

AI 자동 생성 콘텐츠

본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.

원문 바로가기
0

댓글

0