블로그로 돌아가기
Product

초대 코드와 바이럴 퀴즈 — 데이팅 앱의 오가닉 그로스 엔진 설계

초대 코드의 양면 보상 구조, 비로그인 바이럴 퀴즈의 전환 퍼널, 그리고 두 시스템이 만들어내는 그로스 루프의 기술적 설계를 공유합니다.

Team Somebling ·
#그로스 #바이럴 #초대 시스템 #퍼널 설계 #전환율

“좋은 앱인데 어떻게 알리지?”

데이팅 앱을 만들면서 가장 먼저 부딪히는 벽은 기술이 아니라 그로스(Growth)다. OCEAN 모델 기반 성격 분석, 코사인 유사도 매칭 알고리즘, 실시간 채팅까지 공들여 구축했지만, 정작 사용자가 없으면 아무 의미가 없다.

데이팅 앱은 다른 어떤 서비스보다 네트워크 효과가 중요하다. 사용자가 많아야 다양한 매칭 풀이 생기고, 매칭 품질이 올라가고, 더 많은 사람이 가입하는 선순환이 만들어진다. 역으로 초기에 사용자가 적으면 매칭 품질이 떨어져 이탈이 발생하고, 이탈이 쌓이면 더 매칭 풀이 줄어드는 악순환에 빠진다.

이것이 데이팅 앱의 Cold Start Problem이다. 닭이 먼저냐 달걀이 먼저냐의 문제처럼, 좋은 매칭을 위해서는 사용자가 필요하고, 사용자를 모으려면 좋은 매칭이 필요하다.

그렇다면 유료 광고로 초기 사용자를 확보하면 되지 않을까? 현실은 녹록지 않다. 데이팅 시장의 CAC(Customer Acquisition Cost, 고객 획득 비용)는 전자상거래나 SaaS 대비 현저히 높다. 사용자들은 개인적인 데이팅 앱 사용 사실을 SNS에서 자발적으로 드러내지 않으며, 광고를 통해 유입된 사용자의 리텐션(Retention)도 오가닉 유입 대비 낮은 경향이 있다. 광고비를 태워 모은 사용자가 이탈하면 다시 광고비를 써야 하는 구조가 반복된다.

썸블링이 선택한 전략은 오가닉 그로스 엔진을 제품 내부에 내장하는 것이었다. 광고 없이도 기존 사용자가 새로운 사용자를 데려오고, 바이럴 콘텐츠가 자연스럽게 퍼져나가는 구조. 그 두 축이 바로 초대 코드 시스템바이럴 퀴즈다.


그로스 엔진 1: 초대 코드 시스템

양면 보상(Double-Sided Reward)의 설계

초대 시스템에서 가장 중요한 의사결정은 “누구에게, 얼마의 보상을 줄 것인가”다.

썸블링은 다음과 같이 설계했다.

대상보상이유
초대자 (기존 회원)카드 3장공유 행동에 대한 감사
피초대자 (신규 회원)카드 5장첫 경험을 풍성하게

비대칭 보상이다. 신규 사용자에게 더 많은 보상을 주는 이유가 있다.

데이팅 앱에서 신규 사용자의 첫 경험은 결정적이다. 썸블링에서 “카드”는 매칭을 요청하거나 추가 매칭을 받는 데 사용하는 핵심 재화다. 카드가 충분해야 더 많은 매칭을 경험하고, 소울 테스트를 마친 뒤 첫 매칭의 설레임을 충분히 느낄 수 있다. 처음부터 카드가 없어 매칭을 못 하면 앱을 지워버린다.

피초대자에게 5장을 주는 것은 온보딩 마찰을 줄이는 투자다. 카드 걱정 없이 앱을 탐색하고, 소울 테스트를 완료하고, 첫 매칭을 경험하게 하는 것. 그 경험이 좋으면 자연스럽게 자신의 지인을 초대하는 선순환이 만들어진다.

