블로그 목록으로
설계 노트

커뮤니티 덱 마켓플레이스 설계 — 무료 탐색과 유료 다운로드 사이

bloomcardmarketplacemonetizationcommunity

플래시카드 콘텐츠의 근본 문제

플래시카드 앱을 만드는 건 어렵지 않다. 사용자가 계속 쓰게 만드는 앱을 만드는 건 어렵다. 사용자가 플래시카드 앱을 이탈하는 가장 큰 이유는 나쁜 알고리즘이나 못생긴 UI가 아니다. 빈 덱 문제다. 앱을 다운받고, 빈 화면을 보고, 학습하려면 먼저 콘텐츠를 직접 다 만들어야 한다는 걸 깨닫는다. 대부분 그 지점을 넘기지 못한다.

당연한 해결책은 사용자끼리 덱을 공유하는 커뮤니티 마켓플레이스다. 하지만 커뮤니티 마켓플레이스에는 고유한 문제가 있다. 전부 무료로 풀면 공유지의 비극이 발생한다 — 양질의 콘텐츠를 만들 인센티브가 없고, 플랫폼을 유지할 방법도 없다. 전부 페이월로 막으면 마켓플레이스를 살아있게 만드는 탐색 경험이 죽는다. 핵심은 이 두 극단 사이의 선을 찾는 것이다.

BloomCard에서는 "무료 탐색, 유료 다운로드"라는 모델을 채택했다. "유료"라는 표현은 단순화한 것이고, 실제로는 다운로드 쿼터, 인앱 화폐 교환, 크리에이터 보상 경제가 얽힌 시스템이다. 각 레이어를 어떻게 설계했는지 하나씩 풀어보겠다.

탐색: 완전 무료, 예외 없음

첫 번째로 정한 원칙은 탐색에는 절대 비용을 부과하지 않는다는 것이다. 사용자가 마켓플레이스 전체를 둘러볼 수 있어야 한다 — 검색하고, 필터링하고, 설명을 읽고, 평점을 확인하는 모든 과정에서 페이월이나 로그인 요구를 만나면 안 된다. 마켓플레이스는 서점을 걸어 다니는 느낌이어야 하지, 잠긴 문 앞에 서 있는 느낌이면 안 된다.

탐색 화면(ExploreScreen)은 4개 섹션으로 구성했다: 트렌딩 덱, 에디터 추천, 카테고리 그리드, 최신 업로드. 이 구조는 사용자의 의도에 따라 여러 진입점을 제공한다. 일본어 단어장을 찾는 사람은 외국어 카테고리로 바로 이동할 수 있고, 그냥 둘러보는 사람은 트렌딩 덱을 스크롤하며 커뮤니티가 어떤 덱을 만들고 있는지 볼 수 있다.

카테고리와 난이도

분석 결과, 사람들이 실제로 플래시카드 덱을 만드는 주제를 기준으로 7개 카테고리를 정했다: 외국어, 시험, 과학, 역사, IT, 일상, 기타. 이것들은 공유 상수(kCategoryKeys)로 정의돼 앱 전체에서 사용된다 — 업로드 플로우부터 탐색 필터, 데이터베이스 스키마까지. 상수로 관리하면 사용자가 임의 카테고리를 만들 수 있는 플랫폼에서 흔히 보이는 카테고리 분산을 막을 수 있다.

난이도는 BloomCard의 기존 성장 메타포를 그대로 활용해 4단계로 구분했다 — seed, sprout, bud, bloom — kDifficultyKeys로 매핑한다. 이 재사용은 의도적이었다. 사용자들은 이미 정원 시스템에서 seed가 초급이고 bloom이 고급이라는 걸 이해하고 있다. 커뮤니티 덱에도 같은 용어를 적용하면 추가 온보딩 없이 즉각적인 이해가 가능하다.

태그와 필터

각 덱에는 최대 5개 태그를 달 수 있고, deck_tags 조인 테이블에 저장된다. 5개 제한은 기술적 제약보다 UX 제약에 가깝다. 테스트해보니 태그가 너무 많은 덱은 모호하고 도움이 안 되는 태그를 달게 된다. 적은 태그 수는 크리에이터가 덱의 내용을 구체적으로 명시하도록 유도한다.

필터 시스템은 DeckFilter 모델로 캡슐화했다. 카테고리, 난이도, 태그, 정렬 순서를 조합할 수 있다. 사용자는 자유롭게 필터를 조합하고 최신순, 하트순, 다운로드순, 평점순으로 정렬할 수 있다. 필터 UI는 BottomSheet에 배치해 탐색 화면을 깔끔하게 유지하면서 필요할 때 깊은 필터링을 제공한다.

