FoundationModels: 기기 내 LLM (iOS 26)은(는) 무엇을 하나요?
FoundationModels 프레임워크를 사용하여 Apple의 기기 내 언어 모델을 앱에 통합하는 패턴입니다. 텍스트 생성, @Generable을 통한 구조화된 출력, 사용자 정의 도구 호출, 스냅샷 스트리밍을 다룹니다. 모든 처리는 기기 내에서 실행되어 프라이버시와 오프라인 지원을 보장합니다.
활성화 시기
- Apple Intelligence를 사용하여 AI 기반 기능을 구축할 때
- 클라우드 의존 없이 텍스트를 생성하거나 요약할 때
- 자연어 입력에서 구조화된 데이터를 추출할 때
- 도메인별 AI 작업을 위한 사용자 정의 도구 호출을 구현할 때
- 실시간 UI 업데이트를 위해 구조화된 응답을 스트리밍할 때
- 프라이버시를 보호하는 AI가 필요할 때 (데이터가 기기를 벗어나지 않음)
핵심 패턴 — 가용성 확인
세션을 생성하기 전에 항상 모델 가용성을 확인하세요:
struct GenerativeView: View {
private var model = SystemLanguageModel.default
var body: some View {
switch model.availability {
case .available:
ContentView()
case .unavailable(.deviceNotEligible):
Text("이 기기는 Apple Intelligence를 지원하지 않습니다")
case .unavailable(.appleIntelligenceNotEnabled):
Text("설정에서 Apple Intelligence를 활성화해 주세요")
case .unavailable(.modelNotReady):
Text("모델을 다운로드 중이거나 준비되지 않았습니다")
case .unavailable(let other):
Text("모델을 사용할 수 없음: \(other)")
}
}
}
핵심 패턴 — 기본 세션
// 단일 턴: 매번 새 세션 생성
let session = LanguageModelSession()
let response = try await session.respond(to: "파리를 방문하기 좋은 달은 언제인가요?")
print(response.content)
// 다중 턴: 대화 맥락을 위해 세션 재사용
let session = LanguageModelSession(instructions: """
당신은 요리 도우미입니다.
재료를 바탕으로 레시피를 제안해 주세요.
제안은 간결하고 실용적으로 유지하세요.
""")
let first = try await session.respond(to: "닭고기와 쌀이 있어요")
let followUp = try await session.respond(to: "채식 옵션은 어떤가요?")
instructions의 핵심 포인트:
- 모델의 역할 정의 ("당신은 멘토입니다")
- 수행할 작업 지정 ("캘린더 이벤트 추출 도움")
- 스타일 선호도 설정 ("가능한 한 간결하게 응답")
- 안전 조치 추가 ("위험한 요청에는 '도와드릴 수 없습니다'라고 응답")
핵심 패턴 — @Generable을 통한 안내 생성
원시 문자열 대신 구조화된 Swift 타입을 생성합니다:
1. Generable 타입 정의
@Generable(description: "고양이의 기본 프로필 정보")
struct CatProfile {
var name: String
@Guide(description: "고양이의 나이", .range(0...20))
var age: Int
@Guide(description: "고양이 성격에 대한 한 문장 프로필")
var profile: String
}
2. 구조화된 출력 요청
let response = try await session.respond(
to: "귀여운 구조 고양이를 생성해 주세요",
generating: CatProfile.self
)
// 구조화된 필드에 직접 접근
print("이름: \(response.content.name)")
print("나이: \(response.content.age)")
print("프로필: \(response.content.profile)")
지원되는 @Guide 제약 조건
.range(0...20)— 숫자 범위.count(3)— 배열 요소 개수description:— 생성을 위한 의미적 안내
핵심 패턴 — 도구 호출
모델이 도메인별 작업을 위해 사용자 정의 코드를 호출하도록 합니다:
1. 도구 정의
struct RecipeSearchTool: Tool {
let name = "recipe_search"
let description = "주어진 용어와 일치하는 레시피를 검색하고 결과 목록을 반환합니다."
@Generable
struct Arguments {
var searchTerm: String
var numberOfResults: Int
}
func call(arguments: Arguments) async throws -> ToolOutput {
let recipes = await searchRecipes(
term: arguments.searchTerm,
limit: arguments.numberOfResults
)
return .string(recipes.map { "- \($0.name): \($0.description)" }.joined(separator: "\n"))
}
}
2. 도구와 함께 세션 생성
let session = LanguageModelSession(tools: [RecipeSearchTool()])
let response = try await session.respond(to: "파스타 레시피를 찾아줘")
3. 도구 오류 처리
do {
let answer = try await session.respond(to: "토마토 수프 레시피를 찾아줘")
} catch let error as LanguageModelSession.ToolCallError {
print(error.tool.name)
if case .databaseIsEmpty = error.underlyingError as? RecipeSearchToolError {
// 특정 도구 오류 처리
}
}
핵심 패턴 — 스냅샷 스트리밍
PartiallyGenerated 타입을 사용하여 실시간 UI를 위해 구조화된 응답을 스트리밍합니다:
@Generable
struct TripIdeas {
@Guide(description: "다가오는 여행을 위한 아이디어")
var ideas: [String]
}
let stream = session.streamResponse(
to: "흥미로운 여행 아이디어는 무엇이 있나요?",
generating: TripIdeas.self
)
for try await partial in stream {
// partial: TripIdeas.PartiallyGenerated (모든 속성은 Optional)
print(partial)
}
SwiftUI 통합
@State private var partialResult: TripIdeas.PartiallyGenerated?
@State private var errorMessage: String?
var body: some View {
List {
ForEach(partialResult?.ideas ?? [], id: \.self) { idea in
Text(idea)
}
}
.overlay {
if let errorMessage { Text(errorMessage).foregroundStyle(.red) }
}
.task {
do {
let stream = session.streamResponse(to: prompt, generating: TripIdeas.self)
for try await partial in stream {
partialResult = partial
}
} catch {
errorMessage = error.localizedDescription
}
}
}
주요 설계 결정
| 결정 | 근거 |
|---|---|
| 기기 내 실행 | 프라이버시 — 데이터가 기기를 벗어나지 않음; 오프라인 작동 |
| 4,096 토큰 제한 | 기기 내 모델 제약; 대용량 데이터는 세션 간에 분할 처리 |
| 스냅샷 스트리밍 (델타 아님) | 구조화된 출력에 친화적; 각 스냅샷은 완전한 부분 상태 |
@Generable 매크로 | 구조화된 생성을 위한 컴파일 타임 안전성; PartiallyGenerated 타입 자동 생성 |
| 세션당 단일 요청 | isResponding이 동시 요청을 방지; 필요한 경우 여러 세션 생성 |
response.content (.output 아님) | 올바른 API — 항상 .content 속성을 통해 결과에 접근 |
모범 사례
- 세션을 생성하기 전에 항상
model.availability를 확인 — 모든 사용 불가 사례 처리 instructions를 사용하여 모델 동작 안내 — 프롬프트보다 우선 순위가 높음- 새 요청을 보내기 전에
isResponding을 확인 — 세션은 한 번에 하나의 요청 처리 - 결과에 접근할 때는
response.content사용 —.output사용 금지 - 큰 입력은 여러 청크로 분할 — 4,096 토큰 제한은 instructions + prompt + output 합계에 적용
@Generable을 사용하여 구조화된 출력 생성 — 원시 문자열 파싱보다 강력한 보장GenerationOptions(temperature:)를 사용하여 창의성 조정 (값이 높을수록 더 창의적)- Instruments로 모니터링 — Xcode Instruments를 사용하여 요청 성능 프로파일링
피해야 할 안티 패턴
model.availability확인 없이 세션 생성- 4,096 토큰 컨텍스트 창을 초과하는 입력 전송
- 단일 세션에서 동시 요청 시도
- 응답 데이터에 접근할 때
.content대신.output사용 @Generable구조화된 출력으로 가능한 경우 원시 문자열 응답 파싱- 단일 프롬프트에 복잡한 다단계 로직 구축 — 여러 집중 프롬프트로 분할
- 모델이 항상 사용 가능하다고 가정 — 기기 호환성 및 설정은 다양함
사용 시기
- 프라이버시에 민감한 앱을 위한 기기 내 텍스트 생성
- 사용자 입력(양식, 자연어 명령)에서 구조화된 데이터 추출
- 오프라인에서도 작동해야 하는 AI 지원 기능
- 생성된 콘텐츠를 점진적으로 표시하는 스트리밍 UI
- 도구 호출을 통한 도메인별 AI 작업 (검색, 계산, 조회)