시작하며
토스 앱 안에서 돌아가는 미니앱을 만들어봤습니다. 서비스 이름은 풍수지리 — 사용자의 사주와 특정 장소의 지형 기운을 AI로 분석해주는 서비스입니다.
"토스 안에서 돌아가는 웹앱"이라고 하면 단순해 보이지만, 실제로 개발해보니 일반 웹서비스와는 꽤 다른 제약과 규칙들이 있었습니다. 이 글은 그 과정에서 배운 것들을 정리한 기록입니다.
앱인토스가 뭔가요?
앱인토스(Apps in Toss)는 토스 앱 내부에서 실행되는 WebView 기반 미니앱 플랫폼입니다. 사용자 입장에서는 토스 앱을 벗어나지 않고 다양한 서비스를 이용할 수 있고, 개발자 입장에서는 토스의 3,000만 MAU에 접근할 수 있다는 장점이 있습니다.
기술적으로는 @apps-in-toss/web-framework SDK를 사용하고, ait build / ait dev 명령어로 번들을 생성해 콘솔에 업로드하는 방식입니다.
기술 스택
React 18 + TypeScript + Vite 6
@apps-in-toss/web-framework 2.4.1 ← 미니앱 SDK
Firebase (Auth, Firestore, Cloud Functions)
Framer Motion ← 화면 전환 애니메이션
Kakao Maps SDK ← 지도
OpenStreetMap Nominatim ← 역지오코딩
가장 먼저 맞닥뜨린 것 — 규칙의 벽
일반 웹서비스라면 Google 로그인, Stripe 결제, AdSense 광고를 붙이면 되는데, 앱인토스는 이 세 가지를 모두 자체 SDK로만 해야 합니다.
기능 | 일반 웹 | 앱인토스 |
|---|---|---|
로그인 | Google, Kakao, 이메일 | 토스 로그인만 |
결제 | PG사 (토스페이먼츠 등) | 인앱결제 SDK만 |
광고 | AdSense, 카카오 등 | 앱인토스 광고 SDK만 |
처음에는 답답하게 느껴졌는데, 막상 구현해보니 각각 SDK가 잘 추상화되어 있어서 오히려 개발이 단순해지는 면도 있었습니다.
로그인 구현
토스 로그인의 흐름은 이렇습니다.
appLogin() → authorizationCode 발급
→ Firebase Cloud Function (tossLogin) 호출
→ 토스 서버에서 사용자 정보 복호화 (서버 전용)
→ Firebase Custom Token 발급
→ signInWithCustomToken()으로 Firebase 인증 완료
프론트 코드는 놀랍도록 간단합니다.
const { appLogin } = await import('@apps-in-toss/web-framework');
const { authorizationCode, referrer } = await appLogin();
const result = await tossLoginFn({ authorizationCode, referrer });
await signInWithCustomToken(auth, result.data.customToken);
한 가지 중요한 제약이 있었습니다. 사용자 개인정보(이름, 생년월일, 전화번호 등)는 AES-256-GCM으로 암호화된 상태로 전달되기 때문에, 프론트에서는 절대 복호화할 수 없습니다. 서버(Cloud Function)에서만 처리해야 합니다. 처음에 이걸 모르고 프론트에서 처리하려다 삽질했습니다.
뒤로가기 버튼 처리
안드로이드의 하드웨어 뒤로가기 버튼이 문제였습니다. 지도에서 핀을 찍고 AI 분석 결과 화면으로 이동했을 때, 뒤로가기를 누르면 결과 화면이 닫히는 게 아니라 미니앱 자체가 종료되는 현상이 있었습니다.
SDK에 useBackButtonEffect 같은 전용 훅은 없어서, 표준 웹 History API로 해결했습니다.
// 결과 오버레이가 열릴 때 더미 히스토리 엔트리 추가
const handleShowResult = (payload) => {
history.pushState({ resultOverlay: true }, '');
setResultPayload(payload);
};
// popstate 이벤트로 뒤로가기 가로채기
useEffect(() => {
if (!resultPayload) return;
const onPopState = () => setResultPayload(null);
window.addEventListener('popstate', onPopState);
return () => window.removeEventListener('popstate', onPopState);
}, [resultPayload]);
토스 앱의 뒤로가기는 결국 브라우저의 history.back()과 동일하게 동작하기 때문에, history 스택에 엔트리가 있으면 앱이 종료되지 않고 popstate가 발생합니다.
AI 고지 의무 — 법적 의무입니다
생성형 AI 결과물에 라벨을 붙이는 건 앱인토스 정책이기도 하지만, 국내 법률상 의무입니다. 위반 시 최대 3,000만 원 과태료가 부과될 수 있습니다.
구현은 간단합니다. AI가 생성한 텍스트가 노출되는 모든 컴포넌트에 배지를 달았습니다.
<p style={{ fontSize: '11px', color: 'var(--fs-text-muted)', marginTop: 8 }}>
AI가 생성한 해설이에요. 참고용으로 활용해요.
</p>
인앱결제
결제는 처음에 제일 걱정했는데, 오히려 제일 단순했습니다. 외부 PG 연동 없이 SDK 함수 하나로 처리됩니다.
중요한 건 미결 주문 복원 로직입니다. 앱 진입 시 getPendingOrders()를 호출해서 결제는 됐지만 지급이 안 된 주문이 있으면 처리해줘야 합니다. 네트워크 오류, 앱 강제 종료 등으로 이런 케이스가 실제로 발생하기 때문에 필수입니다.
Kakao Maps가 visibility:hidden에서 렌더링 안 되는 문제
탭 전환 시 지도 탭을 display: none으로 숨겼다가 다시 보여주면 Kakao 지도가 크기를 잡지 못하는 문제가 있었습니다. 지도는 컨테이너 크기를 초기화 시점에 계산하기 때문입니다.
해결책은 display: none 대신 visibility: hidden + pointer-events: none으로 DOM에는 유지한 채 숨기는 방식입니다.
<div style={{
position: 'absolute', inset: 0,
visibility: tab === 'map' ? 'visible' : 'hidden',
pointerEvents: tab === 'map' ? 'auto' : 'none',
}}>
<MapHome ... />
</div>
멀티 프로필 관리
사주·풍수는 본인 외에 가족이나 배우자의 프로필도 함께 분석하고 싶은 케이스가 많습니다. Firestore와 localStorage를 이중으로 운용했습니다.
Firestore: 기기 간 동기화, 로그인 상태에서 사용
localStorage: 오프라인 fallback, 빠른 초기 렌더링
앱 진입 시 Firestore에서 프로필을 불러오고, 없으면 localStorage를 사용합니다. 프로필 추가/삭제 시 두 곳 모두 업데이트합니다.
Firebase CORS 설정 — 꼭 잊지 마세요
배포 후 가장 많이 겪는 문제 중 하나입니다. Firebase Authentication의 Authorized Domains에 토스 도메인을 반드시 추가해야 합니다.
pungsu-jiri.private-apps.tossmini.com ← QR 테스트용
pungsu-jiri.apps.tossmini.com ← 라이브
이걸 빠뜨리면 auth/unauthorized-domain 에러가 나는데, 로컬에서는 잘 되다가 토스 앱 WebView에서만 로그인이 안 되는 상황이 됩니다. Firebase Console에서 수동으로 추가해야 하므로 배포 체크리스트에 넣어두세요.
SDK 버전 제약
2026년 3월 23일부터 SDK 1.x로 빌드한 번들은 콘솔 업로드가 불가합니다. @apps-in-toss/web-framework 2.x 이상을 유지해야 합니다. 이 날짜가 생각보다 빨리 왔기 때문에 레거시 프로젝트가 있다면 미리 마이그레이션해두는 게 좋습니다.
마치며
앱인토스 미니앱 개발은 일반 웹 개발과 비슷하면서도, 플랫폼 정책을 지키는 데서 오는 제약이 꽤 많습니다. 특히 로그인·결제·광고 세 영역에서 외부 서비스를 일절 쓸 수 없다는 점은 처음에 답답하게 느껴지지만, 역설적으로 구현이 단순해지는 면도 있습니다.
가장 실질적인 조언은 세 가지입니다:
AI 고지 라벨은 법적 의무 — 설계 초반부터 넣으세요
Firebase Authorized Domains — 배포 직전 체크리스트에 반드시 포함
하드웨어 뒤로가기 — SDK 전용 훅이 없으므로
history.pushState로 직접 처리