커서 페이지네이션

페이지네이션은 Supabase RPC browse_community_decks_v2를 통한 커서 기반 방식을 선택했다. 오프셋 페이지네이션은 커뮤니티 콘텐츠에서 문제가 된다. 새 업로드가 계속 들어오면 페이지 경계가 밀리면서 스크롤할 때 중복이 보이거나 덱을 놓치게 된다. 커서 페이지네이션은 마지막 아이템의 정렬값을 앵커로 사용하므로, 페이지 로드 사이에 새 업로드가 들어와도 일관된 결과를 보여준다.

browseV2() 메서드가 이걸 깔끔하게 처리한다. 각 페이지가 다음 요청의 시작점으로 쓸 커서를 반환한다. Flutter의 스크롤 기반 레이지 로딩과 결합하면, 덱 라이브러리가 수천 개로 늘어나도 빠른 무한 스크롤 경험을 만들 수 있다.

다운로드 쿼터: 수익화가 시작되는 지점

여기서부터 설계가 흥미로워진다. 탐색은 무료지만, 덱을 로컬 라이브러리에 다운로드하려면 쿼터가 필요하다. 무료 사용자는 월 200장, Pro 사용자는 무제한 다운로드가 가능하다.

단위에 주목하자: 덱이 아니라 카드 수 기준이다. 이건 중요한 설계 결정이었다. 덱 수로 제한하면 크리에이터가 시스템을 악용해 콘텐츠를 작은 덱으로 쪼갤 인센티브가 생기고, 사용자는 5장짜리 덱과 500장짜리 덱이 같은 쿼터를 소비하는 걸 보고 억울해할 것이다. 카드 수 기준 쿼터는 인센티브를 정렬한다 — 크고 포괄적인 덱이 진짜로 더 가치 있고, 쿼터가 그걸 반영한다.

월 200장은 캐주얼 사용자가 몇 개 덱을 제한 없이 다운받을 수 있을 만큼 넉넉하지만, 헤비 사용자가 Pro로 업그레이드하거나 물방울 교환 시스템을 활용하게 만들 만큼은 제한적이다.

물방울 교환

BloomCard에는 물방울이라는 인앱 화폐가 있다. 학습, 퀘스트 완료, 스트릭 유지 등 일일 활동을 통해 획득한다. 사용자는 50방울을 추가 100장 다운로드 카드로 교환할 수 있다(increment_bonus_cards RPC). 이건 돈 없이도 다운로드할 수 있는 보조 경로를 만든다 — 참여도만 있으면 된다.

교환 비율은 플레이테스트를 통해 조정했다. 50방울은 약 3~5일의 정상적인 학습량에 해당하므로, 활발한 무료 사용자는 사실상 매주 100장의 추가 카드를 벌 수 있다. 기본 월 200장 쿼터와 합치면, 꾸준한 무료 사용자는 월 약 600장에 접근할 수 있다. 대부분의 학습 시나리오에 충분한 양이다.

쿼터 추적

download_quota 테이블은 사용자별, 월별로 세 가지 값을 추적한다: 기본 쿼터(무료 200장, Pro 무제한), 물방울 교환으로 얻은 보너스 카드, 사용한 카드 수. increment_used_cards RPC는 다운로드 완료 시 사용 카운트를 원자적으로 증가시키고 총 가용 쿼터와 비교한다.

DownloadQuotaService는 세 가지 메서드를 노출한다: getQuota()로 남은 카드 수 확인, recordDownload()로 쿼터 차감, exchangeDropsForQuota()로 물방울을 보너스 카드로 전환. 서비스는 덱의 카드 수가 남은 쿼터를 초과하는 엣지 케이스도 처리한다 — 사용자는 교환 다이얼로그가 뜨기 전에 정확히 몇 장을 더 다운로드할 수 있는지 볼 수 있다.

전반적인 페이월 전략은 이 글에서 다루고 있지만, 다운로드 쿼터는 특별히 인위적인 차단이 아니라 자연스러운 리소스 제한처럼 느껴지도록 설계했다. 차단당한 게 아니라, 월간 할당량을 다 쓴 것이고, 더 얻을 수 있는 두 가지 명확한 경로가 있다.

소셜 기능: 무료, 단 로그인 필요

소셜 레이어의 모든 것 — 하트, 리액션, 북마크 — 은 무료다. 다만 하트와 리액션은 로그인이 필요하다. 이건 의도적인 전환 퍼널이다. 사용자는 익명으로 탐색할 수 있지만, 소셜적으로 참여하려는 순간 계정이 필요하다. login_guard.dart 모듈이 익명 사용자의 이런 행동을 가로채서 로그인 다이얼로그를 보여준다.

