앱인토스 미니앱 물 마시기 화면 세 장
|

앱인토스 미니앱 1 – 물마시기

앱인토스 미니앱 제작기의 첫 번째 앱은 물마시기입니다. 제가 만든 미니앱 중 가장 많이 쓰이는 앱이고, 9월 28일 주에는 한 주에 630명이 썼습니다.

외부 API도 서버도 쓰지 않고, 버튼 하나를 누르면 물병이 차오르는 것이 전부인 앱입니다. 이 프로젝트가 어떤 방식으로 진행되는지는 앱인토스 제작기 소개 글에 먼저 적었습니다.

이 글은 그 앱의 제작기입니다. 화면을 어떻게 짰는지, 이 앱의 핵심 코드 세 조각, 그리고 사용자가 3주 만에 일곱 배로 늘어난 과정을 적습니다.

로컬에서 앱을 직접 실행해 찍은 시연 영상 (소리 없음)

물마시기는 어떤 앱인토스 미니앱인가

토스 앱 안에서 열리는 물 기록 앱입니다. 반 잔, 한 잔, 큰 잔 중 하나를 누르면 기록되고, 오늘 목표까지 몇 잔 남았는지가 바로 보입니다.

물마시기 첫 화면. 오늘 목표까지 3잔 남았다는 문구와 절반 찬 물병, 반 잔·한 잔·큰 잔 버튼
첫 화면: 남은 잔과 기록 버튼
최근 7일 날짜 칩과 오늘 기록 세 건이 시간순으로 보이는 기록 시트
날짜별 기록 시트
하루 기록 목표와 한 잔 크기를 고르는 설정 시트
목표·잔 크기 설정

위 화면은 9월에 만든 개편 버전입니다. 화면은 하나이고 시트가 두 개 붙어 있습니다. 탭도 온보딩도 로그인도 없습니다. 기록은 기기 안에만 저장합니다.

만든 도구는 앱인토스 웹 프레임워크와 React, 토스 디자인 시스템입니다. 기획안은 AI 에이전트가 쓰고 제가 승인했고, 구현도 에이전트가 했습니다.

4주 사이에 사용자가 일곱 배가 됐다

콘솔에서 확인한 주간 사용자 수입니다.

주 9/7 9/14 9/21 9/28
주간 사용자 88 89 199 630
그중 신규 – 51 174 576

3주 만에 일곱 배가 됐습니다. 마지막 주 유입의 95%는 토스 전체탭 노출이었어요. 토스가 이 앱을 더 많은 사람에게 보여주기 시작한 겁니다.

누가 쓰는지도 이때 처음 제대로 봤습니다. 50대가 36%, 60대 이상이 40%이고 여성이 77%, 안드로이드가 88%였어요. 제가 떠올렸던 사용자와는 많이 달라서, 그 뒤로 글자 크기와 화면 대비를 다시 보게 됐습니다.

함께 읽기앱인토스 미니앱 11 – 체중 다이어리앱인토스 미니앱 체중 다이어리 제작기입니다. 오늘 체중을 빠르게 적고 7일·30일 흐름과 주간 요약을 보는 앱을 만들면서, 빠진 날짜를…

노출을 받은 구조 네 가지

노출은 제가 정한 것이 아니지만, 노출을 받을 만한 구조는 설계한 것입니다. 돌아보니 이 앱에는 네 가지가 있었습니다.

  1. 입력 부담이 없다. 숫자를 치지 않고 버튼 한 번으로 끝납니다.
  2. 피드백이 즉시 온다. 누르는 순간 물병이 차고 남은 잔이 줄어듭니다.
  3. 하루에 여러 번 열 이유가 있다. 물은 하루에 여러 번 마십니다.
  4. 데이터 때문에 멈출 일이 없다. 외부 API가 없어서 불러오다 실패하는 화면이 없습니다.

퍼센트 대신 ‘남은 잔’으로

처음 버전은 첫 화면에 달성률을 퍼센트로 보여줬습니다. 50%라는 숫자는 정확하지만 다음에 무엇을 하라는 것인지 알려주지 않습니다. 그래서 9월 개편에서 “3잔 남았어요”로 바꿨습니다. 계산은 이것이 전부입니다.

