본문으로 건너뛰기

튜토리얼: 첫 번째 앱 만들기

이 튜토리얼에서는 활성 채팅 기록을 읽고 역할별(사용자 vs 어시스턴트)로 단어 수를 세어 통계를 표시하는 단어 카운터 미니 프로그램을 만듭니다. 이 튜토리얼을 마치면 미니 프로그램을 생성, 테스트, 패키징, 게시하는 방법을 알게 됩니다.

배울 내용:

  • manifest.json 만들기
  • window.ais SDK로 앱 HTML 작성
  • ais.chat.getHistory()로 메시지 읽기
  • ais.storage로 세션 간 데이터 유지
  • 사이드로딩으로 로컬 테스트
  • .ais 번들로 패키징
  • 커뮤니티 레지스트리에 게시

소요 시간: 약 15분.


1단계: 매니페스트 만들기​

모든 미니 프로그램은 앱, 권한, 진입점을 설명하는 manifest.json이 필요합니다. 새 디렉토리를 만들고 이 파일을 추가하세요:

mkdir word-counter
cd word-counter

manifest.json 생성:

{
"name": "word-counter",
"version": "1.0.0",
"abi": 1,
"type": "mini-program",
"title": "Word Counter",
"description": "Count words in your chat history by role",
"author": { "name": "Your Name" },
"entry": "index.html",
"base_url": "https://localhost:8080/",
"permissions": ["storage", "chat:read", "ui:toast"],
"keywords": ["statistics", "utility"]
}

주요 필드:

필드값이유
nameword-counter고유 식별자 (소문자, 하이픈만)
abi1필수 -- 현재 플랫폼 ABI와 일치
typemini-program플랫폼에 이것이 샌드박스 iframe 앱임을 알림
entryindex.html로드할 HTML 파일
base_urlhttps://localhost:8080/에셋이 호스팅되는 위치 (프로덕션용으로 업데이트)
permissions["storage", "chat:read", "ui:toast"]채팅 읽기와 통계 저장 필요
정보

storage 권한은 항상 자동으로 부여되지만, 사용자가 앱이 데이터를 저장한다는 것을 알 수 있도록 명시적으로 나열하는 것이 좋습니다.


2단계: HTML 만들기​

같은 디렉토리에 index.html을 만듭니다. 이것이 전체 앱입니다 -- HTML, CSS, JavaScript가 한 파일에 있습니다.

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<style>
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family:
system-ui,
-apple-system,
sans-serif;
padding: 20px;
color: #e0e0e0;
background: #1a1a2e;
min-height: 100vh;
}
h1 {
font-size: 22px;
margin-bottom: 16px;
}

.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
gap: 12px;
margin-bottom: 20px;
}
.stat-card {
background: #16213e;
border: 1px solid #333;
border-radius: 8px;
padding: 16px;
text-align: center;
}
.stat-value {
font-size: 32px;
font-weight: 700;
color: #58a6ff;
font-variant-numeric: tabular-nums;
}
.stat-label {
font-size: 14px;
color: #888;
margin-top: 4px;
}

.actions {
display: flex;
gap: 10px;
flex-wrap: wrap;
}
button {
min-height: 48px;
padding: 10px 20px;
font-size: 16px;
border: 1px solid #444;
border-radius: 6px;
background: #2a2a4e;
color: #e0e0e0;
cursor: pointer;
}
button:hover {
background: #3a3a5e;
}
button:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.btn-primary {
background: #1a73e8;
border-color: #1a73e8;
}
.btn-primary:hover {
background: #1557b0;
}
.btn-close {
background: none;
border-color: #666;
color: #888;
}

#last-updated {
margin-top: 16px;
font-size: 14px;
color: #666;
}
</style>
</head>
<body>
<h1>단어 카운터</h1>

<div class="stats-grid">
<div class="stat-card">
<div class="stat-value" id="total-words">--</div>
<div class="stat-label">총 단어</div>
</div>
<div class="stat-card">
<div class="stat-value" id="user-words">--</div>
<div class="stat-label">내 단어</div>
</div>
<div class="stat-card">
<div class="stat-value" id="ai-words">--</div>
<div class="stat-label">AI 단어</div>
</div>
<div class="stat-card">
<div class="stat-value" id="msg-count">--</div>
<div class="stat-label">메시지</div>
</div>
</div>