하트

하트는 주요 품질 신호다. 1인 1덱 1개, deck_hearts 테이블에 저장된다. 구현은 옵티미스틱 UI를 사용한다 — 하트를 탭하면 UI가 즉시 업데이트되고 네트워크 요청은 백그라운드에서 처리된다. 요청이 실패하면 HeartNotifier가 UI 상태를 롤백한다. 느린 연결에서도 하트가 즉각적으로 느껴지게 만든다.

prevent_self_heart라는 DB 트리거가 크리에이터가 자기 덱에 하트를 주는 걸 방지한다. 테스트 중에 크리에이터가 업로드 후 가장 먼저 하는 행동이 자기 덱에 하트를 주는 것임을 발견하고 추가했다. 데이터베이스 레벨에서 방지하면 클라이언트 측 우회가 불가능하다.

리액션

리액션은 단순한 좋아요/싫어요보다 더 많은 뉘앙스를 제공한다. 4가지 타입을 선택했다 — petal(꽃잎), lightning(번개), book(책), star(별) — 각각 다른 감정을 전달한다. 꽃잎은 "예쁜 덱", 번개는 "도전적", 책은 "교육적", 별은 "필수". deck_reactions 테이블에 저장되며 하트와 같은 옵티미스틱 UI 패턴을 사용한다.

하트와 마찬가지로 prevent_self_reaction 트리거가 크리에이터의 자기 리액션을 차단한다. 두 트리거 모두 데이터베이스 레이어에 있다. 경쟁적 기능에서 클라이언트 측 검증만으로는 신뢰할 수 없기 때문이다.

북마크

북마크는 순수하게 로컬이다. BookmarkService를 통해 SharedPreferences에 저장된다. 의도적인 선택이었다 — 북마크는 개인적인 정리 도구이지 소셜 신호가 아니다. 로컬에 두면 오프라인에서도 작동하고, 즉각적이고, 소셜 메트릭을 오염시키지 않는다. 사용자는 로그인 없이도 북마크할 수 있어서 생태계로 들어오는 부드러운 훅 역할을 한다.

크리에이터 보상: 콘텐츠 창작에 가치를 부여하다

마켓플레이스는 콘텐츠만큼만 좋고, 콘텐츠는 크리에이터에게서 온다. 덱을 만들고 공유하는 게 보상 없이 느껴지면 사람들은 하지 않을 것이다. 크리에이터 보상 시스템은 모든 업로드가 가치 있게 느껴지고, 크리에이터가 점진적으로 달성할 마일스톤을 제공하도록 설계했다.

첫 업로드 보너스

커뮤니티에 첫 덱을 퍼블리시하는 순간, 50방울과 30 XP를 받는다. 이 즉각적인 보상은 두 가지 목적을 달성한다: 콘텐츠를 만들고 공유한 노력을 인정하고, 계속 활동하도록 격려하는 보상 시스템을 맛보게 해준다.

이벤트 기반 보상

누군가 당신의 덱에 하트를 주면 3방울과 2 XP를 얻는다. 다운로드당 5방울과 3 XP를 얻는다. 작지만 빈번한 보상으로 꾸준한 도파민 드립을 만든다 — 앱을 열면 "당신의 덱 'JLPT N5 단어장'이 오늘 4번 다운로드됐습니다"라는 알림과 함께 해당 방울과 XP를 볼 수 있다.

어뷰징 방지를 위해 일일 캡이 있다: 하트로 50방울, 다운로드로 100방울까지. Pro 사용자는 모든 크리에이터 보상에 1.5배 배수를 받아, 활발한 크리에이터가 구독할 추가적인 인센티브가 된다.

마일스톤 시스템

이벤트 기반 보상 외에, 크리에이터의 의미 있는 성취를 표시하는 7개 마일스톤이 있다:

  • 다운로드: 10, 50, 100, 500회
  • 하트: 10, 50, 100개

각 마일스톤은 물방울과 보너스 다운로드 카드 묶음을 지급한다. MilestoneCelebrationOverlay가 달성 시 3단계 애니메이션을 보여준다 — 닫아야 할 팝업이 아니라 진짜 성취처럼 느껴지도록 설계했다.

마일스톤 수령은 creator_milestone_claims 테이블에서 추적해 이중 수령을 방지한다. 확인은 Supabase에서 서버 사이드로 이루어지므로 클라이언트 조작으로 마일스톤 보상을 악용할 방법이 없다.

크리에이터 티어