초대자에게 3장을 주는 것도 충분히 동기부여가 된다. 카드가 소진될 때쯤 “아, 친구 초대하면 3장 받을 수 있지”라는 생각이 자연스럽게 든다. 지인을 대상으로 한 초대는 전환율도 높다. 아는 사람의 추천이니까.

초대 코드 흐름

flowchart LR
    A[기존 회원] -->|초대 코드 공유| B[신규 사용자]
    B -->|회원가입 시 코드 입력| C[코드 검증]
    C -->|유효| D[피초대자 카드 5장 지급]
    C -->|유효| E[초대자 카드 3장 지급]
    D --> F[소울 테스트 완료]
    F --> G[매칭 시작]
    G -->|좋은 경험| H[자발적 초대]
    H --> A

전략적 도구: 여성 전용 초대 코드

데이팅 앱에서 성비(性比)는 서비스 생존의 문제다. 대부분의 이성 매칭 데이팅 앱은 남성 사용자가 먼저 유입되는 경향이 있다. 남성 사용자가 압도적으로 많아지면 여성 사용자의 매칭 경험이 과부하 상태가 되고, 결국 여성 사용자가 이탈하면 남성 사용자도 이탈한다.

썸블링은 이를 해결하기 위해 초대 코드 유형을 두 가지로 분리했다.

public enum InviteCodeType {
    /**
     * 일반 초대 코드 — 성별 제한 없음.
     */
    GENERAL,

    /**
     * 여성 전용 초대 코드 — 여성 신규 사용자만 사용 가능.
     * 성비 밸런스 유지를 위해 운영자가 선별 발급.
     */
    WOMEN_ONLY
}

WOMEN_ONLY 코드는 운영자가 특정 여성 인플루언서나 커뮤니티에 선별적으로 배포한다. 이 코드를 통해 가입한 여성 사용자에게는 피초대자 보상에 더해 추가 혜택을 제공하는 방식도 가능하다. 여성 사용자가 충분히 확보되면 남성 사용자의 매칭 경험도 개선되고, 자연스럽게 남성 사용자의 리텐션도 올라간다.

남용 방지 설계

초대 보상을 설계할 때 반드시 고려해야 할 것이 어뷰징(Abusing) 방지다.

public class InviteCode extends BaseEntity {

    /**
     * 초대 코드 최대 사용 횟수.
     * 기본값: 10회 (운영자 설정 가능)
     */
    @Column(nullable = false)
    private Integer maxUses;

    /**
     * 현재 사용 횟수.
     */
    @Column(nullable = false)
    private Integer currentUses = 0;

    /**
     * 코드 만료 일시.
     * null이면 무기한 유효.
     */
    @Column
    private LocalDateTime expiresAt;

    /**
     * 코드 활성 여부.
     * 어뷰징 감지 시 운영자가 비활성화.
     */
    @Column(nullable = false)
    private Boolean isActive = true;
}
flowchart TD
    A["초대 코드 사용 요청"] --> B{코드 유효성 검증}
    B -->|"코드 없음"| X1["❌ INVITE_CODE_NOT_FOUND"]
    B -->|"유효"| C{본인 초대?}
    C -->|"Yes"| X2["❌ SELF_INVITE_NOT_ALLOWED"]
    C -->|"No"| D{중복 사용?}
    D -->|"Yes"| X3["❌ ALREADY_REDEEMED"]
    D -->|"No"| E{여성 전용 코드?}
    E -->|"Yes + 남성"| X4["❌ 사용 불가"]
    E -->|"No 또는 여성"| F["✅ 보상 지급<br/>피초대자 5장 + 초대자 3장"]
    F --> G{maxUses 도달?}
    G -->|"Yes"| H["코드 비활성화"]
    G -->|"No"| I["사용 횟수 +1"]

한 사람이 여러 계정을 만들어 보상을 반복 수령하는 것을 방지하기 위해:


그로스 엔진 2: 바이럴 퀴즈

비로그인 접근의 힘

초대 코드는 기존 사용자에서 시작하는 그로스다. 그렇다면 완전히 새로운 채널에서 유입을 만들 수 있는 방법은 없을까?