waterState.tsts
export function getRemainingCups(goalMl: number, consumedMl: number, cupMl: number): number {
  const goal = normalizePositive(goalMl, defaultState.dailyGoalMl);
  const cup = normalizePositive(cupMl, defaultState.cupSizeMl);
  const consumed = typeof consumedMl === 'number' && Number.isFinite(consumedMl) ? Math.max(0, consumedMl) : 0;
  return Math.ceil(Math.max(0, goal - consumed) / cup);
}

올림과 방어 두 가지만 챙기면 됩니다

  • Math.ceil로 올립니다. 100ml가 남았을 때 “0잔 남았어요”가 나오면 안 됩니다.
  • 목표나 잔 크기가 0이거나 깨진 값이면 기본값(1,500ml, 250ml)으로 바꿉니다. 저장된 값이 깨져도 물병은 항상 그려져야 합니다.

같이 고친 것이 문구입니다. 예전 화면에는 “성인은 물 2L” 같은 문장이 있었습니다. 필요한 수분량은 사람마다 다르고, 앱이 권장량을 말하는 순간 건강 조언이 됩니다. 전부 지우고 “권장량이 아닌 내가 정한 기록 목표예요”로 바꿨습니다. 건강 주제의 토스 미니앱을 만든다면 검수 전에 이 부분부터 확인하는 것을 권합니다.

함께 읽기앱인토스 제작기 – 프로젝트 소개앱인토스 제작기 프로젝트를 소개합니다. AI와 함께 토스 미니앱을 만들면서 정한 작업 방식과 출시 전 점검 단계, 앞으로 쓸 글의 구성을…

전면 광고는 결과가 나오는 순간에만

토스 미니앱의 광고는 배너와 전면 두 가지를 썼습니다. 제 앱들에서는 노출 한 번의 값이 전면 쪽이 배너보다 훨씬 컸습니다.

문제는 전면 광고를 아무 데나 넣으면 사용자가 나간다는 점입니다. 토스의 노출 가이드도 전면 광고가 나오는 시점을 미리 알리고 반복하지 말라고 요구합니다. 그래서 규칙을 이렇게 정했습니다.

  • 광고는 결과가 나오는 순간에 둔다. 이 앱에서는 오늘 목표를 처음 채운 순간과 주간 분석 버튼, 두 가지입니다.
  • 사용자가 “광고 보고 인증 카드 받기”처럼 광고 표시가 붙은 버튼을 직접 눌렀을 때만 띄운다. 목표 달성 화면에는 “광고 없이 닫기”를 항상 같이 둔다.
  • 광고를 보면 광고 없이는 못 받는 것을 준다. 목표 달성 쪽은 몇 번째 달성인지 찍힌 공유용 인증 카드입니다.

횟수 제한은 훅 안에 넣었습니다. 관련된 두 부분만 뽑았습니다.

useInterstitialAd.tsts
const cliffSession = { shown: 0, lastAt: 0 };
const CLIFF_SESSION_MAX = 3;
const CLIFF_GAP_MS = 3 * 60 * 1000;

const canCliff = useCallback(() => loadedRef.current
  && cliffSession.shown < CLIFF_SESSION_MAX
  && Date.now() - cliffSession.lastAt >= CLIFF_GAP_MS, []);

한 번 실행에 최대 3회, 직전 광고 뒤 3분이 지나야 하고, 광고가 이미 로드돼 있을 때만 참이 됩니다. 화면 쪽에서는 목표를 넘는 순간에 이 값만 확인합니다.

HomePage.tsxts
if (getTodayRecord(before).totalMl < before.dailyGoalMl && getTodayRecord(next).totalMl >= next.dailyGoalMl) {
  TossHaptic.success();
  // 목표를 채운 순간이 이 앱의 결과 공개 장면이다. 광고를 고르면 달성 인증 카드를 준다.
  if (canCliff()) setGoalCliff(true);
  else showToast('🎉 오늘 물 목표 달성! 축하해요', '분석 보기', handleOpenWeeklyReport);
}

광고를 못 띄우는 상황이 기본값입니다

canCliff()가 거짓이면 광고 화면 자체를 만들지 않고 축하 토스트만 띄웁니다. 광고가 로드되지 않았다고 사용자의 달성 순간까지 막으면 안 됩니다.

광고가 안 떠도 3초 안에는 넘어가게