크리에이터가 성취를 쌓으면 세 단계를 거친다: none(없음), verified(검증됨), featured(추천). 각 티어는 CustomPainter로 렌더링되는 CreatorBadge로 표현된다(BloomCard의 이모지 미사용 디자인 언어와 일관). 추천 크리에이터는 탐색 화면의 에디터 추천 섹션에서 추가 노출을 받는다.

주간 크리에이터 리포트

매주 월요일, 활성 크리에이터에게 알림(ID=200)이 발송된다. CreatorReportCard가 지난 한 주의 다운로드, 하트, 획득 방울을 전주 대비 트렌드와 함께 보여준다. 이 정기적인 피드백 루프는 마일스톤 달성 사이에도 크리에이터의 참여를 유지한다.

weekly_popular_decks(매시간 갱신)와 creator_leaderboard(매주 갱신) 매터리얼라이즈드 뷰가 이 리포트와 트렌딩 섹션을 구동하며, 메인 테이블에 부하를 주지 않는다.

딥링크: 덱을 공유 가능하게

모든 퍼블리시된 덱은 bloomcard.9-87.org/deck/{share_slug} 형식의 고유 공유 URL을 받는다. share_slug는 자동 생성되는 8자리 문자열이다. 누군가 이 링크를 열면 DeckDeepLinkHandlerDeckPreviewBySlugScreen으로 라우팅하고, 거기서 덱을 미리보고 다운로드할 수 있다(쿼터 제한 적용).

딥링크는 마켓플레이스의 유기적 성장에 필수적이었다. 크리에이터가 소셜 미디어, 포럼, 스터디 그룹에 덱 링크를 공유한다. 공유된 각 링크는 사실상 새 사용자를 생태계로 데려오는 무료 마케팅이다. 8자리 slug는 소셜 공유에 충분히 짧으면서도 충돌을 피할 만큼의 엔트로피(36^8 = 2.8조 조합)를 제공한다.

학습 세션 완료 후에도 덱의 공유 카드를 생성할 수 있다 — 세션 요약과 함께 덱의 공유 링크가 포함된 비주얼 카드다. 모든 학습 세션을 잠재적인 공유 순간으로 만든다.

경제학: 이 모델이 작동하는 이유

시스템의 경제적 논리를 정리해보겠다. 마켓플레이스에는 세 가지 참여자 유형이 있다: 탐색자, 다운로더, 크리에이터. 시스템은 세 유형 모두를 만족시키면서 수익을 만들어야 한다.

탐색자는 서빙 비용이 거의 없다. 캐시된 탐색 엔드포인트와 매터리얼라이즈드 뷰를 조회할 뿐이다. 광고를 보므로(BloomCard는 무료 사용자에게 광고를 보여준다) 전환 없이도 약간의 수익을 만든다. 더 중요한 건, 탐색자가 활발하고 활기찬 마켓플레이스라는 인상을 만들어 크리에이터의 업로드와 다운로더의 참여를 촉진한다는 것이다.

다운로더는 전환 엔진이다. 월 200장 쿼터가 파워 유저의 주요 Pro 전환 트리거다. 한 달에 3~4개 덱을 다운로드하는 사람은 한도에 도달하고 명확한 선택에 직면한다: 무제한 다운로드를 위해 Pro로 업그레이드하거나, 물방울을 교환해서 쿼터를 늘리거나. 어느 쪽이든 깊이 참여하고 있는 상태다.

크리에이터는 콘텐츠 엔진이다. 보상 시스템은 물방울(발행 비용 없음)과 XP(지급 비용 없음)를 쓰지만, 탐색자와 다운로더를 끌어들이는 콘텐츠를 만들어낸다. 크리에이터 보상의 Pro 1.5배 배수는 선순환을 만든다 — 가장 활발한 크리에이터가 구독할 가능성이 가장 높고, 그 구독이 콘텐츠를 호스팅하는 플랫폼을 지탱한다.

물방울 교환은 압력 완화 밸브 역할을 한다. Pro에 돈을 쓸 수 없거나 쓰기 싫은 사용자도 참여를 통해 추가 다운로드에 접근할 수 있다. 이건 하드 페이월 시스템에서 사용자를 이탈시키는 좌절감을 방지한다. 하지만 교환 비율은 월 수백 장 이상 다운로드하는 사람에게는 항상 Pro가 더 나은 거래가 되도록 보정됐다.

데이터베이스 아키텍처

커뮤니티 기능은 Supabase 스키마에 여러 테이블을 추가한다: deck_categories, tags, deck_tags, deck_hearts, deck_reactions, creator_milestone_claims, download_quota. decks 테이블은 category_id, difficulty, heart_count, share_slug, is_featured, report_count, description 컬럼으로 확장됐다.