바이럴 퀴즈는 이 질문에서 출발했다. 썸블링의 핵심 가치 제안은 “성격과 가치관으로 매칭”이다. 그런데 소울 테스트(50문항 전체 검사)를 회원가입 없이 체험하게 하면 어떨까? 더 짧고 가벼운 버전으로?

썸블링의 바이럴 퀴즈는 5~10문항으로 성격 유형을 진단하는 미니 퀴즈다. 로그인 없이 접근 가능하며, 결과 페이지에는 고유 URL과 공유 이미지가 생성된다. SNS에 공유되는 순간 새로운 유입이 만들어진다.

바이럴 퀴즈 전환 퍼널

flowchart TD
    A[SNS에서 퀴즈 링크 클릭] --> B[퀴즈 시작 - 비로그인]
    B --> C[5~10문항 응답]
    C --> D[성격 유형 결과 표시]
    D --> E{사용자 행동}
    E -->|결과 공유| F[SNS 공유 - 새로운 유입]
    E -->|가입 CTA 클릭| G[회원가입]
    G --> H[소울 테스트 50문항]
    H --> I[매칭 시작]
    F --> A

핵심은 마찰을 최소화하는 것이다. 퀴즈를 시작하기 위해 이메일을 입력하거나 앱을 설치하거나 로그인할 필요가 없다. 링크를 클릭하면 바로 퀴즈 시작이다.

결과 페이지에서의 자연스러운 CTA도 중요하다. “당신의 진짜 성격을 더 깊이 알고 싶다면?” 같은 문구로 풀 소울 테스트를 유도하되, 회원가입은 그다음 단계로 최대한 뒤로 미룬다. 퀴즈를 즐긴 사람이 “이 앱이 재미있겠다”는 생각이 들었을 때 가입 버튼을 보여주는 것이다.

공유 가능한 결과 설계

바이럴 퀴즈의 성패는 결과 페이지가 얼마나 “공유하고 싶은가”에 달려있다.

썸블링의 퀴즈 결과는 다음 요소로 구성된다.

  1. 성격 유형 이름: “따뜻한 탐험가”, “신중한 분석가” 같이 매력적인 레이블
  2. 시각적 결과 카드: OG 이미지로 공유 시 미리보기가 예쁘게 나오도록 설계
  3. 고유 URL: /quiz/result/{sessionId} 형태로 공유 링크 생성
  4. 가입 유도 CTA: “나와 맞는 사람을 찾아보세요 → 썸블링 시작하기”
// 퀴즈 결과 공유 URL 생성
const shareUrl = `https://aro.app/quiz/result/${sessionId}`;

// OG 메타 태그 (Next.js generateMetadata)
export async function generateMetadata({ params }) {
  const result = await getQuizResult(params.sessionId);
  return {
    title: `나의 성격 유형: ${result.personalityType}`,
    description: `썸블링 성격 퀴즈 결과 — ${result.description}`,
    openGraph: {
      images: [{ url: `/api/og/quiz/${params.sessionId}` }],
    },
  };
}

전환율 추적: viral_quiz_sessions

퀴즈에서 실제 가입으로 이어지는 전환율을 측정하기 위해 viral_quiz_sessions 테이블을 설계했다.

CREATE TABLE viral_quiz_sessions (
    id              BIGSERIAL PRIMARY KEY,
    session_id      VARCHAR UNIQUE NOT NULL,  -- 퀴즈 세션 고유 ID
    answers         JSONB,                    -- 퀴즈 답변 데이터
    personality_type VARCHAR,                 -- 결과 성격 유형
    completed_at    TIMESTAMP,               -- 퀴즈 완료 시각
    converted_member_id BIGINT,              -- 가입 전환된 회원 ID (null이면 미전환)
    created_at      TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);

converted_member_id가 핵심이다. 퀴즈를 완료한 사람이 실제 회원가입까지 이어지면 해당 회원의 ID를 기록한다. 이를 통해:

이 데이터를 기반으로 퀴즈 문항 수, CTA 문구, 결과 페이지 디자인을 반복적으로 개선할 수 있다.