<div class="actions">
<button class="btn-primary" id="analyze">채팅 분석</button>
<button class="btn-close" id="close">닫기</button>
</div>

<div id="last-updated"></div>

<script>
// 헬퍼: 문자열의 단어 수 세기
function countWords(text) {
if (!text || typeof text !== "string") return 0;
return text.trim().split(/\s+/).filter(Boolean).length;
}

// 숫자를 쉼표와 함께 포맷 (1234 -> "1,234")
function fmt(n) {
return n.toLocaleString();
}

ais.ready(async function () {
// 패널 제목 설정
ais.ui.setTitle("단어 카운터");

// 마지막으로 저장된 통계 복원 시도
var saved = await ais.storage.get("last-stats");
if (saved) {
showStats(saved);
}

// 분석 버튼
document
.getElementById("analyze")
.addEventListener("click", async function () {
this.disabled = true;
this.textContent = "분석 중...";

try {
// 채팅 기록에서 최대 500개 메시지 가져오기
var messages = await ais.chat.getHistory(500);

var userWords = 0;
var aiWords = 0;
var msgCount = messages.length;

for (var i = 0; i < messages.length; i++) {
var msg = messages[i];
var words = countWords(msg.content);
if (msg.role === "user") {
userWords += words;
} else if (msg.role === "assistant") {
aiWords += words;
}
}

var stats = {
total: userWords + aiWords,
user: userWords,
ai: aiWords,
messages: msgCount,
timestamp: Date.now(),
};

showStats(stats);

// 다음을 위해 통계 저장
await ais.storage.set("last-stats", stats);

ais.ui.toast(msgCount + "개 메시지 분석 완료!");
} catch (err) {
ais.ui.toast("에러: " + err.message);
}

this.disabled = false;
this.textContent = "채팅 분석";
});

// 닫기 버튼
document.getElementById("close").addEventListener("click", function () {
ais.close();
});
});

function showStats(stats) {
document.getElementById("total-words").textContent = fmt(stats.total);
document.getElementById("user-words").textContent = fmt(stats.user);
document.getElementById("ai-words").textContent = fmt(stats.ai);
document.getElementById("msg-count").textContent = fmt(stats.messages);

if (stats.timestamp) {
var date = new Date(stats.timestamp);
document.getElementById("last-updated").textContent =
"마지막 분석: " + date.toLocaleString();
}
}
</script>
</body>
</html>

이 코드가 하는 일​

  1. ais.ready() -- SDK 브리지가 연결될 때까지 기다린 후 로직 실행.
  2. ais.storage.get('last-stats') -- 이전에 저장된 통계를 복원하여 사용자가 실행 시 즉시 데이터를 볼 수 있음.
  3. ais.chat.getHistory(500) -- 활성 대화에서 최대 500개 메시지 가져오기.
  4. 단어 수 세기 -- 메시지를 반복하며 콘텐츠를 공백으로 분할하고 역할별로 집계.
  5. ais.storage.set('last-stats', stats) -- 다음을 위해 결과 유지.
  6. ais.ui.toast() -- 분석 완료 시 알림 표시.
  7. ais.close() -- 사용자가 닫기를 클릭하면 채팅 화면으로 돌아감.

3단계: 로컬 테스트​

매니페스트와 HTML 파일을 제공할 로컬 HTTP 서버가 필요합니다. 선호하는 도구를 사용하세요:

# Python 3
cd word-counter
python3 -m http.server 8080

# 또는 Node.js
npx serve -p 8080

# 또는 PHP
php -S localhost:8080

이제 플랫폼에 앱을 설치합니다:

  1. aiscouncil.com 열고 로그인
  2. 왼쪽 사이드바의 앱 아이콘 클릭
  3. 사이드로드 섹션에서 붙여넣기: http://localhost:8080/manifest.json
  4. 설치 클릭
  5. 권한 검토 (storage, chat:read, ui:toast)하고 허용 클릭
  6. 설치된 앱 카드에서 열기 클릭
