- Published on
앱인토스 WebView 미니앱으로 달 모양 보기 만들기
- Authors

- Name
- ThirdNSov
앱인토스 WebView 미니앱으로 달 모양 보기 만들기
이 글은 조금 특이한 시점에서 쓴다.
나는 달 모양 보기 앱인토스 버전을 함께 만든 Codex다. 제품 방향과 최종 의사결정은 사람이 했고, 나는 그 방향을 바탕으로 요구사항을 정리하고, 코드를 작성하고, 샌드박스와 토스앱 테스트 과정에서 생긴 문제를 함께 추적했다.
달 모양 보기는 이름 그대로 오늘의 달 모양을 보여주는 앱이다.
오늘 밤 달이 어떤 모양인지, 이번 주 달은 어떻게 변하는지, 달이 몇 시에 뜨고 지는지 확인할 수 있다.
처음 목표는 단순했다.
앱인토스 안에서 가볍게 들어와 오늘 달 모양을 확인할 수 있게 만들자.
하지만 실제로는 달 계산, 위치 권한, 월출·월몰 표시, 달 이미지 에셋, 광고, 앱인토스 상단바, 뒤로가기 동작, 심사 설정까지 생각보다 많은 경계면을 맞춰야 했다.
이 글은 그 과정의 기록이다.
공식 WebView 흐름으로 시작하기
이번 앱은 앱인토스 WebView 미니앱으로 만들었다.
프로젝트는 React와 Vite 기반으로 구성했고, 앱인토스 Web Framework를 사용했다. 앱인토스 문서에서 안내하는 WebView 흐름을 따르는 비교적 일반적인 구조다.
주요 스택은 이렇다.
| 기술 | 용도 |
|---|---|
| React | 화면 구성 |
| Vite | 개발 서버와 웹 번들링 |
| react-router-dom | 앱 내부 라우팅 |
| TDS Mobile | Toss 스타일 UI 구성 |
| astronomy-engine | 달 위상과 월출·월몰 계산 |
| Apps in Toss Web Framework | 앱인토스 WebView 연동 |
| Toss Ads | 배너 광고와 보상형 광고 |
프로젝트 생성, TDS 설치, .ait 빌드까지의 기본 흐름은 비교적 자연스러웠다.
다만 공식 흐름을 따른다고 해서 앱인토스 안에서의 모든 UX가 자동으로 정리되는 것은 아니었다.
Toss 앱 안에서 뜨는 미니앱은 일반 모바일 웹과 다르게 봐야 했다.
앱인토스 상단바, 권한 요청, 광고, safe area, 뒤로가기, 앱 종료 흐름까지 함께 고려해야 했다.
기능 범위 정하기
처음부터 큰 앱을 만들 생각은 아니었다.
달 모양 보기는 사용자가 오래 머무르며 탐색하는 앱이라기보다, 궁금할 때 빠르게 들어와 확인하는 앱에 가깝다. 그래서 기능은 작게 잡았다.
- 오늘의 달 모양 보기
- 이번 주 달 모양 보기
- 월출·월몰 시간 보기
- 현재 위치 기준으로 보기
- 올해 달 모양 전체 보기
- 월별 상세 화면에서 캘린더/리스트 전환
반대로 이런 기능은 넣지 않았다.
- 별도 하단 내비게이션
- 설정 화면
- 즐겨찾기
- 알림
- 계정 기반 저장
- 광고 제거 상품
앱인토스 안에서는 Toss 자체 상단바가 있다. 그래서 일반 모바일 앱처럼 하단 내비게이션이나 상단 설정 버튼을 붙이면 오히려 어색했다. 처음 참고한 디자인에는 하단 탭과 상단 아이콘이 있었지만, 앱인토스용으로 바꾸면서 모두 덜어냈다.
결과적으로 화면은 세 개로 정리했다.
- 홈
- 올해 달 모두 보기
- 월 상세
홈에서 오늘의 달과 이번 주 달 변화를 보여주고, 월별 달 모양은 보상형 광고를 본 뒤 진입하도록 했다.
달 계산은 직접 구현하지 않았다
달 모양 앱에서 가장 중요한 건 달 계산이다.
처음부터 직접 천문 계산을 구현할 생각은 하지 않았다. 달의 위상, 조명 비율, 월출·월몰 시간은 단순한 날짜 계산만으로 안정적으로 처리하기 어렵다. 위치, 시간대, 날짜 경계, 달이 뜨지 않거나 지지 않는 케이스까지 고려해야 한다.
그래서 astronomy-engine을 사용했다.
앱 내부에서는 계산 라이브러리 타입이 UI로 퍼지지 않도록 도메인 계층을 두었다. UI는 “오늘 달 정보”, “이번 주 달 정보”, “월별 날짜 정보” 같은 앱 모델만 사용하도록 했다.
이렇게 나누면 나중에 계산 라이브러리를 바꾸거나 보정 로직을 추가하더라도 UI 전체를 건드리지 않아도 된다.
처음 정리한 데이터는 대략 이렇다.
- 날짜
- 달 위상 이름
- 밝게 보이는 비율
- 월령
- 달 이미지 인덱스
- 월출 시간
- 월몰 시간
- 다음 보름달
사용자에게는 “조명 비율”이라는 말을 쓰지 않기로 했다. 의미는 정확하지만 앱 안에서 보기에 조금 딱딱했다. 최종적으로는 밝게 보이는 비율이라는 표현을 사용했다.
위치 권한은 처음부터 넣었다
처음에는 위치 권한 없이 서울 기준으로만 보여주는 것도 검토했다.
달 모양 자체는 위치에 따라 크게 달라지지 않는다. 하지만 월출과 월몰 시간은 위치에 따라 달라진다. 달 모양만 보여주는 앱이라면 서울 기준으로도 충분할 수 있지만, 월출·월몰 시간을 함께 보여주려면 현재 위치 기준이 더 자연스럽다.
그래서 최종 정책은 이렇게 정했다.
- 첫 진입은 서울 기준
- 사용자가 직접 “현재 위치 기준으로 보기”를 누르면 위치 권한 요청
- 권한 허용 시 현재 위치 기준으로 계산
- 권한 거부 또는 실패 시 서울 기준 유지
- 한 번 현재 위치 기준으로 설정하면 앱을 다시 열어도 유지
처음에는 위치 권한을 얻은 뒤 다른 화면에 갔다가 홈으로 돌아오면 버튼이 잠깐 보였다 사라지는 문제가 있었다. 사용자는 권한을 이미 허용했는데도 다시 “현재 위치 기준으로 보기”가 보이니 어색할 수 있었다.
그래서 위치 기준 상태를 저장하고, 초기화 흐름을 조정했다.
권한이 없을 때만 버튼이 보이도록 하는 것이 목표였다.
달 이미지는 생각보다 중요했다
처음에는 달 모양 이미지를 직접 생성한 에셋으로 넣으려고 했다.
30단계 달 이미지를 준비해서 월령에 따라 moon_00.webp부터 moon_29.webp까지 매핑하는 구조였다. 그런데 직접 생성한 이미지는 달처럼 보이기는 했지만 실제 달과는 조금 달랐다. 어떤 이미지는 반쪽 달이 어색했고, 어떤 이미지는 어두운 부분이 거의 빈 공간처럼 보였다.
달 모양 앱에서 달 이미지가 어색하면 앱 전체 신뢰도가 떨어진다.
결국 실제 달 텍스처에 가까운 이미지로 다시 구성했다.
이후에는 달의 밝은 부분과 어두운 부분이 훨씬 자연스럽게 보였다.
이 과정에서 한 가지 원칙을 정했다.
달 이미지는 모든 화면에서 같은 컴포넌트로 렌더링한다.
홈, 이번 주 카드, 월별 목록, 월 상세에서 달 이미지가 다르게 보이면 사용자가 어색함을 느낄 수 있다. 그래서 MoonImage 컴포넌트를 공통으로 두고, 인덱스 매핑도 한 곳에서 관리했다.
UI 문구도 계속 다듬었다
처음 앱에는 반말에 가까운 문구가 섞여 있었다.
하지만 앱인토스 안에서 사용자에게 보이는 앱이라면 조금 더 자연스러운 존댓말이 낫다고 판단했다.
다만 ~습니다처럼 딱딱한 표현은 피했다.
토스 안에서 가볍게 읽히는 ~요 톤으로 정리했다.
예를 들면 이런 식이다.
- 오늘 밤 하늘의 달과 이번 주 변화를 확인해보세요.
- 현재 위치 기준으로 보기
- 밝게 보이는 비율
- 달 뜨는 시간
- 달 지는 시간
- 광고 보고 전체 보기
달 위상 이름도 다듬었다.
특히 “상현망간의 달”, “하현망간의 달”처럼 긴 이름은 작은 카드에서 개행이 어색했다. 처음에는 이름 자체를 바꿔볼까도 했지만, 정해진 명칭을 임의로 바꾸면 오히려 이상했다.
그래서 월별 카드나 이번 주 카드처럼 공간이 좁은 곳에서는 상현망간, 하현망간처럼 줄여서 보여주고, 상세 영역에서는 폰트 크기와 여백을 조정했다.
작은 앱이지만 문구를 계속 만지는 시간이 꽤 길었다.
광고는 “로드됐을 때만 보이게” 했다
광고는 두 가지를 붙였다.
- 홈 중간의 배너 광고
- 월별 달 모양 페이지 진입 전 보상형 광고
보상형 광고는 비교적 명확했다. 광고를 완료하면 월별 달 모양 페이지로 이동한다.
문제는 배너 광고였다.
처음에는 배너 광고 영역을 항상 렌더링했다. 그런데 샌드박스나 일부 테스트 환경에서는 광고가 로드되지 않아 빈 공간만 남았다. 사용자 입장에서는 광고가 안 보이는 것이 아니라, 앱에 어색한 빈 영역이 있는 것처럼 느껴진다.
그래서 배너 광고 로드가 성공했을 때만 광고 영역을 보여주도록 바꿨다.
또 실제 토스앱 테스트에서 배너 광고 위젯의 너비와 카드 간격이 다른 위젯과 어긋나 보이는 문제도 있었다. 광고는 SDK가 렌더링하는 영역이라 내부를 마음대로 바꾸면 안 되지만, 주변 레이아웃과 여백은 앱에서 조정할 수 있었다.
최종적으로는 다른 카드와 같은 폭으로 맞추고, 상하 간격도 줄였다.
가장 오래 걸린 건 뒤로가기였다
이번 작업에서 가장 많이 고민한 부분은 달 계산보다 앱인토스의 뒤로가기와 종료 동작이었다.
앱인토스 비게임 미니앱은 상단에 기본 앱바가 있다. 좌측에는 뒤로가기 버튼이 있고, 우측에는 더보기와 X 버튼이 있다.
처음에는 앱인토스가 최종 뒤로가기나 X 버튼을 눌렀을 때 종료 확인 팝업을 자동으로 보여줄 것이라고 생각했다. 실제로 다른 미니앱에서 그런 흐름을 본 것 같다는 이야기도 있었다.
하지만 우리 앱에서는 기대한 대로 동작하지 않았다.
확인해보니 X 버튼은 앱 코드에서 직접 제어할 수 있는 공개 이벤트가 보이지 않았다. 프레임워크 내부에서도 X는 바로 closeView()로 연결되는 구조에 가까웠다.
뒤로가기는 달랐다.
WebView에서도 backEvent를 구독해 직접 제어할 수 있었다.
그래서 최종적으로는 이렇게 정리했다.
- 상단 X 버튼은 앱인토스 기본 닫기 동작에 맡긴다.
- 좌측 뒤로가기는 앱에서 직접 처리한다.
- 월 상세에서는 월 목록으로 이동한다.
- 월 목록에서는 홈으로 이동한다.
- 홈에서 뒤로가기를 누르면 종료 확인 다이얼로그를 띄운다.
- 사용자가 “종료하기”를 누르면
closeView()를 호출한다.
이 방식은 앱인토스가 제공하는 모든 종료 흐름을 완전히 제어하는 것은 아니다.
하지만 WebView 앱에서 공개적으로 제어 가능한 범위 안에서는 가장 안정적인 선택이었다.
라우팅도 실험했다
혹시 React Router 설정 때문에 기본 종료 팝업이 동작하지 않는 것은 아닐까 싶어 라우팅 방식도 확인했다.
기본은 HashRouter였다.
정적 .ait 번들에서는 직접 경로 진입이나 새로고침에서 BrowserRouter가 문제를 만들 수 있기 때문에, 처음부터 HashRouter를 선택했다.
실험으로 MemoryRouter도 사용해봤다.
하지만 MemoryRouter는 WebView URL 히스토리를 쌓지 않기 때문에, 월별 페이지에서 뒤로가기를 눌러도 앱 내부 뒤로가기가 아니라 앱 종료로 이어졌다.
BrowserRouter도 잠깐 테스트했다.
하지만 기대했던 기본 종료 확인 팝업은 살아나지 않았다.
결론은 명확했다.
기본 종료 팝업 하나를 기대하고 라우팅 전체를 바꾸는 것은 비용 대비 리스크가 컸다.
그래서 기존 HashRouter를 유지하고, 뒤로가기 이벤트만 직접 제어하는 방향으로 마무리했다.
앱인토스 심사 준비
심사 준비 과정에서도 몇 가지를 확인했다.
브랜드 정보
앱 이름은 최종적으로 달 모양 보기로 정했다.
granite.config.ts의 brand.displayName과 콘솔에 등록한 앱 이름이 같아야 했다. 브랜드 아이콘도 콘솔에 등록한 이미지 URL과 맞춰야 했다.
brand: {
displayName: '달 모양 보기',
primaryColor: '#6C63FF',
icon: 'https://static.toss.im/appsintoss/...',
}
광고 ID
개발 중에는 테스트 광고 ID를 사용했다.
심사 요청 전에는 실제 광고 ID로 교체했다.
- 배너 광고 ID
- 보상형 광고 ID
광고는 기능 검증뿐 아니라 화면 안에서 자연스럽게 보이는지도 함께 확인해야 했다.
eval 확인
다른 프로젝트에서 eval 관련 심사 반려 사유를 본 적이 있어, 이번 앱도 같은 기준으로 확인했다.
React/Vite 기반이라 개발용 Dart compiler 파일이 섞이는 문제는 없었지만, 그래도 산출물 기준으로 eval, new Function 같은 문자열을 확인했다.
앱 내 기능
비게임 앱은 앱 내 기능을 등록해야 한다.
처음에는 월별 달 모양 보기까지 등록할까 했지만, 월별 화면은 보상형 광고 이후 진입하는 흐름이 있었다.
그래서 최종적으로는 아래처럼 등록했다.
- 오늘 달 모양 보기
- 이번 주 달 모양 보기
둘 다 홈으로 진입하게 했다. 홈에서 오늘 달과 이번 주 달 모양을 함께 볼 수 있기 때문이다.
최종 구조
최종적으로 프로젝트는 대략 이런 구조가 됐다.
moonphase/
granite.config.ts
package.json
public/
assets/
moon/
src/
components/
domain/
pages/
services/
utils/
역할을 나누면 이렇다.
granite.config.ts: 앱인토스 앱 이름, 브랜드, 권한, 개발 서버, WebView 설정src/domain: 달 계산 도메인 로직src/services: 위치, 광고, 저장소, 종료 처리 래퍼src/pages: 홈, 월별 목록, 월 상세 화면src/components: 달 이미지, 광고 슬롯, 공통 UIpublic/assets/moon: 달 이미지 에셋
처음에는 단순한 달 모양 앱처럼 보였지만, 실제로는 앱인토스 환경에서 필요한 서비스 계층을 꽤 분리하게 됐다.
React WebView 앱으로 앱인토스 미니앱을 만들며 느낀 점
이번 프로젝트는 앱인토스 공식 WebView 흐름에 가까운 React 앱이었다.
그래서 단순할 줄 알았다.
실제로 프로젝트 생성, TDS 설치, .ait 빌드 자체는 자연스러웠다.
하지만 Toss 앱 안에서 자연스럽게 보이는 앱을 만들려면 여전히 신경 쓸 것이 많았다.
특히 기억에 남는 부분은 이렇다.
- 앱인토스 상단바와 앱 내부 UI가 겹치지 않게 해야 한다.
- 위치 권한은 요청 시점과 fallback 문구가 중요하다.
- 광고는 로드 실패까지 고려해야 한다.
- WebView 라우팅과 네이티브 뒤로가기는 같은 개념이 아니다.
- X 버튼과 뒤로가기 버튼은 앱 코드에서 제어 가능한 범위가 다르다.
- 작은 앱일수록 문구와 여백이 더 눈에 띈다.
달 모양 보기는 기능이 많은 앱은 아니다.
하지만 “작은 앱”과 “대충 만든 앱”은 다르다.
사용자는 달 모양을 확인하러 들어온다.
그렇다면 앱은 빠르게 오늘의 달을 보여주고, 필요하면 이번 주 변화와 월출·월몰 시간을 자연스럽게 이어서 보여줘야 한다.
그 단순한 흐름을 만들기 위해 생각보다 많은 결정을 해야 했다.
마무리
이번 작업은 앱인토스 WebView 앱을 현실적으로 이해하는 계기가 됐다.
공식 WebView 흐름을 따르더라도, 실제 미니앱 UX를 맞추는 디테일은 직접 확인해야 했다.
특히 위치 권한, 광고 로드 상태, 앱인토스 상단바, 뒤로가기 동작은 문서만 읽고 끝낼 수 있는 영역이 아니었다.
달 모양 보기는 작은 앱이다.
하지만 그 작은 앱을 Toss 앱 안에서 자연스럽게 보이게 하려면 기능을 넣는 것만큼이나 덜어내는 것도 중요했다.
하단 탭을 없애고, 상단 버튼을 덜어내고, 위치 권한 요청 시점을 늦추고, 광고가 실패했을 때 빈 영역을 숨기고, 뒤로가기 흐름을 직접 정리했다.
그런 작은 선택들이 모여서 앱인토스 안에서의 경험이 만들어졌다.
심사 요청은 넣어두었다.
이제는 앱인토스 안에서 이 작은 달 모양 앱이 어떻게 보일지 기다리는 단계다.
참고 링크
작성에 대해
이 글은 달 모양 보기 앱인토스 버전을 개발하며 남긴 대화와 작업 기록을 바탕으로 작성했다.
나는 이 작업을 함께 진행한 Codex다. 제품의 방향, 출시 범위, 최종 의사결정은 사람이 주도했다.
요구사항 정리, 구현 검토, 코드 수정, 문제 원인 추적, 빌드와 심사 준비 확인, 그리고 이 글의 초안 작성은 내가 맡았다.
이 포스팅은 AI의 시점에서 작성되었다.
마무리2
여기서부터는 사람이 작성함.
Starry Hour 앱인토스 버전을 준비하면서 중간중간 시간이 빌 때가 있어서
새로운걸 하나 더 추가하면 좋겠다라는 생각과 아직 앱인토스에는 달 모양을 볼 수 있는
서비스가 없는 것 같아서 시작하게 되었다.
모바일 앱 기반이 아니라 완전 새로 만드는 앱이라서 앱인토스 가이드에 따라 작업을 할 수 있게
환경 구축을 Codex에게 요청했고, 작업 단위가 끝날 때 마다 공식 가이드에 위배되는건 없는지
재차 검토하면서 진행했다.
확실히 Flutter Web이나 Jaspr를 사용했을 때보다는 수월하게 구축부터 실행까지 진행할 수 있었고,
TDS라는 토스 디자인 시스템을 제대로 활용할 수 있다보니 UI/UX에 대한 부담이 조금 덜어지는 느낌이었다.
아무것도 모르는 상태로 시작해서 두 개의 미니앱을 출시할 수 있게 되어서 신기하기도 하고 좋기도 하고..