iOS용 AI 키보드 구축: 키보드 확장 기능에 대해 알려주지 않는 것들
요약
iOS 시스템 키보드 확장 기능을 활용하여 AI 기반의 스마트 답장(SmartReply)을 구현하는 방법을 설명합니다. 이 솔루션은 클립보드 또는 스크린샷 OCR로 메시지를 읽어 DeepSeek API를 통해 5개의 답장 후보를 단 한 번의 탭으로 제공하는 것을 목표로 합니다. 핵심은 iOS의 프로세스 격리 및 확장 기능 제약 조건을 이해하고, 메인 앱-확장 프로그램-백엔드 세 개의 컴포넌트로 아키텍처를 설계한 것입니다.
핵심 포인트
- iOS 키보드는 별도 샌드박스에서 실행되는 '2등급 시민'임을 인지해야 합니다.
- 메시지를 복사/붙여넣기 과정 없이 단일 탭으로 답장 후보를 제공하는 것이 핵심 개선점입니다.
- App Group과 Darwin notifications를 사용하여 메인 앱과 확장 프로그램 간의 통신을 구현합니다.
- 백엔드에서는 DeepSeek API 호출 및 서버 측 OCR 처리를 담당하여 기능을 완성합니다.
iOS용 AI 키보드 구축: 키보드 확장 기능에 대해 알려주지 않는 것들
요약(TL;DR): 저는 SmartReply를 만들었습니다. 이는 메시지를 읽어들이는(클립보드 또는 스크린샷 OCR) iOS 시스템 키보드로, 이 내용을 AI 백엔드(DeepSeek)로 전송하고 채팅 앱에 5개의 답장 후보를 바로 떨어뜨려 줍니다. 가장 어려운 부분은 AI 호출 자체가 아닙니다. 그것들은 iOS 샌드박스, 프로세스 격리(process isolation), 그리고 확장 기능 내의 StoreKit 2입니다. 제가 배운 점을 공유합니다.
왜 또 다른 AI 키보드가 필요한가?
AI 키보드는 새로운 것이 아닙니다. SwiftKey는 수년간 예측 기능을 제공해 왔고, Gboard는 일부 시장에서 AI 답장을 지원합니다. 하지만 제가 시도했던 모든 제품에는 같은 격차가 있었습니다:
메시지를 붙여넣기 → 키보드로 전환 → "AI"를 탭하기 → 결과를 채팅에 다시 복사하기.
이것은 네 번의 탭과 두 번의 컨텍스트 스위칭을 필요로 합니다. 제 아이디어는 단 한 번의 탭으로 해결하는 것이었습니다. 스크린샷에서 말이죠. 따라서 WhatsApp에서 "야, 오늘 밤 약속 아직이야?"라는 메시지를 받는 상황이라면, 아예 복사할 필요조차 없습니다. iPhone 뒷면을 더블 탭하면 키보드가 활성화되고, 다섯 개의 답장 후보가 이미 그곳에 놓여 있습니다.
실제 아키텍처는 다음과 같습니다.
키보드 확장 기능이 실제로 무엇인지
코드를 보여주기 전에 한 가지 명확히 하겠습니다: iOS 키보드 확장은 2등급 시민(second-class citizens)입니다. Apple은 보안을 위해 그렇게 설계했으며, 모든 제한 사항은 개발자가 우회해야 하는 제품 제약 조건입니다.
| 제약 조건 | 의미하는 바 |
|---|---|
| 별도 프로세스 | 키보드는 자체 샌드박스에서 실행됩니다. 호스트 앱의 메모리, 파일 접근, API 접근 권한이 전혀 없습니다. 심지어 UserDefaults조차 메인 앱과 확장 기능 사이에서는 작동하지 않습니다. |
| ... |
SmartReply에 들어간 모든 설계 결정은 이 표에서 비롯되었습니다.
아키텍처: 세 개의 프로세스, 하나의 목표
세 개의 움직이는 조각들이 작업을 공유합니다. 메인 앱이 사용자 인터페이스 흐름(user-facing flow), 구독 관리, 그리고 무거운 계산(OCR, Shortcut intent)을 담당합니다. 키보드는 UI와 삽입 기능을 담당합니다. 백엔드(Cloudflare Workers, 908줄 분량)는 AI 호출을 담당합니다.
통신(Communication): 메인 앱 ⇄ 확장 프로그램 간 통신은 App Group (group.com.smartkey.ime)과 Darwin notifications를 사용합니다. (단방향 "ping, check the store" 신호만 전달하며 데이터는 전송하지 않습니다).
백엔드 엔드포인트(Backend endpoints):
-
POST /ai/reply: 메시지 텍스트를 받아 DeepSeek API를 호출하고, 5개의 후보(candidates)를 반환합니다. -
POST /ocr: base64 이미지 데이터를 받아 서버 측에서 OCR을 실행합니다 (온디바이스 OCR이 없는 사용자를 위한 폴백). -
POST /verify: 구독 검증(receipt → App Store → 캐시된 권한(cached entitlement)). -
GET /config: 원격 플래그(remote flags)를 가져옵니다 (일일 무료 제한, 기능 토글, 공지사항 등).
Darwin Notification Helper (~30 lines)
단방향 통신입니다. 메인 앱이 키보드의 문을 두드립니다. 그러면 키보드가 App Group 스토어를 읽습니다:
import Foundation
import CoreFoundation
...
App Group Store
UserDefaults(suiteName:)를 감싼 얇은 래퍼입니다. 이는 앱과 확장 프로그램 간 Apple이 허용하는 유일한 IPC(Inter-Process Communication) 방식입니다:
public final class AppGroupStore {
private let defaults = UserDefaults(suiteName: "group.com.smartkey.ime")!
private let pendingScanKey = "pending.scan.v1"
...
프로젝트를 거의 망하게 만들었던 세 가지 핵심 전환점 (The Three Pivots That Almost Killed the Project)
전환점 1: 확장 프로그램에서는 StoreKit을 사용할 수 없습니다 (그리고 괜찮습니다)
저는 KeyboardViewController에 StoreKit을 가져오려고 이틀을 허비했습니다. 컴파일은 됩니다. 실행도 합니다. 하지만 Transaction.currentEntitlements는 아무것도 반환하지 않습니다—절대요. Apple이 확장 프로그램에서 이를 조용히 차단합니다.
해결책은 어리석을 정도로 간단합니다: 메인 앱이 구독 처리를 담당하고, 그 결과를 App Group을 통해 전달하는 것입니다.
-
메인 앱은 실행 시
SKSubscriptionManager.shared.start()를 호출하여 권한(entitlements)을 가져온 다음, 공유 스토어에SubscriptionEntitlement구조체를 작성합니다. -
키보드 확장 프로그램은 매번
viewDidAppear에서 이를 읽습니다:
// KeyboardViewController.swift
func refreshEntitlement() {
guard let entitle = AppGroupStore.shared.loadEntitlements() else {
...
확장 프로그램 내에서는 어떤 경우에도 StoreKit을 직접 호출하지 않습니다.
Pivot 2: 키보드 + 백 탭 + 단축어 = 생애주기 지뢰밭
원하는 사용자 흐름:
- 사용자가 WhatsApp에서 메시지를 보고 있다.
- 사용자가 iPhone 뒷면을 두 번 탭한다 (백 탭 → 단축어).
- 단축어가 스크린샷을 찍고 → 앱 인텐트(App Intent)를 실행하며 → 메인 앱이 OCR + AI를 수행한다.
- 키보드 확장 기능이 채팅 앱 위에 AI 후보군을 보여준다
문제는 4번이었다. 시스템은 텍스트 필드가 포커스될 때만 키보드 확장을 생성한다. 만약 사용자가 WhatsApp을 탐색하는 동안 (입력하지 않고) 두 번 탭하면, 확장 기능 자체가 메모리에 존재하지 않는다. 프로세스가 없다. 이야기할 상대가 없다.
세 가지 해결책:
| 접근 방식 | 실현 가능성 | 사용자 경험(UX) |
|---|---|---|
| 호스트 앱 위에 창 오버레이(Overlay a window on the host app) | ❌ iOS에서 허용하지 않음 | — |
| ... |
나는 옵션 3을 선택했다. 키보드가 활성화되어 있지 않으면, AI 결과는 그냥 기다린다. 사용자가 다음에 텍스트 필드를 탭하면 → viewDidAppear가 발생하며 → loadPendingScanIfAny()가 이를 읽고 → 후보군이 상단에 나타난다. 대부분의 경우(95%) 사용자는 두 번 탭할 때 타이핑하고 있으므로, 확장 기능은 이미 살아있다.
Pivot 3: '전체 접근 허용'은 코드 문제가 아닌 사용자 신뢰 문제다
Apple의 키보드 권한 프롬프트:
"SmartReply가 전체 접근을 허용하도록 요청합니다"
대부분의 사용자는 이것이 무엇을 의미하는지 모른다. 많은 사람이 이를 불신한다. 그리고 이것 없이는, 키보드 확장 기능의 URLSession이 조용히 실패한다 — 네트워크도 없고, AI 답장도 없고, 아무것도 작동하지 않는다.
세 가지 계층으로 해결했다:
- 앱 내 온보딩: 전체 접근(Full Access)이 필요한 이유를 명시적으로 보여준다 (
작동하는 모습
15초 분량의 제품 영상 (1080×1920 / 30fps / 1.3 MB):
| 프레임 | 시간 | 액션 |
|---|---|---|
| Hook | 0–1.5 | WhatsApp 채팅창 + 수신 메시지, 어두운 흐림 효과(dark blur vignette), 자막 "어떻게 답장해야 할지 모를 때" |
| ... | ||
| 🎬 YouTube 전체 영상: |
다른 1인 개발자를 위한 출시 노트
- 키보드 확장 기능(Keyboard extensions)은 코드베이스의 10%를 차지하지만 디버깅 시간의 50%를 차지합니다. 기능을 중심으로 설계하기보다, 한계점(limitations)을 염두에 두고 설계를 시작하세요.
- **App Group + Darwin + 메인 앱 오케스트레이션(orchestration)**이 표준 패턴입니다. 제가 본 AI 키보드 세 개 모두 이 방식을 사용했습니다. 너무 영리해지려고 애쓰지 마세요.
- 시뮬레이터가 아닌 실제 기기에서 구독 흐름을 테스트하세요. StoreKit 샌드박스는 프로덕션 환경과 다르게 작동하며, 확장 기능별 버그(예: Full Access 없이
URLSession사용)는 실제 하드웨어에서만 나타납니다. - Apple의 심사팀은 검토 노트(review notes)를 읽을 것입니다. "개인정보 보호 정책 참조"라고 쓰지 마세요. 그곳에 전체 설명을 한 번, 명확하게 작성하세요.
직접 시도해보고 싶다면
SmartReply는 시스템 키보드로 작동하는 iOS 앱(iPhone + iPad, iOS 16 이상)입니다. 현재 App Store 심사 중입니다. 다음을 할 수 있습니다:
-
TestFlight 참여 — 이메일을 남겨주시면 제가 초대해 드리겠습니다.
-
랜딩 페이지 읽기 → https://smartkey-api.pages.dev
-
데모 영상 시청 →
-
출시 팔로우 — 2026년 10월 8일 (태평양 시간 00:01) Product Hunt에 게시될 예정입니다.
읽어주셔서 감사합니다. 만약 비슷한 것을 개발하고 계신다면 연락 주세요. 키보드 확장 기능 생태계는 작고, 문제는 현실적이며, 저는 경험담을 교환할 준비가 되어 있습니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기