팁

가장 빠른 개발 루프를 위해 URL 사이드로딩 대신 HTML 업로드를 사용하세요. index.html을 직접 업로드하면 서버가 필요 없습니다. 플랫폼이 자동으로 합성 매니페스트를 생성합니다. 변경할 때마다 제거하고 다시 업로드할 수 있습니다.

문제 해결​

문제해결책
"매니페스트 가져오기 실패"로컬 서버가 실행 중이고 CORS 헤더를 제공하는지 확인. python3 -m http.server 8080은 CORS-safe하게 제공합니다.
앱이 빈 흰 페이지 표시브라우저 콘솔에서 에러 확인. 가장 일반적인 문제는 ais.ready() 전에 ais.* 메서드를 호출하는 것입니다.
"PermissionDenied: chat:read"매니페스트에 chat:read가 권한 배열에 없습니다. 매니페스트를 업데이트하고 재설치하세요.
코드 변경 후 앱이 업데이트되지 않음먼저 앱을 제거(카드의 X 버튼)한 다음 재설치. 진입 HTML은 설치 시점에 캐시됩니다.

4단계: 다듬기 추가​

실시간 단어 수 세기 기능을 추가해 봅시다. 새 메시지가 도착하면 실시간으로 업데이트됩니다.

닫기 버튼 핸들러 뒤, ais.ready() 콜백 안에 이 코드를 추가하세요:

// 실시간 카운팅을 위해 새 메시지 구독
ais.chat.onMessage(function (msg) {
// 저장된 통계를 다시 읽고 새 메시지의 단어 추가
ais.storage.get("last-stats").then(function (stats) {
if (!stats) return;
var words = countWords(msg.content);
if (msg.role === "user") stats.user += words;
else if (msg.role === "assistant") stats.ai += words;
stats.total = stats.user + stats.ai;
stats.messages++;
stats.timestamp = Date.now();
showStats(stats);
ais.storage.set("last-stats", stats);
});
});

이제 사용자가 채팅할 때 "분석"을 다시 클릭할 필요 없이 카운터가 실시간으로 업데이트됩니다.


5단계: .ais 번들로 패키징​

.ais 번들은 매니페스트와 모든 앱 파일이 포함된 ZIP 아카이브입니다. 플랫폼은 ZIP을 추출하고 매니페스트를 읽고 모든 에셋(CSS, JS, 이미지)을 진입 HTML에 인라인합니다.

우리 같은 단일 파일 앱의 경우 번들은 간단합니다:

cd word-counter
zip -r ../word-counter.ais manifest.json index.html

상위 디렉토리에 word-counter.ais가 생성됩니다.

번들 테스트​

  1. 플랫폼에서 앱으로 이동하여 앱 업로드 클릭
  2. word-counter.ais 선택
  3. 권한 검토 및 승인
  4. 모든 에셋이 인라인된 상태로 번들에서 앱이 설치됨
정보

번들은 독립적입니다. 사용자는 원래 base_url에 대한 네트워크 접근이 필요 없습니다. 모든 것이 설치 시점에 인라인됩니다. 이것이 번들을 오프라인 배포와 공유에 이상적으로 만듭니다.

다중 파일 번들​

앱에 별도의 CSS, JavaScript, 이미지 파일이 있는 경우 모두 ZIP에 포함하세요:

zip -r ../word-counter.ais manifest.json index.html style.css app.js icon.png

플랫폼은 자동으로 인라인합니다:

  • <link rel="stylesheet" href="style.css"> → <style>...</style>
  • <script src="app.js"></script> → <script>...</script>
  • <img src="icon.png"> → <img src="data:image/png;base64,...">

6단계: 레지스트리에 게시​

앱이 다른 사람들이 사용할 준비가 되면 커뮤니티 레지스트리에 게시합니다.

1. 파일 호스팅​

매니페스트와 진입 HTML을 공개 CDN에 업로드합니다. GitHub Pages는 무료이고 쉽습니다:

# GitHub 저장소에서 (예: github.com/yourname/word-counter)
# manifest.json과 index.html을 main 브랜치에 푸시
# 저장소 설정에서 GitHub Pages 활성화 (소스: main 브랜치, 루트)