두 엔진이 만드는 그로스 루프

초대 코드와 바이럴 퀴즈는 각각 독립적으로도 작동하지만, 함께 작동할 때 더 강력한 그로스 루프를 만든다.

flowchart TD
    A[신규 가입] --> B[소울 테스트]
    B --> C[매칭 경험]
    C --> D{만족도}
    D -->|높음| E[초대 코드 공유]
    D -->|높음| F[SNS에 결과 공유]
    E --> G[초대된 친구 가입]
    F --> H[바이럴 퀴즈 유입]
    H --> I[퀴즈 결과 공유]
    I --> H
    H -->|전환| A
    G --> A
    D -->|낮음| J[이탈]
    J -.->|피드백| K[제품 개선]
    K --> C

그로스 루프의 핵심 지표는 **K-factor(바이럴 계수)**다.

K-factor = 사용자 1인당 평균 초대 수 × 초대 → 가입 전환율

예를 들어 사용자 1명이 평균 2명을 초대하고, 그 중 60%가 가입한다면:

K = 2 × 0.6 = 1.2

K > 1이면 사용자가 광고 없이도 기하급수적으로 증가한다. K = 1이면 현상 유지, K < 1이면 자연 감소다.

물론 K > 1을 달성하는 것은 쉽지 않다. 특히 데이팅 앱처럼 공유에 민감한 서비스에서는 더욱 그렇다. 그래서 바이럴 퀴즈가 중요한 역할을 한다. “데이팅 앱 쓴다”고 공유하기는 민망하지만, “재미있는 성격 퀴즈 해봤더니 이런 유형이 나왔어”라고 공유하는 것은 자연스럽다. 퀴즈가 바이럴의 **프록시(Proxy)**가 되는 것이다.


기술적 구현: InviteService 핵심 로직

초대 코드 생성

초대 코드는 기억하기 쉬우면서도 충돌 가능성이 낮아야 한다. 썸블링은 영문 대문자 + 숫자 조합의 8자리 코드를 사용한다.

/**
 * 고유한 초대 코드를 생성합니다.
 * 충돌 감지 후 재시도 방식으로 유니크 보장.
 *
 * @return 8자리 영문 대문자 + 숫자 초대 코드
 * @author Rojae
 */
private String generateUniqueCode() {
    String characters = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789";
    // 혼동하기 쉬운 문자 제외: I, O, 0, 1
    SecureRandom random = new SecureRandom();
    String code;
    int maxRetries = 10;

    do {
        StringBuilder sb = new StringBuilder(8);
        for (int i = 0; i < 8; i++) {
            sb.append(characters.charAt(random.nextInt(characters.length())));
        }
        code = sb.toString();
        maxRetries--;
    } while (inviteCodeRepository.existsByCode(code) && maxRetries > 0);

    if (maxRetries == 0) {
        throw new AroException(ErrorCode.INVITE_CODE_GENERATION_FAILED);
    }

    return code;
}

초대 코드 사용: 동시성 처리

초대 코드 사용 시 current_uses를 증가시키는 과정에서 동시성 문제가 발생할 수 있다. 동시에 두 사람이 같은 코드를 사용해 maxUses를 초과하는 상황을 방지해야 한다.

/**
 * 초대 코드를 사용하여 보상을 지급합니다.
 *
 * <p>트랜잭션 내에서 비관적 락(Pessimistic Lock)으로 동시성을 제어합니다.
 * 초대자와 피초대자 모두에게 보상 카드를 지급하며,
 * 사용 횟수가 최대치에 도달하면 코드를 비활성화합니다.</p>
 *
 * @param memberId 초대 코드를 사용하는 회원 ID (피초대자)
 * @param code     사용할 초대 코드 문자열
 * @return 보상 지급 결과
 * @throws AroException INVITE_CODE_NOT_FOUND, ALREADY_REDEEMED, SELF_INVITE_NOT_ALLOWED
 * @author Rojae
 */
