
iOS 26에서 공개된 FoundationModels 프레임워크를 실무에 적용해보며 만난 문제들과 해결 방법에 대한 기록이다.
FoundationModels는 Apple이 iOS 26부터 제공하는 온디바이스 AI 프레임워크다. 기기의 Neural Engine 위에서 언어 모델을 직접 실행한다. 서버가 없다.
import FoundationModels
let session = LanguageModelSession(model: SystemLanguageModel.default)
let response = try await session.respond(to: "Hello").content
API 키도 없고 네트워크도 필요 없다. 이게 전부다.
일반 LLM API와 비교하면 이렇다.
| Foundation Models | 클라우드 LLM API | |
|---|---|---|
| 실행 위치 | 온디바이스 | 외부 서버 |
| 네트워크 | 불필요 | 필수 |
| 비용 | 없음 | 토큰당 과금 |
| 컨텍스트 한도 | 4,096 토큰 | 수만~수십만 토큰 |
| 응답 품질 | 단순 작업에 적합 | 복잡한 추론 가능 |
| 개인정보 | 기기 밖 미전송 | 서버 전송 |
| 지원 언어 | 영어 중심 | 다국어 |
단순한 분류, 추출, 선택 작업에는 충분히 쓸 만하다. 반면 4,096 토큰이라는 작은 컨텍스트 한도와 영어 제약은 실제로 꽤 걸림돌이 된다.
사용 조건도 있다. iPhone 15 Pro 이상, iPad M1 이상, iOS 26 이상, Apple Intelligence 활성화까지 맞아야 한다. 코드에서는 먼저 가용 여부를 확인해야 한다.
guard SystemLanguageModel.default.isAvailable else {
// 시뮬레이터 또는 미지원 기기
return
}
처음 만난 문제다. 한국어 입력을 그대로 넣으면 이 에러가 난다.
unsupportedLanguageOrLocale: "Unsupported language."
Foundation Models는 현재 영어 중심 모델이다. 처음엔 Instructions 객체 설정 문제인가 싶어서 이것저것 바꿔봤는데 무관했다. 직접 검증해보니 원인은 단순했다. 프롬프트나 컨텍스트에 한국어가 포함되는 것 자체가 문제였다.
해결책은 모든 입력을 영어로 유지하는 것이다.
한국어 입력
↓ [세션 1: 번역]
영어 텍스트
↓ [세션 2: 실제 작업]
결과
↓ 매핑
한국어로 표시
Foundation Models를 두 번 호출하는 구조가 됐다.
let session = LanguageModelSession(model: SystemLanguageModel.default)
// Instructions 없음 — 단순 번역이므로 불필요
let translated = try await session.respond(
to: "Output only the English translation, no preamble or explanation:\n\(userInput)"
).content
여기서 "no preamble or explanation" 지시가 중요하다. 없으면 모델이 이렇게 응답한다.
Here's the English translation:
"Fashion items for a 20-year-old woman"
앞에 붙은 메타 발화가 다음 세션에 그대로 전달되면서 결과가 이상해진다. 실제로 이것 때문에 엉뚱한 결과가 나왔었다.
번역 실패 시 원문을 그대로 넘기는 fallback도 뒀다.
do {
translated = try await session.respond(
to: "Output only the English translation, no preamble:\n\(input)"
).content
} catch {
translated = input // 실패 시 원문 사용
}
Instructions 객체로 역할을 정의하고 실제 작업을 수행한다.
let session = LanguageModelSession(
model: SystemLanguageModel.default,
instructions: Instructions("""
You are a recommendation AI.
Choose between 1 and 9 items from the list that best match the request.
Choose fewer items for specific requests, more for broad ones.
Use ONLY the exact item codes shown in brackets. Do NOT invent codes.
End your response with ITEM_CODES:[code1,code2,...].
""")
)
let response = try await session.respond(to: context).content
Instructions 객체는 영어 내용이면 에러를 유발하지 않는다. 역할 정의와 출력 형식 지시를 프롬프트와 분리해서 구조적으로 관리할 수 있다.
소형 온디바이스 모델은 자유 형식 응답에서 원하는 데이터를 추출하기가 어렵다. ITEM_CODES:[...] 같은 명확한 앵커를 요구하는 게 훨씬 안정적이다.
// 1순위: 명시적 형식 파싱
let pattern = #"ITEM_CODES\s*:\s*\[([^\]]+)\]"#
// 2순위 (폴백): 응답 전체에서 ID 패턴 추출
let fallback = #"\b([A-Z]\d{8,})\b"#
존재하지 않는 ID를 만들어내는 경우가 있다. compactMap으로 자연스럽게 필터링되지만 모니터링은 해두는 게 좋다.
let invalid = codes.filter { itemMap[$0] == nil }
if !invalid.isEmpty {
print("⚠️ hallucination: \(invalid)")
}
let result = Array(codes.compactMap { itemMap[$0] }.uniqued().prefix(9))
가장 실질적인 제약이다. 실제로 이런 에러를 만났다.
exceededContextWindowSize: Content contains 4,781 tokens,
which exceeds the maximum allowed context size of 4,096.
컨텍스트 예산을 분배하면 이렇게 된다.
4,096 토큰
├── Instructions ~200
├── 입력 프롬프트 ~150
├── 응답 예산 ~400
└── 실제 데이터 ~3,350
실제 데이터에 쓸 수 있는 공간이 약 3,350 토큰이다. 넘기지 않으려면 두 가지를 해야 한다.
1. 사전 필터링: 관련 있는 데이터만 넘긴다. 전체 데이터를 항상 다 넣으면 금방 한도를 초과한다.
2. 동적 트리밍: 생성 시 예산을 초과하면 자동으로 끊는다.
let charBudget = 3350 * 4 // 1토큰 ≈ 4자 (영어 기준)
var usedChars = 0
var lines: [String] = []
for item in items {
let line = formatItem(item)
if usedChars + line.count > charBudget { break }
lines.append(line)
usedChars += line.count + 1
}
한국어 텍스트는 영어 대비 토큰 소모가 2~3배 많다. 컨텍스트에 넣는 데이터는 반드시 영어로 준비해야 한다.
매 요청마다 새 세션을 생성해야 한다. 세션을 재사용하면 이전 컨텍스트가 누적되면서 예상과 다른 응답이 나온다.
// ❌ 세션 재사용 — 컨텍스트 누적
class Service {
let session = LanguageModelSession(...) // 한 번만 생성
}
// ✅ 매 요청마다 생성
func process() async {
let session = LanguageModelSession(...) // 요청마다 새로 생성
let response = try await session.respond(to: context).content
}
SwiftUI TextField로 입력 UX를 만들면서 두 가지 한계를 만났다. Foundation Models와 직접 관련은 없지만 같이 작업하면서 부딪힌 것들이라 기록해둔다.
엔터 키가 검색 대신 줄바꿈을 한다
TextField(axis: .vertical)에 .onSubmit을 달아도 내부적으로 multiline 처리가 끼어드는 경우가 있다. UITextField를 직접 래핑하면 textFieldShouldReturn에서 return false로 줄바꿈을 완전히 차단할 수 있다.
func textFieldShouldReturn(_ textField: UITextField) -> Bool {
onSubmit()
return false // 줄바꿈 삽입 방지
}
한국어 입력이 자모로 분리된다
거실 → ㄱㅓㅅㅣㄹ
shouldChangeCharactersIn 델리게이트는 IME 조합 중간 상태도 호출한다. editingChanged 이벤트로 교체하면 조합이 완료된 확정 텍스트만 받는다.
// ❌ shouldChangeCharactersIn — 조합 중 자모 분리됨
// ✅ editingChanged — 조합 완료 후 확정 텍스트
tf.addTarget(coordinator, action: #selector(Coordinator.textChanged(_:)), for: .editingChanged)
updateUIView에서 becomeFirstResponder / resignFirstResponder를 호출하면 텍스트 변경 때마다 재호출되면서 키보드가 내려가고 AttributeGraph cycle 경고가 쏟아진다. Focus 제어는 명시적인 사용자 액션에서만 해야 한다.
Foundation Models는 생각보다 쓸 만하다. 특히 네트워크 없이 동작하고 비용이 없다는 점은 특정 시나리오에서 강력한 장점이다.
실제 적용하면서 기억할 것들:
"주어진 데이터에서 조건에 맞는 항목을 고르는" 작업에 잘 맞는다. Apple이 다국어 지원과 컨텍스트 한도를 개선해 준다면 활용 범위가 훨씬 넓어질 것 같다.
iOS 26.0, FoundationModels 첫 번째 공개 API 기준