도쿄에서 Even G2와 R1을 사고 며칠 만에 플러그인까지 만들었습니다. 안경을 쓰고 텔레그램 메시지를 확인하고, G2 마이크로 답장을 보내는 서드파티 텔레그램 클라이언트 G2gram을 만들었어요.
처음부터 거창한 이유가 있었던 건 아닙니다. 제가 찾아본 기존 텔레그램 플러그인은 사용자가 api_id와 api_hash를 직접 발급받아 넣거나, BotFather에서 봇 토큰을 만들고 별도 봇을 거치는 방식이 많았습니다. 음성 입력까지 쓰려면 STT 서비스의 API 키도 따로 요구하더라고요.
“이 기기를 사는 사람이라면 이런 설정쯤은 알아서 하겠지만, 꼭 그래야 하나?”
Even G2 사용자가 모두 개발자는 아닙니다. 기기에 익숙하지 않은 사람도 텔레그램 계정으로 로그인만 하면 바로 쓸 수 있었으면 했어요. 그렇게 시작한 일이 MTProto 클라이언트를 직접 만들고, Whisper를 WebAssembly로 올리고, SDK 버전을 되돌리는 삽질로 커졌습니다.

봇 토큰과 외부 STT API 키 없이 로그인, 대화 확인, 로컬 음성 답장을 한 흐름으로 묶었습니다.
제가 없애고 싶었던 건 설정 과정이었습니다
핵심은 세 가지였습니다.
- 사용자가 개인 텔레그램 API 키나 봇 토큰을 만들지 않아도 될 것
- 음성 인식을 외부 STT 서버에 보내지 않을 것
- 로그인 세션과 메시지를 제가 운영하는 프록시나 서버에 통과시키지 않을 것
여기서 “API 키가 없다"는 표현은 조금 정확하게 짚어야 합니다. Telegram API로 서드파티 클라이언트를 만들려면 애플리케이션의 api_id와 api_hash 자체는 필요합니다. G2gram은 사용자가 자기 키를 발급받아 입력하게 하지 않고, G2gram 클라이언트용 자격증명을 앱에 포함했습니다. 이 값만으로 사용자 계정에 로그인할 수 있는 건 아니고, 실제 인증은 각 사용자의 전화번호와 인증 코드, 필요한 경우 2단계 인증 비밀번호로 진행됩니다.
현재 G2gram은 두 가지 로그인 경로를 구현했습니다.
- 다른 기기의 텔레그램 앱으로 스캔하는 QR 로그인
- 전화번호, 인증 코드, 2단계 인증 비밀번호를 사용하는 로그인
로그인이 끝나면 만들어진 MTProto 세션만 Even Hub의 기기 로컬 저장소에 보관합니다. 인증 코드와 2단계 인증 비밀번호는 저장하지 않고, 텔레그램 데이터도 제가 운영하는 서버로 보내지 않습니다.
사용자 입장에서는 텔레그램 계정으로 로그인하면 끝입니다. 그 한 단계를 만들기 위해 앱 안쪽은 오히려 훨씬 복잡해졌습니다.
봇을 우회하지 않고 텔레그램에 직접 붙였습니다
G2gram은 Telegram Bot API를 쓰는 알림용 봇이 아닙니다. TypeScript로 MTProto 클라이언트를 구현해 텔레그램 데이터 센터와 직접 통신합니다.
사용자와 Even G2
↓
Even Realities 앱의 WebView
↓
G2gram MTProto 클라이언트
↓ MTProto over WSS
Telegram 데이터 센터
앱 매니페스트의 네트워크 화이트리스트도 텔레그램 웹 데이터 센터 다섯 곳만 열었습니다.
[
"wss://pluto.web.telegram.org",
"wss://venus.web.telegram.org",
"wss://aurora.web.telegram.org",
"wss://vesta.web.telegram.org",
"wss://flora.web.telegram.org"
]
직접 구현한 범위는 생각보다 깊습니다. WebSocket 전송 계층부터 MTProto 인증 키 교환, 암호화 세션, TL 스키마 직렬화, 전화번호·QR 로그인, 2단계 인증, 대화 목록과 포럼 토픽, 읽음 상태, 메시지 전송, 미디어 다운로드까지 들어갔습니다.
현재 0.1.6에서는 휴대폰 WebView에서 로그인과 설정, 채팅을 다루고 G2 화면에서는 채팅 목록을 넘기며 메시지를 읽고 답장할 수 있습니다. 그룹과 포럼 토픽의 읽지 않은 메시지 수도 계산하고, 최근에는 사진과 정적 이미지 미리보기도 대화 흐름 안에 넣었습니다.