@Transactional
public RedeemResult redeemCode(Long memberId, String code) {
    // 비관적 락으로 조회 — 동시 접근 시 대기
    InviteCode inviteCode = inviteCodeRepository
        .findByCodeWithLock(code)
        .orElseThrow(() -> new AroException(ErrorCode.INVITE_CODE_NOT_FOUND));

    // 유효성 검증
    validateRedemption(inviteCode, memberId);

    // 사용 횟수 증가
    inviteCode.incrementUses();

    // 최대 사용 횟수 도달 시 비활성화
    if (inviteCode.getCurrentUses() >= inviteCode.getMaxUses()) {
        inviteCode.deactivate();
    }

    // InviteRedemption 기록 생성
    InviteRedemption redemption = InviteRedemption.builder()
        .inviteCode(inviteCode)
        .inviteeId(memberId)
        .inviterId(inviteCode.getOwner().getId())
        .build();
    inviteRedemptionRepository.save(redemption);

    // 보상 지급: 피초대자 카드 5장, 초대자 카드 3장
    grantRewards(redemption);

    return RedeemResult.of(redemption);
}

private void validateRedemption(InviteCode inviteCode, Long memberId) {
    if (!inviteCode.isUsable()) {
        throw new AroException(ErrorCode.INVITE_CODE_NOT_FOUND);
    }
    if (inviteCode.getOwner().getId().equals(memberId)) {
        throw new AroException(ErrorCode.SELF_INVITE_NOT_ALLOWED);
    }
    if (inviteRedemptionRepository.existsByInviteeIdAndInviteCode(memberId, inviteCode)) {
        throw new AroException(ErrorCode.ALREADY_REDEEMED);
    }
    if (inviteCode.getType() == InviteCodeType.WOMEN_ONLY) {
        Member invitee = memberRepository.findById(memberId)
            .orElseThrow(() -> new AroException(ErrorCode.MEMBER_NOT_FOUND));
        if (invitee.getGender() != Gender.FEMALE) {
            throw new AroException(ErrorCode.INVITE_CODE_NOT_FOUND);
        }
    }
}

Repository에서의 비관적 락 적용:

@Repository
public interface InviteCodeRepository extends JpaRepository<InviteCode, Long> {

    /**
     * 비관적 쓰기 락으로 초대 코드를 조회합니다.
     * 동시 접근 시 첫 번째 트랜잭션이 완료될 때까지 대기합니다.
     *
     * @param code 초대 코드 문자열
     * @return 초대 코드 Optional
     * @author Rojae
     */
    @Lock(LockModeType.PESSIMISTIC_WRITE)
    @Query("SELECT ic FROM InviteCode ic WHERE ic.code = :code")
    Optional<InviteCode> findByCodeWithLock(@Param("code") String code);
}

전환율 측정과 최적화

측정 가능한 퍼널 지표

오가닉 그로스 엔진이 잘 작동하는지 확인하려면 각 단계의 전환율을 추적해야 한다.

flowchart LR
    A[퀴즈 링크 클릭<br/>100%] --> B[퀴즈 완료<br/>목표 80%]
    B --> C[결과 공유<br/>목표 40%]
    C --> D[가입 시도<br/>목표 25%]
    D --> E[가입 완료<br/>목표 20%]

    F[초대 링크 수신<br/>100%] --> G[링크 클릭<br/>목표 60%]
    G --> H[회원가입<br/>목표 50%]

각 단계에서 측정해야 할 지표:

지표측정 방법목표값
퀴즈 완료율completed_at IS NOT NULL 비율80% 이상
퀴즈 공유율공유 버튼 클릭 / 퀴즈 완료40% 이상
공유 → 가입 전환율converted_member_id IS NOT NULL / 총 세션20% 이상
초대 코드 전환율InviteRedemption 수 / 코드 공유 수 추정50% 이상
K-factor(초대 발생 수 × 초대 전환율) / 신규 가입 수0.5 이상 목표

A/B 테스트 포인트

데이터가 쌓이면 다음 요소를 A/B 테스트로 최적화할 수 있다.

퀴즈 관련:

초대 코드 관련:

