— “Call to main actor-isolated instance method…” 에러의 원인과 해결법
🎯 들어가며
Swift 6로 프로젝트를 올렸더니 갑자기 이런 에러가 떴다면 👇
Call to main actor-isolated instance method 'fetchDiaries(mode:)'
in a synchronous nonisolated context; this is an error in the Swift 6 language mode
Swift 5까지는 잘 되던 코드인데 왜 이제는 안 되는 걸까?
이건 단순한 “문법 변경”이 아니라, Swift의 동시성(Concurrency) 모델이 진짜 완성되었기 때문이야.
⚙️ 1️⃣ Swift Concurrency의 핵심 개념 정리
| 개념 | 설명 | 예시 |
| Actor | 데이터를 안전하게 보호하는 실행 단위 (스레드 대신 데이터 중심 동기화) |
@MainActor |
| Actor Isolation | 각 Actor는 자신의 데이터만 접근 가능, 다른 Actor 접근 시 명시 필요 |
await, Task { @MainActor in ... } |
| Sendable | 스레드 간 안전하게 전달 가능한 타입을 의미 | Int, String, struct 등은 Sendable ✅ / NSManagedObject는 ❌ |
💥 2️⃣ Swift 5에서는 왜 됐을까?
Swift 5에서는 async/await이 “실험적 기능” 이었어.
그래서 다음 같은 코드도 단순 경고로 끝났어 👇
@MainActor
func fetchDiaries() -> [Diary] { ... }
func fetchAsync() async {
DispatchQueue.global().async {
let result = self.fetchDiaries() // ⚠️ Swift 5에서는 경고
}
}
⚠️ “메인 액터 격리 위반일 수 있음”
이 정도로 끝났지, 컴파일은 됐어.
🔒 3️⃣ 그런데 Swift 6부터는?
Swift 6에서는 동시성 모델이 언어 차원에서 완성됨
(즉, 더 이상 실험 단계가 아님)
그래서 같은 코드가 이렇게 인식돼 👇
❌ “너 지금 메인 액터(@MainActor)에 속한 함수(fetchDiaries)를
백그라운드(DispatchQueue.global)에서 호출했네?
이건 데이터 레이스 위험이 있으니까 컴파일 자체를 막을게.”
🧠 4️⃣ Swift 5 → 6 변화 도식
| 구분 | Swift 5 | Swift 6 |
| Actor 격리 위반 | ⚠️ 경고만 표시 | 🚫 컴파일 에러 |
| Sendable 위반 | 일부만 체크 | 모든 async 경로에서 검사 |
| Core Data (NSManagedObjectContext) | Thread-safe 체크 X | Sendable 아님 → 오류 처리 필요 |
| 해결 필요 여부 | 무시 가능 | 필수 수정 |
🧩 5️⃣ 실제 사례 (Core Data 예시)
@MainActor
final class DiaryCoreDataManager {
func fetchDiaries(mode: FetchMode) -> [EmotionDiaryModel] { ... }
func fetchDiariesAsync(mode: FetchMode) async -> [EmotionDiaryModel] {
await withCheckedContinuation { continuation in
DispatchQueue.global(qos: .userInitiated).async {
// ❌ Swift 6에서는 여기서 에러 발생
let result = self.fetchDiaries(mode: mode)
continuation.resume(returning: result)
}
}
}
}
- fetchDiaries(mode:)는 @MainActor에 묶여 있음
- DispatchQueue.global()은 백그라운드 큐 (actor 격리 깨짐)
✅ 6️⃣ 해결 방법 3가지
| 방법 | 설명 | 사용 조건 | 안정성 |
| 🟩 Task { @MainActor in … } | 메인 액터 컨텍스트로 다시 실행 | viewContext 사용 시 | ✅✅✅ |
| 🟦 nonisolated func | 액터 격리에서 제외 (thread-safe 전제) | backgroundContext 사용 시 | ⚠️ 주의 |
| 🟨 backgroundContext.perform {} | Core Data 전용 백그라운드 작업 | 대량 처리, 이미지 포함 | ✅✅ |
🟩 방법 1 — Task로 메인 액터 지정 (가장 안전)
func fetchDiariesAsync(mode: FetchMode) async -> [EmotionDiaryModel] {
await withCheckedContinuation { continuation in
DispatchQueue.global(qos: .userInitiated).async {
Task { @MainActor in
let result = self.fetchDiaries(mode: mode)
continuation.resume(returning: result)
}
}
}
}
➡️ Core Data의 viewContext와 100% 호환
➡️ Swift 6의 actor 규칙도 준수
🟦 방법 2 — nonisolated로 격리 해제
@MainActor
final class DiaryCoreDataManager {
nonisolated func fetchDiaries(mode: FetchMode) -> [EmotionDiaryModel] { ... }
}
⚠️ 단, 내부에서 viewContext를 접근하면 여전히 위험함
→ 반드시 backgroundContext 사용해야 함
🟨 방법 3 — Core Data Background Context
func fetchDiariesInBackground(mode: FetchMode) async -> [EmotionDiaryModel] {
await withCheckedContinuation { continuation in
backgroundContext.perform {
let request = NSFetchRequest<EmotionDiaryEntity>(entityName: "EmotionDiaryEntity")
let entities = try? backgroundContext.fetch(request)
continuation.resume(returning: entities?.compactMap { $0.toModel() } ?? [])
}
}
}
🧭 7️⃣ Swift 6 Actor 격리 — 한눈에 정리 도식
┌────────────────────────────────────────────┐
│ Swift 5 (경고 수준) │
│────────────────────────────────────────────│
│ ✅ @MainActor 함수 │
│ ⚠️ 백그라운드 호출 허용 (주의만 표시) │
│ │
│ DispatchQueue.global() → 가능 │
└────────────────────────────────────────────┘
↓ ↓ ↓
┌────────────────────────────────────────────┐
│ Swift 6 (엄격한 규칙) │
│────────────────────────────────────────────│
│ 🚫 @MainActor 함수는 Actor 외부 호출 불가 │
│ ✅ 반드시 Task { @MainActor in … } 로 래핑 │
│ │
│ DispatchQueue.global() → ❌ 불가 │
└────────────────────────────────────────────┘
📘 결론
Swift 6부터는 동시성 규칙이 “선택”이 아니라 “의무”다.
- @MainActor는 반드시 메인 스레드에서만 호출해야 하고,
- 백그라운드 접근은 Task { @MainActor in … }로 안전하게 감싸야 한다.
✅ 즉, Thread-safe한 경계선이 코드로 명확히 보이는 시대가 온 거다.
'감정일기(가칭)' 카테고리의 다른 글
| 🍋 Core Data Fetch 고급 설계 — Predicate, Compound, FetchLimit 완전정복 (0) | 2025.10.20 |
|---|---|
| 🍋 Swift FileManager 설계 — private 유지하면서 안전하게 경로 노출하기 (0) | 2025.10.19 |
| 🧠 Swift Concurrency — “Capture of 'self' with non-sendable type” 완전 정리 (1) | 2025.10.19 |
| 💾 왜 Core Data의 Read는 반드시 async여야 할까? (0) | 2025.10.18 |
| 📚 DiaryCoreDataManager 내부 구조 완전 정리 (0) | 2025.10.18 |