[iOS/UIKit] @objc와 #selector - 커스텀뷰에 addTarget을 하면 안되는 이유

UIKit에서 커스텀뷰를 만들어서 작업하는 도중에, 반복작업을 더 줄이기 위해 `addTarget()` 메서드도 커스텀뷰 안에 만들고 싶어졌다.

그런데 예상한것과 다르게 제대로 동작하지 않았고, 그 이유가 궁금해져서 찾아본 결과를 정리해보도록 하겠다.


UIKit의 커스텀뷰

UIKit에서 반복되는 뷰를 편리하게 관리하기 위해 커스텀뷰를 만들곤 한다.

예를 들어 다음과 같은 화면이 있다고 하면 각 버튼의 UI를 하나씩 전부 구성하는게 아니라, 하나의 커스텀뷰를 만들어서 일부분만 변경하여 사용할 수 있다.

 

UIButton을 상속하는 커스텀버튼 클래스를 만들고, 버튼을 생성할때 UIButton이 아닌, 방금 만든 커스텀버튼을 타입으로 지정해주면 된다.

// 커스텀버튼 클래스

class SortButton: UIButton {
    init(title: String, selector: Selector = #selector(defaultAction)) {
        super.init(frame: .zero)
        
        setTitle("\(title)", for: .normal)
        titleLabel?.textAlignment = .center
        setTitleColor(.white, for: .normal)
        setTitleColor(.black, for: .selected)
        setBackgroundColor(.black, for: .normal)
        setBackgroundColor(.white, for: .selected)
        clipsToBounds = true
        layer.borderColor = UIColor.white.cgColor
        layer.borderWidth = 1
        layer.cornerRadius = 10
    }
    
    override init(frame: CGRect) {
        super.init(frame: frame)
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
}



// 해당 커스텀뷰 사용
let accuracyButton = SortButton(title: "정확도")
let dateButton = SortButton(title: "날짜순")
let highPriceButton = SortButton(title: "가격높은순")
let lowPriceButton = SortButton(title: "가격낮은순")

이렇게 하면 위의 사진처럼 반복되는 뷰의 UI를 계속 그려줄 필요 없이 바뀌는 부분만 아규먼트로 넘겨주면, 반복되는 뷰를 쉽게 그릴 수 있다.

이렇게 커스텀뷰에는 보통 UI와 관련된 코드를 작성하는게 일반적인데, 문득 이런 궁금증이 들었다.

 

"버튼에 대한 액션도 미리 커스텀뷰에 구현해놓으면 더 간편하게 사용할 수 있지 않을까?"

 

그래서 다음과 같이 코드를 수정해봤다.

// 커스텀버튼 클래스

class SortButton: UIButton {
    init(title: String, selector: Selector = #selector(defaultAction)) {
        super.init(frame: .zero)
        
        setTitle("\(title)", for: .normal)
        titleLabel?.textAlignment = .center
        setTitleColor(.white, for: .normal)
        setTitleColor(.black, for: .selected)
        setBackgroundColor(.black, for: .normal)
        setBackgroundColor(.white, for: .selected)
        clipsToBounds = true
        layer.borderColor = UIColor.white.cgColor
        layer.borderWidth = 1
        layer.cornerRadius = 10
        
        // addTarget을 통해 버튼에 액션 지정
        addTarget(self, action: selector, for: .touchUpInside)
    }
    
    override init(frame: CGRect) {
        super.init(frame: frame)
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    // 테스트용 액션
    @objc
    func defaultAction() {
        print(self)
    }
}


// 해당 커스텀뷰 사용
let accuracyButton = SortButton(title: "정확도", selector: #selector(accuracyButtonTapped))

//...
@objc
func accuracyButtonTapped() {
    print(#function)
}

 

이렇게 해서 사용하려고 해보니 다음과 같은 런타임 오류가 발생하면서 앱이 죽었다.

 

 


커스텀뷰에 AddTarget을 사용하면 안되는 이유

왜 위와같은 문제가 발생했을까?

처음에 예상했을땐 "뭔가 인스턴스를 생성할 시점에 accuracyButtonTapped라는 메서드를 찾지 못해서 그런건가?" 라고 생각했었다.

그리고 이와 관련해서 찾아봤을 때의 결론은 다음과 같다.

 

AddTarget에서 `accuracyButtonTapped()`메서드를 호출해서 사용하는 방식은 `#selector`를 통해서이다.

`#selector`는 Objective-C 런타임 라이브러리에 해당한다. 쉽게 말해서 Objective-C 메서드 또는 프로퍼티를 호출하거나 엑세스할 때 사용되는 키워드인데, accuracyButtonTapped가 @objc메서드로 지정된 이유가 그 때문이다.

 

여기서 문제는, Objective-C 런타임은 이름에서도 알 수 있듯이 해당 메서드들은 런타임때 실행이 된다.

즉, @objc로 명시된accuracyButtonTapped 메서드는 런타임시점에 실행이 된다....!

 

그런데 지금 코드에서 보면

// 해당 커스텀뷰 사용
let accuracyButton = SortButton(title: "정확도", selector: #selector(accuracyButtonTapped))

//...
@objc
func accuracyButtonTapped() {
    print(#function)
}

이런식으로 accuracyButton라는 인스턴스를 생성할때 accuracyButtonTapped에 접근할래! 라는 모양새이다.

그러나, 해당 함수가 실행되는 시점은 인스턴스가 생성되는 시점보다 이후이기 때문에 위와같은 오류가 발생했던 것이다!!

 

그래서 이런식으로 커스텀뷰에 액션을 추가하거나, 완전히 동일한 상황은 아니더라도, @objc 메서드의 실행시점과 충돌할 수 있는 코드는 작성하지 않는게 좋겠다.

 

 

※참고자료

https://developer.apple.com/documentation/swift/using-objective-c-runtime-features-in-swift

https://developer.apple.com/documentation/objectivec/objective-c_runtime

https://ios-development.tistory.com/1381