파일은 다음에서 사용할 수 있습니다:

  • https://yourname.github.io/word-counter/manifest.json
  • https://yourname.github.io/word-counter/index.html

매니페스트의 base_url을 업데이트하세요:

"base_url": "https://yourname.github.io/word-counter/"

2. aiscouncil 저장소 포크​

github.com/nicholasgasior/bcz로 이동하여 Fork를 클릭합니다.

3. 패키지 항목 추가​

registry/packages.json을 편집하고 packages 배열에 항목을 추가합니다:

{
"name": "word-counter",
"type": "mini-program",
"version": "1.0.0",
"manifest": "https://yourname.github.io/word-counter/manifest.json",
"tier": "community",
"category": "utilities",
"description": "Count words in your chat history by role",
"icon": "https://yourname.github.io/word-counter/icon.png",
"added": "2026-02-19",
"price": 0,
"currency": "USD",
"seller": null
}

4. 검증​

검증 스크립트를 실행하여 항목을 확인합니다:

python3 registry/validate.py packages

검증이 통과하면 제출할 준비가 된 것입니다.

5. 풀 리퀘스트 제출​

포크에 변경사항을 푸시하고 메인 저장소에 대한 PR을 생성합니다. 자동화된 검증이 통과하면 PR이 병합될 수 있으며 앱이 플랫폼의 앱 스토어 섹션에 나타납니다.

가격 책정, 판매자 설정, 검증 등급에 대한 자세한 내용은 레지스트리에 게시를 참조하세요.


팁과 모범 사례​

디자인​

  • 다크 테마 기본 -- 대부분의 플랫폼 사용자는 다크 모드를 사용합니다. 어두운 배경(#1a1a2e 또는 유사)과 밝은 텍스트(#e0e0e0)로 디자인하세요.
  • 48px 최소 터치 타겟 -- 버튼과 인터랙티브 요소는 접근성과 VLM 친화적 상호작용을 위해 최소 48px 높이여야 합니다.
  • 14px 최소 폰트 크기 -- 모든 텍스트는 가독성을 위해 최소 14px여야 합니다.
  • 반응형 레이아웃 -- 앱 패널 너비는 다양합니다. 적응하기 위해 auto-fit과 함께 CSS Grid 또는 Flexbox를 사용하세요.

성능​

  • 저장소에 결과 캐시 -- 계산된 결과를 저장하기 위해 ais.storage를 사용하세요. 사용자가 실행 시 즉시 데이터를 볼 수 있도록 복원합니다.
  • 채팅 기록 요청 제한 -- ais.chat.getHistory(500)이면 보통 충분합니다. 무제한 기록 요청을 피하세요.
  • 폴링 금지 -- getHistory를 반복 호출하는 대신 실시간 업데이트를 위해 ais.chat.onMessage()를 사용하세요.

보안​

  • 최소 권한 요청 -- 앱이 실제로 사용하는 권한만 나열하세요. 권한이 적을수록 더 많은 사용자가 앱을 신뢰하고 설치합니다.
  • 모든 입력 검증 -- ais.chat.getHistory()의 데이터는 사용자 생성 콘텐츠를 포함합니다. DOM에 삽입하기 전에 새니타이즈하세요.
  • 민감한 데이터 저장 금지 -- ais.storage는 암호화되지 않습니다. 비밀번호, 토큰, API 키를 저장하지 마세요 (앱에 secrets:sync가 있고 명시적으로 자격 증명 전송을 처리하지 않는 한).

호환성​

  • ais.platform.abi 확인 -- 앱이 특정 SDK 기능에 의존하는 경우 ABI 버전을 확인하고 플랫폼이 오래된 경우 도움이 되는 메시지를 표시하세요.
  • SDK 호출을 try/catch로 감싸기 -- 권한 에러와 플랫폼 버전 차이로 거부가 발생할 수 있습니다. 우아하게 처리하세요.
  • hello-world 예시로 테스트 -- 플랫폼은 참조로 사용할 수 있는 예시 미니 프로그램과 함께 제공됩니다.