앱인토스 미니앱 13 – 멍스타그램
앱인토스 미니앱 제작기 열세 번째는 멍스타그램입니다. 강아지 사진 두 장이 위아래로 뜨면 더 마음에 드는 쪽을 누르는 앱이에요. 한 판에 최대 10쌍을 고르고, 참가한 강아지는 그 표로 이번 주 대결 순위가 정해집니다.
멍스타그램은 이 제작기 줄기에서 처음으로 직접 운영하는 서버를 붙인 앱입니다. 여러 사람이 고른 표를 한곳에 모아 순위를 내는 앱이라서요. 그래서 이번 글은 화면 이야기보다 숫자를 어떻게 보여 줄지와 서버가 맡는 일을 더 길게 적었습니다.
멍스타그램은 이런 앱인토스 미니앱이에요



멍스타그램의 하단 탭은 대결과 순위 두 개입니다. 대결 탭에서 사진을 고르고, 순위 탭에서 이번 주 순위를 훑어봐요. 우리 강아지를 참가시키면 홈의 내 강아지 카드와 리포트 시트에서 전적과 받은 간식을 확인합니다.
화면 속 사진에 대해
위 화면의 사진은 전부 앱에 들어 있는 시범견 12마리 중에서 나왔고, 이 시범견 사진은 AI로 만든 반려견 사진이에요. 순위와 리포트 화면은 시범견 사진에 예시 이름과 전적을 넣은 화면이라 실제 대결 기록이 아니에요. 실제 순위 화면에서 시범견은 순위 없이 “시범견” 배지로 나옵니다.
두 장 중 한 장만 고르면 한 판이 끝나요
멍스타그램의 대결 카드는 사진 두 장을 위아래로 붙이고 가운데에 VS 리본을 둔 모양입니다. 한 장을 누르면 주황색 테두리와 체크 표시가 생기고, “다음 쌍” 버튼으로 넘어가요. 자동으로 넘기지 않아서 고른 걸 확인하고 넘어갈 수 있습니다. 판의 마지막 쌍에서는 버튼이 “결과 보기”로 바뀝니다.
쌍은 서버가 정합니다. 최근 4주 동안 대결이 적었던 강아지부터 먼저 편성하고, 내 강아지는 내 판에 나오지 않아요. 같은 조합은 한 사람에게 하루 한 번만 나오고, 한 사람이 하루에 낼 수 있는 표는 300표까지입니다. 쌍마다 서버가 확인용 표식(토큰)을 붙여 보내고, 그 표식이 맞는 표만 셉니다.
앱을 열면 서버 응답을 기다리지 않고 시범견 6쌍으로 된 연습 대결이 먼저 뜹니다. 서버에서 진짜 판이 오면 “진짜 대결이 준비됐어요”라는 안내와 함께 바뀌고, 서버에 닿지 않으면 연습을 계속해요. 연습 모드는 순위에 반영되지 않는다고 화면에 적혀 있습니다.
앱인토스 소개 – 인앱 광고(In-App Ads), 종류와 붙이는 방법대결이 다섯 번 쌓이기 전에는 승률을 보여 주지 않아요
멍스타그램의 실제 대결에서는 사진 위에 그 강아지의 최근 4주 전적이 나오고, 한 장을 고르면 방금 낸 표까지 더한 숫자로 바뀝니다. 그런데 1승 0패를 “승률 100%”로 띄우면 숫자가 과장돼 보이죠. 기획서에는 “5회 미만 표본을 승률로 포장하지 않는다”는 기준이 적혀 있고, 코드는 이렇게 생겼어요.
function recordLabel(wins: number, losses: number) {
const games = wins + losses;
return games >= 5 ? `${wins}승 ${losses}패 · 승률 ${Math.round((wins / games) * 100)}%` : `${wins}승 ${losses}패`;
}이 몇 줄이 하는 일
- 대결이 5회 미만이면 “1승 0패”처럼 승패만 보여 줍니다.
- 5회부터 “8승 14패 · 승률 36%”처럼 승률이 붙어요. 위 리포트 화면 맨 위에 가려진 카드가 그 모양입니다.
- 대결 화면과 결과 화면에 들어가는 승패는 최근 4주 합계입니다. 결과 화면에도 “선택한 강아지의 최근 4주 전적을 표시해요. 승률은 대결 5회부터 보여요.”라는 문장이 있어요.
1승 0패가 바로 1위가 되지 않게 계산해요
멍스타그램 주간 순위도 같은 문제가 있습니다. 승률 순으로 줄을 세우면 한 번 이기고 끝난 강아지가 스무 번 넘게 싸운 강아지보다 위로 올라갈 수 있어요. 순위 점수는 이번 주 승패에 5승과 10판을 미리 더해서 계산합니다.
예를 들어 계산해 보면 차이가 보입니다. 1승 0패는 (1+5)÷(1+10)으로 약 0.55, 8승 2패는 (8+5)÷(10+10)으로 0.65예요. 단순 승률로는 100%와 80%라 1승 0패가 앞서지만, 이 점수로는 8승 2패가 위에 섭니다. 대결이 쌓일수록 미리 더한 5와 10의 영향은 작아지고 실제 승률에 가까워져요.
const fraction = (entry) => ({ n: BigInt(entry.wins) + 5n, d: BigInt(entry.wins + entry.losses) + 10n });
ranked.sort((a, b) => {
const left = fraction(a);
const right = fraction(b);
const diff = right.n * left.d - left.n * right.d;
return diff > 0n ? 1 : diff < 0n ? -1 : a.id.localeCompare(b.id);
});
let rank = 0;
let prior = null;
const dogs = ranked.map((entry, index) => {
const now = fraction(entry);
if (!prior || now.n * prior.d !== prior.n * now.d) rank = index + 1;
prior = now;
return { ...entry, rank };
});코드에서 볼 곳
- 점수를 소수로 나누지 않고 분자와 분모를 엇갈려 곱해서 비교합니다. 그래서 점수가 같은 두 강아지는 정확히 같다고 판정되고 같은 순위를 받아요.
- 순위를 매기는 대상은 지금 공개 중인 참가견뿐입니다. 시범견과 내려간 사진은 이 목록에 들어오지 않아요.
- 순위 화면에는 승률 %를 찍지 않고 승패만 적습니다. 기획서에 “순위는 보정 점수라 승률 순과 다를 수 있다”고 적어 둔 부분이에요.
이번 주 대결이 아직 없는 참가견은 순위 없이 “대결 전” 배지를 달고 뒤에 붙습니다. 참가견이 12마리가 안 되면 시범견이 순위 대신 “시범견” 배지와 “연습용” 표시를 달고 남은 자리를 채워요.
앱인토스 소개 – 스마트 메시지(Smart Message, 푸시알림) 규칙, 보내기 전에 알아둘 것간식은 선물로만 쓰고 순위에는 넣지 않아요
멍스타그램에서 한 판을 끝까지 고르면 간식 1개를 받습니다. 결과 화면에서 보상형 광고(Rewarded Ad)를 끝까지 보면 2개가 더 들어오는데, 평생 첫 판에는 이 제안이 나오지 않고, 한 판에 한 번, 3분 간격으로만 서버가 받아 줍니다. 광고를 건너뛰어도 기본 1개와 결과, 순위는 똑같아요.
간식은 “간식 주기” 버튼으로 공개된 참가견에게 한 번에 1개씩 줄 수 있습니다. 받은 개수는 그 강아지의 카드와 리포트에 따로 쌓이고, 앞에서 본 순위 점수에는 들어가지 않아요. 잔액은 주마다 따로 세기 때문에 새 주가 되면 지난주 간식은 쓸 수 없게 됩니다. 시범견에게는 줄 수 없고요.
응원 링크로 친구에게 간식을 받아요
공개가 끝난 강아지의 보호자는 “응원 링크 보내기”를 누를 수 있습니다. 앱인토스의 공유 링크 기능(Share.createLink)으로 앱 안 주소와 미리보기 그림을 묶은 링크를 만들고, “우리 강아지 응원하러 와 주세요!”라는 문장과 함께 메시지로 보내요.
미리보기 그림은 멍스타그램이 1200×600 크기로 직접 그립니다. 왼쪽 주황색 칸에 강아지 이름과 “이번 주 대결에서 응원해 주세요”가 들어가고, 오른쪽에 사진이 붙어요. 링크를 누르고 들어온 사람에게는 홈 맨 위에 그 강아지 카드와 “응원 간식 주기” 버튼이 뜨고, “링크로 들어오면 이번 주에 한 번, 간식 없이도 응원할 수 있어요.”라는 안내가 붙습니다.
이 응원 간식은 잔액에서 빠지지 않는 대신 한 사람이 같은 강아지에게 일주일에 한 번만 보낼 수 있어요. 서버 코드에서 일반 간식과 응원 간식이 갈리는 부분입니다.
async giveSnack({ owner, dogId, week, cheer = false }) {
const target = await this.env.DB.prepare("SELECT id,is_demo,owner_key FROM dogs WHERE id=? AND status='APPROVED'").bind(dogId).first();
if (!target || target.is_demo) throw Object.assign(new Error('DOG_NOT_ELIGIBLE'), { status: 404 });
if (cheer && target.owner_key === owner) throw Object.assign(new Error('OWN_DOG_CHEER'), { status: 409 });
return this.state.storage.transaction(async (txn) => {
const balanceKey = `snacks:${week}:${owner}`;
const balance = await txn.get(balanceKey) ?? 0;
if (cheer) {
const cheeredKey = `cheered:${week}:${owner}`;
const cheered = new Set(await txn.get(cheeredKey) ?? []);
if (cheered.has(dogId)) throw Object.assign(new Error('ALREADY_CHEERED_THIS_WEEK'), { status: 409 });
cheered.add(dogId);
await txn.put(cheeredKey, [...cheered]);
} else {
if (balance < 1) throw Object.assign(new Error('NO_SNACKS'), { status: 409 });
await txn.put(balanceKey, balance - 1);
}
const receivedKey = `received:${week}:${dogId}`;
const received = (await txn.get(receivedKey) ?? 0) + 1;
await txn.put(receivedKey, received);
return { snacks: cheer ? balance : balance - 1, received };
});
}코드에서 볼 곳
- 맨 위 세 줄에서 공개 승인되지 않은 강아지와 시범견을 막고, 내 강아지에게 응원 링크로 간식을 보내는 것도 막습니다.
- 잔액 열쇠 이름에 주(
week)가 들어 있어요. 간식이 주마다 새로 세어지는 이유가 이 이름입니다. - 응원은 “이번 주에 응원한 강아지 목록”에 이미 있으면 거절하고, 일반 간식은 잔액이 1개 미만이면 거절해요.
이번에는 기기 밖에 서버를 붙였어요
앞서 쓴 물마시기나 경조사 봉투장부는 기록을 휴대폰 안에만 저장했습니다. 멍스타그램은 그렇게 할 수가 없어요. 내가 고른 표가 다른 사람 화면의 전적과 순위에 반영돼야 하니까, 모든 사람의 표가 모이는 곳이 따로 있어야 합니다.
그 자리를 맡은 게 클라우드플레어 워커(Cloudflare Workers)라는 서버예요. 앱이 “다음 판 주세요”, “이 표 받아 주세요”, “간식 줄게요” 같은 요청을 보내면 워커가 받아서 처리하고 결과를 돌려줍니다.
데이터베이스(D1)에 두는 것
- 참가한 강아지의 이름·소개·공개 상태
- 사진 신고 기록
- 알림 신청 기록과 보낼 알림 목록
리그 담당 듀러블 오브젝트(Durable Object)에 두는 것
- 그날 낸 표와 오늘 본 조합
- 주간 승패와 대결 기록
- 간식 잔액과 받은 간식 수
사람은 토스 로그인 없이 구분합니다. 앱이 토스에게 받은 1회용 인증 코드를 서버에 보내면, 서버가 앱인토스 서버와 직접 교환해 사용자 식별키(anonKey)를 받아요. 공식 문서 기준으로 이 인증 코드는 300초 동안 유효하고 한 번만 쓸 수 있습니다.
멍스타그램은 남의 사진을 받는 앱이라 사진 처리도 서버 몫이 커졌어요. 참가 신청은 사진을 1080×810 WebP로 다시 그려서 보내고, 서버는 사진 속성(EXIF) 정보가 남아 있으면 받지 않습니다. 운영자가 확인한 사진만 공개되고, 확인이 끝나지 않은 신청은 7일 뒤 지우도록 만들어 두었어요. 저작권이나 개인정보 사유로 신고된 사진은 바로 숨기고, 다른 사유는 서로 다른 3명이 신고하면 숨깁니다.
알림도 신청한 사람에게만 갑니다. 내 강아지 시트에서 사진 공개 직후 알림과 매주 월요일 오전 10시 주간 결과 알림을 하나씩 따로 켜고 끌 수 있게 했고, 주간 결과 알림은 대결 기록이 있는 주에만 보내도록 만들었어요. 알림 규칙은 스마트 메시지 규칙 글에 정리해 두었습니다.
출시 상태
기획서는 2026년 10월 7일에 승인됐고, 프로젝트 상태 기록에는 10월 14일 라이브로 적혀 있습니다. 시범견 연습, 순위 공식, 간식 규칙은 이 글을 쓴 10월 19일에 원본 코드에서 확인한 내용이에요.
이 프로젝트를 어떤 식으로 진행하는지는 프로젝트 진행 방식에, 결과 화면의 광고를 붙이는 방법은 인앱 광고 종류와 붙이는 방법에 있습니다.
업종과 업무에 맞춘 AX 설계 상담은 무료 AX 상담 신청에서 받습니다.
멍스타그램 자주 묻는 질문
멍스타그램은 어떤 앱인가요?
강아지 사진 두 장 중 한 장을 고르는 대결을 한 판에 최대 10쌍씩 심사하고, 참가한 강아지의 주간 전적과 순위를 보는 앱인토스 미니앱입니다. 한 판을 마치면 간식을 받아 마음에 드는 강아지에게 줄 수 있습니다.
승률은 언제부터 보이나요?
최근 4주 대결이 5회 이상 쌓인 강아지부터 승률이 함께 나옵니다. 그 전에는 몇 승 몇 패인지만 보여 줍니다.
간식을 많이 받으면 순위가 올라가나요?
올라가지 않습니다. 순위는 이번 주 대결의 승패만으로 계산하고, 간식은 받은 개수를 따로 보여 주는 선물입니다.
이 글은 앱인토스 미니앱 「멍스타그램」의 기록입니다. 앱인토스는 토스 앱 안에서 미니앱을 서비스하는 플랫폼이고, 공식 안내는 앱인토스 개발자센터에 있습니다.
이 글은 직접 만들고 운영하며 남긴 기록입니다. 적힌 수치는 작성 시점의 운영 계정 기준이며, 같은 결과나 수익을 보장하지 않습니다.