전면 광고에서 가장 위험한 것은 사용자가 버튼을 눌렀는데 아무 일도 일어나지 않는 상태입니다. 광고 SDK가 응답하지 않으면 화면이 멈춘 것처럼 보입니다. 그래서 3초 타이머를 걸었습니다.

useInterstitialAd.tsts
const show = useCallback((onClose: (watched: boolean) => void) => {
  let done = false;
  let impressed = false;
  const finish = () => { if (!done) { done = true; onClose(impressed); load(); } };
  if (!loadedRef.current || !cliffSupported()) { finish(); return; }
  loadedRef.current = false;
  cliffSession.shown += 1;
  cliffSession.lastAt = Date.now();
  const guard = window.setTimeout(finish, 3000);
  try {
    showFullScreenAd({
      options: { adGroupId },
      onEvent: (event) => {
        if (event.type === 'show' || event.type === 'impression') { impressed = true; window.clearTimeout(guard); }
        if (event.type === 'dismissed' || event.type === 'failedToShow') { window.clearTimeout(guard); finish(); }
      },
      onError: () => { window.clearTimeout(guard); finish(); },
    });
  } catch {
    window.clearTimeout(guard);
    finish();
  }
}, [adGroupId, load]);

끝나는 길을 하나로 모으세요

  • 광고가 닫히든, 실패하든, 예외가 나든, 3초가 지나든 전부 finish 하나로 끝납니다. done 플래그가 있어서 두 번 실행되지 않습니다.
  • 광고가 실제로 화면에 뜨면 타이머를 풉니다. 안 그러면 광고를 보는 도중에 다음 화면으로 넘어갑니다.
  • 횟수와 시각은 광고를 띄우기 직전에 올립니다. 앞에서 본 세션당 3회, 3분 간격 제한이 여기서 채워집니다.
  • 광고가 실패해도 인증 카드는 줍니다. 사용자는 광고를 보겠다고 고른 사람이기 때문입니다.

이 구조는 물마시기에서 먼저 만들고, 10월 5일에 다른 앱들에도 옮겼습니다. 그때 옮긴 쪽은 타이머를 5초로 잡았습니다.

다음에 할 일

알림은 매일 저녁 8시에 나가고, 알림을 받고 들어온 사람은 그 뒤로도 꾸준히 다시 찾아왔습니다. 그래서 다음 작업은 이쪽에 맞췄어요.

  1. 알림을 더 많은 사람이 받게 하기. 첫 기록 직후에 자연스럽게 제안하도록 동의 흐름을 다듬습니다.
  2. 다음 날 다시 열 이유 만들기. “어제 3잔, 오늘 목표는 6잔”처럼 어제와 이어지는 한 줄을 넣으려고 합니다.
  3. 글자 크기와 대비 점검하기. 사용자 넷 중 셋이 50대 이상이라 읽기 편한 화면이 중요합니다.

물마시기 자주 묻는 질문

토스 미니앱은 서버 없이도 만들 수 있나요?

물마시기는 서버와 외부 API 없이 만들었습니다. 기록은 기기 안에만 저장합니다. 데이터를 불러오다 실패할 일이 없고 서버 비용도 들지 않습니다.

토스 미니앱에 전면 광고는 어디에 넣나요?

결과가 나오는 순간에만 넣었습니다. 물마시기에서는 오늘 목표를 채운 순간과 주간 분석 버튼 두 가지이고, 광고 표시가 붙은 버튼을 사용자가 직접 눌렀을 때만 띄웁니다.

사용자는 어디서 들어왔나요?

주간 사용자가 199명에서 630명으로 늘어난 주에는 유입의 95%가 토스 전체탭 노출이었습니다. 토스 앱 안에서 더 많은 사람에게 소개되기 시작한 시기입니다.

이 글은 앱인토스 미니앱 「물마시기」의 기록입니다. 앱인토스는 토스 앱 안에서 미니앱을 서비스하는 플랫폼이고, 공식 안내는 앱인토스 개발자센터에 있습니다.

이 글은 직접 만들고 운영하며 남긴 기록입니다. 적힌 수치는 작성 시점의 제 계정 기준이며, 같은 결과나 수익을 보장하지 않습니다.

Similar Posts

6 Comments

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다