두 개의 매터리얼라이즈드 뷰가 무거운 분석 쿼리를 처리한다: weekly_popular_decks는 매시간 갱신돼 트렌딩 섹션에 사용되고, creator_leaderboard는 매주 갱신돼 크리에이터 랭킹에 사용된다. 매터리얼라이즈드 뷰를 쓴 건 필수적이었다 — 이런 집계를 탐색 요청마다 돌리면 덱 라이브러리가 커질수록 감당할 수 없는 비용이 된다.

모든 커뮤니티 RPC는 Supabase의 Row Level Security를 거친다. browse_community_decks_v2 RPC는 퍼블리시되고 신고되지 않은 덱만 반환한다. increment_used_cards RPC는 쿼터 한도를 원자적으로 확인한다. 마일스톤 클레임 RPC는 보상 지급 전에 서버 사이드에서 자격을 검증한다.

라우팅

커뮤니티 섹션은 자체 라우트 네임스페이스를 가진다:

  • /community — 메인 탐색 화면 (CommunityBrowseScreen)
  • /community/bookmarks — 북마크한 덱 목록 (BookmarkedDecksScreen)
  • /community/share/:slug — 딥링크를 통한 덱 미리보기 (DeckPreviewBySlugScreen)
  • /community/creator/:userId — 크리에이터 프로필 (CreatorProfileScreen)
  • /explore — 트렌딩, 에디터 추천, 카테고리가 있는 탐색 화면

/community/explore의 분리는 의도적이다. Explore는 발견 표면이다 — 큐레이션되고, 에디토리얼하며, 최고의 콘텐츠를 노출하도록 설계됐다. Community browse는 전체 카탈로그다 — 필터링 가능하고, 정렬 가능하고, 포괄적이다. 다른 의도, 다른 화면.

다르게 했을 것들

돌아보면 재고할 점이 몇 가지 있다. 월 200장 쿼터는 현재로선 적절하지만, 처음부터 A/B 테스트 인프라를 구축해서 다른 한도를 실험해봤어야 했다. 200장이 최적인 걸까, 아니면 150장이 이탈 증가 없이 더 많은 사용자를 Pro로 전환시킬까? 아직 답할 데이터가 없다.

북마크 시스템이 순수 로컬이라서 사용자가 기기를 바꾸면 북마크를 잃는다. 단순함과 오프라인 접근성을 위해 로컬 저장을 선택했지만, 로컬 우선에 로그인 사용자를 위한 선택적 클라우드 싱크를 더하는 하이브리드 접근이 더 나았을 것이다.

리액션 타입(꽃잎, 번개, 책, 별)이 코드에 고정돼 있다. 새 리액션을 추가하려면 앱 업데이트가 필요하다. 리액션 타입을 서버에서 가져오는 설정 기반 접근이 더 유연했을 것이다. 특히 시즌이나 이벤트 전용 리액션을 위해서.

큰 그림

커뮤니티 마켓플레이스는 단순한 기능이 아니라 BloomCard를 지속 가능하게 만드는 플라이휠이다. 크리에이터가 덱을 업로드하고, 그게 학습자를 끌어들이고, 학습자가 덱을 다운로드하며 크리에이터에게 보상을 만들고, 크리에이터가 더 많은 덱을 업로드한다. 다운로드 쿼터는 Pro 구독의 자연스러운 전환 포인트를 만들고, 구독이 플랫폼을 지탱한다. 물방울은 무료 사용자의 참여를 유지하고 콘텐츠 생산을 촉진하는 보조 경제를 만든다.

시스템의 모든 조각 — 무료 탐색, 카드 수 기준 쿼터, 크리에이터 마일스톤, 딥링크, 물방울 교환 — 이 플라이휠을 돌리기 위해 존재한다. 어느 한 조각을 제거해도 시스템이 약해진다. 마켓플레이스 설계는 어떤 단일 기능에 관한 것이 아니라, 모든 기능이 어떻게 상호작용해서 자생적 생태계를 만드는지에 관한 것이다.


BloomCard는 Flutter로 만든 정원 테마 플래시카드 앱입니다. 커뮤니티 마켓플레이스는 열린 탐색과 지속 가능한 수익화의 균형을 맞추도록 설계됐습니다 — UGC 플랫폼을 만들고 계신다면 이 트레이드오프 분석이 도움이 되길 바랍니다.

커뮤니티 덱 마켓플레이스 설계 — 무료 탐색과 유료 다운로드 사이