Skip to content
이도연 edited this page Jul 24, 2024 · 8 revisions

중첩 클로저 캡처 문제

❌ 잘못된 사용

reactor.state.map { $0.todos }
    .distinctUntilChanged()
    .withUnretained(self)
    .map { owner, todos in
        EditTodoConverter
            .convertToTodo(todos: todos) { todo in
                owner.reactor.action.onNext(.deleteTodo(todo)) // ❌
            }
    }
    .bind(to: baseView.collectionView.sectionBinder)
    .disposed(by: disposeBag)
  • closure 캡쳐는 상위 스코프 내에서 일어난다.
  • convertToTodo 후행 클로저의 상위 스코프는 map { } 이다.
  • map { }에서 .withUnretained(self)구문을 통해 self를 약한 참조로 변경하였지만 convertToTodo의 클로저가 self(owner)에 대해 캡쳐하면서 암시적으로 강한 참조로 변경된다.

✅ 올바른 사용

reactor.state.map { $0.todos }
    .distinctUntilChanged()
    .withUnretained(self)
    .map { owner, todos in
        EditTodoConverter
            .convertToTodo(todos: todos) { [weak owner] todo in
                owner?.reactor.action.onNext(.deleteTodo(todo))
            }
    }
    .bind(to: baseView.collectionView.sectionBinder)
    .disposed(by: disposeBag)
  • 중첩된 클로저 내부에서도 상위 스코프에서 캡쳐할 내용을 weak으로 명시해준다.

➕ 추가내용

reactor.state.map { $0.timeOptions }
    .withUnretained(self)
    .map { owner, options in
        EditNotificationConverter.convertToTimeOption(
            options: options,
            selectedOptions: owner.reactor.currentState
                .selectedTimeOptions,
            onSelect: { [weak owner] option in
                guard let owner = owner else { return false } // ✅
                
                let isOptionAlreadySelected = owner.reactor.currentState
                    .selectedTimeOptions
                    .contains(option)
                
                // ...
                
                return !isOptionAlreadySelected
            }
        )
    }
    .bind(to: baseView.timeOptionCollectionView.sectionBinder)
    .disposed(by: disposeBag)
  • 중첩된 클로저 내에서 weak을 사용한 후 guard let 구문을 사용하는 것은 가능하다.

❓ 의문점

baseView.addTodoButton.onTap
    .withUnretained(self)
    .subscribe(onNext: { owner, _ in
        UIView.animate(withDuration: 0.25) {
            owner.baseView.newTodoTextField.layer.opacity = 1.0
        }
        
        owner.baseView.newTodoTextField.textField.becomeFirstResponder()
    })
    .disposed(by: disposeBag)
  • UIView.animation 클로저의 경우, 상위 스코프의 owner를 재사용해도 캡처 문제가 발생하지 않았다. 왜 일까?
  • 단, self를 그대로 캡처하면 문제가 발생했다. ➡️ 상위 스코프에서 강한 참조로 전환되어 문제가 발생한 것.

  • 알았당! UIView.animate의 클로저 캡처가 실행되는 시점은 onTap이 실행되는 시점이다! onTap은 vc > view > button > onTap 구조로 존재하므로 vc가 해제되면 onTap도 해제되어 UIView.animate가 owner를 캡쳐할 일이 아예 없게된다. 즉, 클로저 내부에서 [weak owner]를 사용하지 않으면 owner를 강하게 캡쳐하는것이 맞지만, 이 경우엔 owner가 없다면 owner를 캡쳐할 일도 없게되는 것이다.

  • 이와 다르게 첫번째 경우에는 owner 캡쳐를 가지고 있는 곳이 vc > view > collection view > data source > item 이다. 즉, item에서 클로저를 '저장'하고 있기 때문에 [weak owner]를 사용하지 않으면 반드시 강한 캡처로 저장되게 된다.

  • 저장 되는 클로저가 상위 스코프이냐, 하위 스코프이냐에 따른 차이이다.

Clone this wiki locally