AI 기능에는 더 나은 프롬프트뿐만 아니라 제품의 엣지(Product Edges)가 필요합니다
요약
성공적인 AI 제품은 모델의 성능을 넘어 오프라인 처리, 크레딧 관리, 상태 제어와 같은 세밀한 제품적 디테일(Product Edges)이 뒷받침되어야 합니다. 단순한 API 호출을 넘어 사용자 경험을 완성하는 예외 처리와 상태 관리의 중요성을 강조합니다.
핵심 포인트
- AI 모델 자체보다 모델 주변의 제품적 디테일이 앱의 품질을 결정함
- 오프라인 상태, 크레딧 잔액, 중복 요청 방지 등 엣지 케이스 처리가 필수적임
- AI 응답에 크레딧 정보를 포함하여 UI 상태와 백엔드 상태를 동기화해야 함
- 연결 실패와 서버 에러를 구분하는 등 정교한 에러 매핑이 필요함
대부분의 AI 기능이 실패하는 이유는 모델이 나쁘기 때문이 아닙니다.
모델 주변의 모든 것이 마치 데모(demo)처럼 취급되기 때문에 실패하는 것입니다.
이번 주에 저는 Supabase Edge Functions를 통해 Claude를 사용하는 iOS 운동 앱을 다듬고 있었습니다. 모델 부분은 간단합니다. 훈련 컨텍스트(training context)를 보내고, 구조화된 운동이나 계획을 돌려받고, 이를 검증한 뒤 SwiftData에 기록하는 방식입니다.
덜 화려하지만 실제 제품처럼 느껴지게 만드는 작업은 다음과 같은 부분들이었습니다:
- 월간 AI 크레딧 잔액 (monthly AI credit balance)
- 오프라인 처리 (offline handling)
- 인증 토큰 저장 (auth token storage)
- 생성(generation)이 실행되는 동안의 비활성화 상태 (disabled states)
- “운동 추가”와 “이 운동 교체”에 대한 서로 다른 규칙
- 지루한 엣지 케이스(edge cases)에 대한 테스트
그곳에 대부분의 AI 앱 품질이 존재합니다.
하나의 버튼, 여러 가지 상태
UI에는 운동 편집기에 “AI로 채우기”라는 작은 어포던스(affordance)가 있습니다. 그 아래에서 버튼은 단순히 “엔드포인트 호출(call endpoint)”만 하는 것이 아닙니다. 제안이 현재 허용되는지 여부를 알아야 합니다:
var canSuggest: Bool {
guard !isLoadingAI,
NetworkMonitor.shared.isOnline,
...
그 작은 서술어(predicate)가 많은 제품적 작업을 수행하고 있습니다.
사용자가 오프라인 상태라면, 실패할 것이 뻔한 네트워크 요청을 탭하지 못하게 해야 합니다.
이미 생성이 실행 중이라면, 중복 지출을 방지해야 합니다.
크레딧이 0이라면, 기능이 사용 가능한 것처럼 속이지 말아야 합니다.
기존 운동을 교체하는 중이라면, 이전 운동이 이미 유용한 컨텍스트(context)이므로 사용자에게 이름을 직접 입력하도록 강요하지 말아야 합니다.
모델은 이 중 어떤 것에도 신경 쓰지 않습니다. 사용자는 신경을 씁니다.
크레딧은 응답의 일부여야 합니다
제안 응답에는 업데이트된 크레딧 수량이 포함됩니다:
struct SuggestedExercise: Decodable {
let name: String
let briefDescription: String
...
그다음 뷰 모델(view model)은 성공적인 채우기 직후에 로컬 상태를 업데이트합니다:
creditsRemaining = result.creditsRemaining
aiFilledFields = true
이렇게 하면 백엔드는 사용자가 크레딧을 사용했다는 것을 알고 있지만, UI는 다음 새로고침 전까지 오래된 허용량을 계속 보여주는 전형적인 AI 제품의 기이한 현상을 방지할 수 있습니다.
또한 테스트하기가 더 쉽습니다. 앱 테스트에서 크레딧 전환은 명시적입니다:
func testAIDisabledWhenCreditsReachZero() {
let vm = ExerciseEditViewModel(mode: .add(makeDay()), context: container.mainContext)
vm.name = "Row"
...
운동 편집 뷰 모델(exercise edit view model)에 대해서만 13개의 테스트가 있으며, 오프라인 에러 매핑(offline error mapping)을 위한 별도의 커버리지도 존재합니다. 이것이 학술적으로 흥미로워서가 아니라, 실제 사용자 앞에서 문제가 발생하는 부분들이 바로 이런 것들이기 때문입니다.
오프라인은 서버 에러가 아닙니다
또 다른 작은 디테일: 연결 실패(connectivity failures)는 백엔드 실패(backend failures)와 별도로 매핑됩니다.
static let connectivityCodes: Set<URLError.Code> = [
.notConnectedToInternet,
.networkConnectionLost,
...
그 결과로 나오는 메시지는 의도적으로 단순합니다:
"오프라인 상태입니다. AI 기능을 사용하려면 다시 연결하세요."
"예기치 않은 서버 응답" 같은 말은 없습니다. 가짜 지능(fake intelligence)도 없습니다. 그저 사용자에게 무슨 일이 일어났는지 알려줄 뿐입니다.
실제 교훈
AI 기능을 출시하는 것은 대부분 중간에 확률적 의존성(probabilistic dependency)이 포함된 일반적인 소프트웨어 엔지니어링입니다.
모델 호출(model call)도 중요하지만, 이를 둘러싼 계약(contract)이 더 중요합니다:
- 사용자가 지금 바로 호출할 수 있는가?
- 네트워크가 끊기면 어떻게 되는가?
- 사용량이 일관되게 집계되는가?
- UI가 서버 상태를 즉시 반영하는가?
- 모델을 호출하지 않고도 엣지 케이스(edge cases)를 테스트할 수 있는가?
이러한 요소들이 갖춰지면, AI 기능은 버튼에 연결된 프롬프트처럼 느껴지는 것을 멈추고 앱의 일부처럼 느껴지기 시작합니다.
이것이 제가 계속해서 되새기는 기준입니다. "모델이 답변을 하는가?"가 아니라 "이것이 일반적인 제품의 현실(product reality)에서 살아남을 수 있는가?"입니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기