@Dependency를 @Sendable 클로저 안에서 쓰면 안 되는 이유 — TCA Dependency 올바른 패턴

문제: @Sendable 클로저 캡처 경고

TCA Reducer에서 @Dependency를 선언하고 .run { send in ... } 내부에서 self로 접근하면 Swift Concurrency 경고가 발생합니다.

// ❌ 잘못된 패턴
struct HomeFeature: Reducer {
    @Dependency(\.childRepository) var childRepo

    func reduce(into state: inout State, action: Action) -> Effect<Action> {
        case .loadChildren:
            return .run { send in
                // ⚠️ Capture of 'self' with non-sendable type in @Sendable closure
                let children = try await self.childRepo.fetchAll()
                await send(.childrenLoaded(children))
            }
    }
}

문제 원인: @Dependency 프로퍼티가 Sendable을 보장하지 않는 구조체에 속해 있는데, 이를 @Sendable 클로저인 .run { } 안에서 캡처하려 하기 때문입니다.

해결 1: 로컬 let으로 먼저 바인딩

// ✅ 클로저 외부에서 먼저 캡처
func reduce(into state: inout State, action: Action) -> Effect<Action> {
    case .loadChildren:
        let repo = childRepo  // ← 로컬 변수로 먼저 꺼내기
        return .run { send in
            let children = try await repo.fetchAll()
            await send(.childrenLoaded(children))
        }
}

해결 2: Repository 자체를 Sendable로 설계

// ✅ 가장 근본적인 해결 — Repository를 Sendable struct로
struct ChildRepository: Sendable {
    var fetchAll: @Sendable () async throws -> [Child]
    var create: @Sendable (ChildCreate) async throws -> Child
    var delete: @Sendable (String) async throws -> Void
}

// Sendable이므로 캡처 리스트로 안전하게 전달
return .run { [childRepo] send in
    let children = try await childRepo.fetchAll()
    await send(.childrenLoaded(children))
}

TCA 공식 권장 패턴: struct + @Sendable 클로저

// TCA 스타일 — 모든 Repository/Client를 이렇게 정의
struct ScheduleRepository: Sendable {
    var fetchToday: @Sendable () async throws -> [Schedule]
    var create: @Sendable (ScheduleCreate) async throws -> Schedule
    var update: @Sendable (String, ScheduleUpdate) async throws -> Schedule
    var delete: @Sendable (String) async throws -> Void

    static var live: Self {
        Self(
            fetchToday: { try await APIClient.shared.request("schedules/today") },
            create: { try await APIClient.shared.request("schedules/", method: "POST", body: ...) },
            update: { id, body in try await APIClient.shared.request("schedules/\(id)", method: "PUT", body: ...) },
            delete: { id in try await APIClient.shared.request("schedules/\(id)", method: "DELETE") }
        )
    }
}

교훈

TCA와 Swift Concurrency를 함께 쓸 때는 Sendable 준수 여부에 항상 주의해야 합니다. Repository/Service 타입을 struct + @Sendable 클로저 프로퍼티로 설계하면 TCA @Dependency로 사용할 때 캡처 문제가 근본적으로 해결됩니다.

Leave a Reply

Your email address will not be published. Required fields are marked *