G2의 녹색 화면에서 채팅 목록과 읽지 않은 메시지 수를 확인하고 터치패드나 R1으로 대화를 고릅니다.
공식 텔레그램 앱을 그대로 옮긴 수준은 아닙니다. 지금은 텍스트 읽기와 전송, 채팅 탐색, 일부 이미지 미리보기에 집중했고, 동영상·음성 메시지·파일·반응·통화까지 모두 지원하는 완성형 클라이언트라고 부르기는 어렵습니다.
STT API 키 대신 Whisper를 앱에 넣었습니다
음성 입력의 출발점은 G2의 4개 마이크에서 들어오는 16kHz, 16비트 리틀엔디언 모노 PCM입니다. 녹음이 끝나면 변환과 인식을 한 번에 이어 갑니다. PCM을 Float32Array로 바꿔 별도 Web Worker의 whisper.cpp WebAssembly 런타임에 넘기고, 나온 문장을 G2에서 확인한 뒤 탭 한 번으로 텔레그램에 보냅니다.

안경을 탭해 녹음을 마치면 로컬 Whisper가 문장을 변환하고, 확인 화면을 거쳐 메시지를 보냅니다.
PCM은 열려 있는데 STT는 열려 있지 않습니다
공개 SDK는 음성 인식 결과를 주지 않습니다. 그래서 Whisper를 직접 넣었습니다. audioControl로 G2 마이크를 켜도 플러그인이 받는 건 원시 PCM뿐입니다. 기본 앱에는 음성을 텍스트로 바꾸는 기능이 있지만, 서드파티 플러그인이 짧은 문장 하나를 STT로 요청할 공개 API는 없습니다.
이건 센서 하나가 빠진 것보다 더 크게 느껴졌습니다. 이유는 단순합니다. G2에서 쓸 수 있는 입력 수단이 사실상 안경다리 터치, R1 링 터치, 마이크뿐이기 때문입니다. 터치와 링은 선택·스크롤에는 맞지만 긴 문장을 입력하기 어렵습니다. 메시지, 검색, AI 질의처럼 텍스트가 필요한 플러그인에서는 마이크가 사실상 유일한 입력 장치예요.
개발자에게 남는 선택은 두 가지입니다.
- 외부 STT 서비스와 API 키, 필요하면 프록시 서버까지 요구하기
- 저처럼 로컬 모델과 WASM 런타임을 플러그인에 통째로 넣기
Even 쪽 서버에서 STT를 처리하면 비용, 동시 처리 용량, 개인정보 동의 문제가 생긴다는 건 이해합니다. 그래도 선택지는 있었으면 합니다. 길이와 호출량을 제한한 STT 브리지 하나만 SDK로 열려도 G2의 제한된 입력 장치를 활용하는 플러그인이 훨씬 많아질 겁니다.
로컬 음성 인식에는 Whisper Base q5_1 양자화 버전을 넣었습니다. 파일명은 ggml-base-q5_1.bin이고 크기는 약 56.9MB입니다.
현재 구현은 스트리밍 음성 인식이 아닙니다. 녹음 중에는 PCM 조각을 메모리에 모으고, 사용자가 탭해 녹음을 끝낸 뒤 전체 샘플을 한 번에 Whisper로 넘깁니다. 원래 원했던 건 언어별로 특화된 스트리밍 STT 모델이 PCM을 받는 즉시 부분 인식 결과를 내는 구조였습니다.
멀티스레드 WASM을 쓸 수 없는 플러그인 환경
단일 스레드는 앱 용량이나 구현 편의를 위해 제가 고른 선택이 아닙니다. Even Hub 플러그인 WebView에서는 멀티스레드 WebAssembly를 사용할 수 없어서 그렇게 빌드해야 했습니다.
WebAssembly 스레드에는 SharedArrayBuffer가 필요합니다. 여러 Worker가 같은 메모리를 쓰기 위해서입니다. 브라우저에서 이를 사용하려면 최상위 문서가 COOP·COEP 헤더를 갖춘 교차 출처 격리 상태여야 합니다. 그런데 .ehpk를 띄우는 WebView 응답과 격리 설정은 Even Hub 호스트가 관리하고, 플러그인이 직접 헤더를 설정할 수 없습니다.
그래서 G2gram의 whisper.cpp 런타임은 Emscripten의 pthread와 OpenMP를 끄고 단일 스레드로 빌드했습니다.
- Release 빌드와 WASM SIMD 사용
GGML_OPENMP=OFF-pthread와SharedArrayBuffer미사용- greedy best-of-one과 한 번의 추론만 사용
- 녹음 길이에 맞춰 오디오 컨텍스트를 512~1500 토큰 범위로 조정
Web Worker 덕분에 추론 작업은 휴대폰 WebView의 UI 스레드 밖으로 옮겼습니다. 다만 병렬화까지 되는 건 아닙니다. Worker 하나 안에서 Whisper 추론이 동기적으로 실행되므로 여러 CPU 코어에 연산이 나뉘지 않고, 녹음 후 대기 시간과 기기별 속도 편차를 애플리케이션 최적화만으로 줄이기 어렵습니다.
외부 STT 비용과 API 키는 사라졌습니다. 그 대신 모델 무게를 앱이 떠안았습니다. 현재 .ehpk는 약 54.7MB이고 대부분이 모델이며, 앱을 열 때 메모리로 읽고 초기화하는 비용도 생겼습니다.
화이트리스트는 이해하지만 RSS는 CORS에서 막혔습니다
Even Hub 플러그인은 안경에서 독립 실행되는 네이티브 앱이 아닙니다. 실행 장소는 휴대폰입니다. Even Realities 앱이 띄운 Flutter WebView 안에서 플러그인의 HTML과 TypeScript가 돌아가고, 안경은 화면 컨테이너를 렌더링하며 입력 이벤트를 보내는 쪽에 가깝습니다.
G2gram에서는 CORS가 문제가 되지 않았습니다. 텔레그램 웹 데이터 센터를 app.json에 등록하고 WebSocket으로 직접 연결할 수 있었거든요. 답답함을 느낀 건 별도로 간단한 RSS 리더를 만들어 보려 했을 때였습니다.
Even Hub 네트워크에는 두 개의 문이 있습니다.
- 요청 대상의 전체 origin이
app.json네트워크 화이트리스트에 있어야 합니다. - 화이트리스트를 통과해도 상대 서버가 올바른 CORS 헤더를 보내야 응답을 읽을 수 있습니다.
URL 화이트리스트는 이해할 수 있습니다. 플러그인이 정체불명의 서버로 사용자 데이터를 보내지 못하게 막고, 심사 과정에서도 통신 목적지를 확인할 수 있으니까요. 문제는 일반 웹사이트와 RSS 피드 중에는 Access-Control-Allow-Origin을 보내지 않는 곳이 많다는 점입니다. 사용자가 읽고 싶은 피드 URL을 화이트리스트에 넣어도 CORS에서 응답 본문을 받지 못하니, 범용 웹 데이터 뷰어를 만들기 어렵습니다.
웹 페이지를 iframe으로 직접 열어 긁는 것도 답이 아니었습니다. 동일 출처 정책부터 막힙니다. 다른 origin의 문서는 플러그인 JavaScript가 DOM을 읽을 수 없고, 많은 사이트는 X-Frame-Options나 CSP로 삽입 자체를 막습니다. WebView 전체를 외부 페이지로 이동하면 G2gram 코드와 SDK 브리지 문맥을 잃습니다. 현재 공개 SDK에는 외부 페이지에 스크립트를 주입하고 결과만 돌려받는 API도 없습니다. fetch(..., { mode: \"no-cors\" }) 역시 응답이 불투명해져 본문을 읽을 수 없으니 RSS 파싱에는 쓸 수 없습니다.
현재 가장 확실한 방법은 CORS 헤더를 붙여 주는 자체 프록시나 RSS 변환 서버를 두고, 그 서버 origin 하나를 화이트리스트에 등록하는 것입니다. 하지만 단순한 RSS 리더 하나에도 별도 서버를 운영해야 한다는 건 아쉽습니다. URL 화이트리스트와 응답 크기 제한을 그대로 적용하면서, RSS나 공개 문서를 읽을 수 있는 호스트 측 프록시 요청 API를 SDK가 제공한다면 보안과 활용성을 함께 잡을 수 있을 것 같습니다.
언어별 스트리밍 STT 모델은 처음 실행할 때 받는 쪽을 보고 있습니다
현재 G2gram은 Whisper Base q5_1 모델을 .ehpk 안에 함께 넣습니다. 구현할 때는 Even Hub 브리지의 setLocalStorage(key, value)와 getLocalStorage(key)만 보고, WebView에서 대형 바이너리를 안정적으로 저장할 다른 방법이 있는지 몰랐기 때문입니다.
공식 FAQ를 더 찾아보니 IndexedDB와 OPFS를 사용할 수 있었습니다. IndexedDB는 WebView 안에서 Blob이나 ArrayBuffer 같은 데이터를 저장할 수 있는 브라우저 데이터베이스이고, OPFS는 플러그인 origin에 격리된 전용 파일 시스템입니다.
먼저 IndexedDB와 OPFS를 실제 기기에서 검증해 볼 생각입니다. 안정적으로 쓸 수 있다는 걸 확인한 뒤 모델 배포 방식을 바꾸는 방향을 고려하고 있습니다. 처음 실행할 때 사용 언어를 고르고 해당 언어에 특화된 스트리밍 STT 모델만 다운로드해 보관하는 방식입니다. 이후에는 G2 마이크의 PCM을 녹음 중부터 모델에 계속 넣어 부분 인식 결과를 받으면 됩니다.
이 구조가 안정적으로 동작하면 54.7MB인 현재 .ehpk를 크게 줄이고, 사용자에게 필요 없는 언어 모델까지 한꺼번에 배포하지 않아도 됩니다. 다음 개발에서는 Android와 iOS의 저장 용량, 앱 재실행·업데이트 뒤 모델 유지 여부, 체크섬 검증과 다운로드 복구부터 확인해 볼 생각입니다.
PNG 업로드도 없이 대시보드에서 2×2 도트로 그렸습니다
기능 구현과 별개로 앱 아이콘이 의외의 복병이었습니다. PNG를 프로젝트에 넣거나 대시보드에 업로드하는 방식이 아니었습니다. Even Hub 대시보드가 보여 주는 네모 격자 위에서 직접 도트를 찍어 아이콘을 만들어야 했습니다.
그런데 브러시가 1×1도 아니고 2×2였습니다. 한 번 누르면 네 칸이 같이 칠해지니 대각선과 곡선을 조금씩 다듬거나 한 칸만 수정할 수가 없습니다. 픽셀 아이콘을 직접 그리게 해 놓고 정작 정교하게 픽셀을 찍을 도구가 없는 셈이었어요.
그래서 2×2 블록을 하나씩 놓으면서 G2gram 아이콘의 실루엣을 억지로 맞췄습니다. 개발자가 앱 로직과 네트워크, 음성 모델을 다 만든 뒤 대시보드의 제한된 브러시로 아이콘까지 수작업해야 하는 흐름은 꽤 짜증났습니다.
최소한 1×1 브러시를 제공하거나, PNG·SVG를 올리면 격자 아이콘으로 변환해 주는 기능이 필요합니다. 작은 아이콘을 외부 도구에서 제대로 만든 뒤 가져올 수만 있어도 이 노가다는 거의 사라질 겁니다.
SDK 0.0.12에서는 사진이 안 나왔습니다
사진 미리보기를 추가하면서는 최신 npm SDK인 0.0.12가 발목을 잡았습니다. 0.0.12는 이미지 전송에 LZ4 압축 경로를 추가했고, ImageRawDataUpdate를 JSON으로 바꿀 때 compressMode: 2를 자동으로 넣습니다.
제가 같은 이미지 payload를 0.0.11과 0.0.12에서 직접 직렬화해 보니 차이는 정확히 이랬습니다.
0.0.11 → containerID, containerName, imageData
0.0.12 → containerID, containerName, imageData, compressMode: 2
문제는 현재 제가 테스트한 Even 앱과 G2 조합에서 이 0.0.12 경로로 보낸 사진이 정상적으로 표시되지 않았다는 점입니다. 앱 코드를 아무리 바꿔도 해결되지 않았고, npm SDK를 0.0.11로 고정해 compressMode가 없는 payload를 보내자 이미지가 표시됐습니다.
그래서 현재 package.json은 @evenrealities/even_hub_sdk를 범위 지정 없이 정확히 0.0.11에 고정했습니다. 반면 app.json의 min_sdk_version은 0.0.12로 남아 있습니다. 호스트 호환 조건은 유지하되, 앱 번들에서 이미지 직렬화를 담당하는 JavaScript SDK만 0.0.11을 쓰는 상태입니다.

사진과 텍스트를 같은 대화 흐름에 넣기 위해 SDK 0.0.11로 돌아가 이미지 컨테이너를 다시 구성했습니다.
최신 SDK가 무조건 정답은 아니었습니다. 특히 SDK와 Even 앱, 안경 펌웨어가 함께 맞물리는 기능은 패키지 버전 하나만 보고 판단하기 어렵습니다. 이미지 기능을 추가한 뒤에는 실제 하드웨어에서 버전별 payload까지 비교해야 했습니다.
화면 UI도 웹처럼 만들 수 있는 건 아닙니다
휴대폰 쪽 설정 화면은 HTML과 CSS로 만들 수 있지만, G2에 보이는 화면은 일반 웹 페이지가 아닙니다. 576×288 캔버스 위에 텍스트, 목록, 이미지 컨테이너를 절대 좌표로 배치합니다.
| 제약 | 실제 영향 |
|---|---|
| 페이지당 전체 컨테이너 최대 12개 | 헤더, 본문, 스크롤 표시, 이미지, 버튼을 미리 나눠 써야 함 |
| 이미지 컨테이너 최대 4개 | 텔레그램 사진을 한꺼번에 많이 보여 주기 어려움 |
| 이미지 하나 최대 288×144 | 원본 사진을 내려받아 G2 크기로 다시 디코딩·축소해야 함 |
| 한 페이지에 입력 캡처 컨테이너 하나 | 여러 버튼을 웹처럼 각각 클릭 대상으로 만들기 어려움 |
| 글꼴·크기·정렬 제어 없음 | 텍스트 폭을 픽셀로 재고 직접 줄바꿈해야 함 |
| 이미지 업데이트는 직렬 처리 | 사진을 동시에 보내면 안 되고 한 장씩 기다려야 함 |
G2gram은 메시지를 줄 단위로 미리 나누고 사진이 들어갈 높이만큼 빈 줄을 예약합니다. 애매한 스크롤 위치는 건너뜁니다. 다운로드는 이미지가 실제 화면에 들어왔을 때만 시작하고, 그 전까지 텍스트 컨테이너로 스피너를 돌립니다. 로딩이 끝나면 이미지 컨테이너를 다시 구성합니다.
웹 앱을 만든다는 설명은 맞습니다. 다만 안경 화면까지 웹 UI처럼 자유롭게 그릴 수 있다는 뜻은 아닙니다.
그래도 이 제약 안에서 가능한 건 꽤 많았습니다
아쉬운 점만 있는 건 아닙니다. TypeScript와 Vite로 시작할 수 있고, G2 마이크 PCM과 터치패드·R1 이벤트를 비교적 단순한 브리지 API로 받을 수 있다는 건 분명한 장점입니다. 안경 디스플레이와 입력을 독특한 웹 주변기기처럼 다루는 느낌도 재미있었습니다.
제가 0.1.6 기준으로 다시 확인한 결과는 이렇습니다.
| 항목 | 현재 상태 |
|---|---|
| G2gram 버전 | 0.1.6 |
| Telegram 연결 | 자체 프록시 없는 MTProto over WSS |
| 로그인 | QR, 전화번호·인증 코드, 선택적 2단계 인증 |
| 음성 인식 | Even Hub 제약으로 단일 스레드인 whisper.cpp WASM, Whisper Base q5_1 |
| npm SDK | 0.0.11 고정 |
.ehpk 크기 | 약 54.7MB |
| 자동 검증 | 테스트 파일 16개, 테스트 106개 통과 |
| 프로덕션 빌드 | TypeScript·Vite 빌드와 동적 코드 실행·WebSocket 화이트리스트 검사 통과 |
G2gram은 “Even G2에서 텔레그램의 모든 기능을 쓰겠다"는 프로젝트가 아닙니다. 안경을 쓴 채 최근 대화를 확인하고, 짧은 답장을 음성으로 보내는 흐름을 별도 키 발급 없이 최대한 단순하게 만드는 프로젝트입니다.
누구에게 맞을까
이런 사람에게는 잘 맞습니다.
- BotFather나
my.telegram.org설정 없이 G2에서 텔레그램을 쓰고 싶은 사람 - 메시지를 자체 프록시에 맡기고 싶지 않은 사람
- 외부 STT API 키와 사용량 과금 없이 짧은 음성 답장을 보내고 싶은 사람
- G2의 제한된 화면에 맞춘 가벼운 텔레그램 사용 흐름을 원하는 사람
반대로 이런 기대에는 아직 맞지 않습니다.
- 공식 텔레그램 앱의 모든 미디어와 기능이 필요한 경우
- 로컬 Whisper가 언제나 즉시 끝나야 하는 경우
- 큰 모델을 앱 설치 후 자유롭게 바꾸고 캐시하고 싶은 경우
- 최신 SDK만으로 모든 하드웨어 기능이 안정적으로 동작하길 기대하는 경우
정리하면
G2gram에서 제가 가장 중요하게 본 건 기능 수보다 첫 진입이었습니다. 사용자는 별도 텔레그램 API 키와 봇 토큰, STT 키를 준비하지 않고 로그인만 하면 쓸 수 있어야 했습니다. 그래서 MTProto에 직접 붙고 Whisper를 앱 안에 넣었습니다.
대신 Even Hub 플러그인 환경의 경계도 아주 선명하게 봤습니다. 화이트리스트에 등록한 공개 RSS조차 CORS 때문에 읽지 못하고 이를 대신할 호스트 요청 API가 없는 점, PCM만 제공하고 STT는 제공하지 않는 마이크 API, IndexedDB·OPFS의 불명확한 용량 정책, 멀티스레드 WASM을 사용할 수 없는 WebView 런타임, 작은 컨테이너 기반 UI, PNG 업로드 없이 2×2 브러시만 주는 대시보드 아이콘 편집기, 0.0.12 이미지 전송 문제까지 하나씩 만났습니다.
Even Hub SDK는 아이디어를 빠르게 G2 화면에 올리는 데는 꽤 매력적입니다. 이제 아쉬운 건 그다음 단계입니다. 짧은 음성을 위한 선택형 STT 브리지, 화이트리스트를 유지하면서 공개 RSS와 문서를 읽을 수 있는 제한된 호스트 요청 API, 대형 모델 저장 기준, 교차 출처 격리와 SharedArrayBuffer를 제공하는 멀티스레드 WASM 실행 환경, 안정적인 이미지 전송, 1×1 브러시나 파일 가져오기를 지원하는 아이콘 제작 도구가 보강되면 "재미있는 데모"에서 "매일 쓰는 플러그인"으로 넘어가는 길이 훨씬 짧아질 것 같습니다.
댓글