// 프론트엔드: A/B 테스트 변형에 따른 CTA 렌더링
const ctaVariant = useABTest('quiz_cta_v2');

const ctaText = {
  control: '소울 테스트 전체 보기',
  variant_a: '나에게 맞는 사람 찾기',
  variant_b: '썸블링에서 진짜 나를 발견하세요',
}[ctaVariant];

성비 밸런스: 그로스의 숨은 변수

사용자를 빠르게 늘리는 것만큼 성비 균형을 유지하는 것도 중요하다. 데이팅 앱에서 성비 불균형은 서비스 품질 저하의 직접적 원인이다.

일반적으로 데이팅 앱에는 남성 사용자가 먼저, 그리고 더 많이 유입된다. 이 구조가 고착화되면:

  1. 남성 매칭 풀: 경쟁자 과다 → 매칭 성공률 하락 → 이탈
  2. 여성 매칭 풀: 알림 과부하 → 번아웃 → 이탈
  3. 결과: 전체 서비스 품질 하락

썸블링의 대응 전략:

flowchart TD
    A[성비 모니터링] --> B{여성 비율 < 40%?}
    B -->|Yes| C[여성 전용 코드 배포 확대]
    B -->|Yes| D[여성 타겟 퀴즈 콘텐츠 강화]
    B -->|No| E[일반 그로스 채널 유지]
    C --> F[여성 커뮤니티/인플루언서 협업]
    D --> G[성격 유형 퀴즈 공유 유도]
    F --> H[여성 사용자 증가]
    G --> H
    H --> I[전체 매칭 품질 향상]
    I --> J[남성 사용자 리텐션 상승]
    J --> K[균형 잡힌 성장]

여성 전용 초대 코드는 단순한 마케팅 도구가 아니라 서비스 품질 유지를 위한 운영 도구다. 운영자는 대시보드에서 실시간 성비를 모니터링하고, 불균형이 감지되면 여성 전용 코드 배포를 확대하거나 퀴즈 콘텐츠의 방향성을 조정한다.


마무리: 좋은 제품이 최고의 그로스

초대 코드와 바이럴 퀴즈는 강력한 그로스 도구다. 하지만 솔직히 말하면, 이 모든 것은 제품 자체가 좋아야 작동한다.

사용자가 초대 코드를 공유하는 이유는 보상 카드 3장 때문만이 아니다. “이 앱이 진짜 좋아서 친구한테 알려주고 싶다”는 마음이 있어야 공유가 일어난다. 퀴즈 결과를 SNS에 올리는 것도 “이 결과가 재미있고 나를 잘 표현해서”라는 자발적인 동기에서 나온다.

썸블링이 집중하는 것은 결국 매칭 품질과 사용자 경험이다. OCEAN 모델 기반의 정교한 성격 분석, 코사인 유사도와 가치관 가중합산으로 계산하는 Soul Score, 소울 테스트 결과를 시각화한 레이더 차트. 이 모든 것이 “이 앱 진짜 신기하고 재미있다”는 경험을 만들어야 그로스 엔진이 돌아간다.

바이럴은 좋은 제품 경험의 결과물이지, 좋은 제품 경험을 대체할 수 없다.

그래서 썸블링은 그로스 엔진을 설계하면서도 항상 이 질문을 먼저 한다. “사용자가 이 앱을 쓰고 진심으로 기뻤나?” 그 대답이 Yes일 때, 초대 코드와 바이럴 퀴즈는 그 기쁨을 바깥으로 전파하는 통로가 된다.


Somebling Tech 블로그는 AI 기반 성격 매칭 데이팅 앱 썸블링을 만들며 배운 기술적 경험을 공유합니다. 다음 글에서는 Soul Score 알고리즘의 상세 구현과 매칭 배치 시스템의 설계를 다룰 예정입니다.

댓글

GitHub 계정으로 로그인하여 댓글을 남길 수 있습니다. 허위사실, 욕설, 사칭 등의 댓글은 통보 없이 삭제될 수 있습니다.

블로그로 돌아가기