'self' refers to the method '.self', which may be unexpected // self를 사용할 때 초기화 시점의 문제

임혜정·2024년 7월 11일
0
post-thumbnail
post-custom-banner

fix를 누르면 self -> ViewController.self로 고쳐주는 데 이는 해결법이 아니다.
에러는 사라지지만 의도한 동작을 하지않는다.

해결법은

버튼을 let이 아닌 lazy var로 선언하여 해당 프로퍼티에 처음 접근할 때 초기화를 해주던가, button.addTarget...라인을 viewDidLoad안에 작성하여 뷰가 완전히 로드된 후에 접근하도록 하는 것이다.

addTarget은 화면이 로드된 이후에 읽히도록 해야한다.

addTargetself 참조

addTarget을 뷰가 로드된 이후에 작성해야 하는 이유는 self 참조가 안전하게 초기화된 후에야 사용할 수 있기 때문이다. 클로저 내부에서 self를 참조해야 할 때 반드시 알고있어야할 개념이다.
UIViewUIButton과 같은 UI 요소들은 뷰 컨트롤러의 생명주기에서 뷰가 로드된 후에 안전하게 설정할 수 있다.

초기화 시점과 lazy var

lazy var로 선언된 프로퍼티는 해당 프로퍼티가 처음 접근될 때 초기화된다. lazy키워드로 인해 초기화가 제대로 된 이후에 접근하므로 self 참조가 강한 참조와 관련된 경고나 에러가 발생시키지 않는다.




문제가 발생한 코드를 더 자세히


import UIKit
import SnapKit

class ViewController: UIViewController {
    
    private let button: UIButton = {
        let button = UIButton()
        button.setTitle("저장", for: .normal)
        button.setTitleColor(.white, for: .normal)
        button.backgroundColor = .gray
        button.titleLabel?.font = .boldSystemFont(ofSize: 30)
        button.layer.cornerRadius = 10
        button.addTarget(self, action: #selector(buttonTapped), for: .touchDown)
        return button
    }()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        configureUI()
    }
    
    private func configureUI() {
        [button].forEach { view.addSubview($0) }
        
        view.backgroundColor = .white
        button.snp.makeConstraints {
            $0.top.equalTo(textView.snp.bottom).offset(50)
            $0.width.equalTo(60)
            $0.height.equalTo(40)
            $0.centerX.equalToSuperview()
        }
    }
    
    @objc private func buttonTapped() {
        UserDefaults.standard.set(textView.text, forKey: "memo")
        print("ok")
    }
}

여기서 "self"는 이 화면을 나타내는 ViewController 클래스의 인스턴스를 의미한다. 클로저나 함수내에서 self를 사용할 때 이를 캡쳐하여 해당 블록이 실행될 때 self에 접근한다.

문제는 버튼을 생성할 때, 버튼이 눌렸을 때 실행될 동작을 지정하는 코드에서 발생한다.

private let button: UIButton = {
    let button = UIButton()
    button.setTitle("저장", for: .normal)
    button.setTitleColor(.white, for: .normal)
    button.backgroundColor = .gray
    button.titleLabel?.font = .boldSystemFont(ofSize: 30)
    button.layer.cornerRadius = 10
    button.addTarget(self, action: #selector(buttonTapped), for: .touchDown)
    return button
}()

여기서 button.addTarget(self, action: #selector(buttonTapped), for: .touchDown) 부분을 보면, 버튼이 눌렸을 때 buttonTapped라는 함수를 호출하게 되어있다.
그런데 이 코드가 버튼을 초기화하는 동안 실행되기 때문에, 그 시점에 self (ViewController 인스턴스)가 완전히 초기화되지 않았을 수 있다.

그래서 안전하게 버튼의 동작을 설정하려면, 버튼을 초기화할 때가 아니라, 화면이 다 로드된 이후로 설정해야 하는 것이다.





추가 ) lazy var를 사용해야 하는 경우

: lazy var는 해당 프로퍼티가 처음 접근될 때 초기화되는 특징을 갖고있다.

  1. 초기화 시점에 값을 알 수 없을 경우, 필요할 때 초기화하는 것이 좋다

  2. 객체가 완전히 초기화된 후에 설정되어야 하는 경우, 예를 들어 지금처럼 self를 참조해야 하는 경우

  3. 비싼 연산이 필요한 경우:

    • 초기화가 비싼 연산을 요구하는 경우, 즉시 초기화하지 않고 실제로 필요할 때 초기화하는 것이 효율적일 수 있다.
  4. 객체 간의 강한 참조 순환을 피해야 하는 경우:

    • 클로저 내에서 self를 사용해야 하는 경우, self가 초기화된 후에 접근하므로 lazy var가 적합하다.
lazy var expensiveObject: SomeClass = {
    let instance = SomeClass()
    instance.configureExpensiveResources()
    return instance
}()

lazy var button: UIButton = {
    let button = UIButton()
    button.setTitle("저장", for: .normal)
    button.setTitleColor(.white, for: .normal)
    button.backgroundColor = .gray
    button.titleLabel?.font = .boldSystemFont(ofSize: 30)
    button.layer.cornerRadius = 10
    button.addTarget(self, action: #selector(buttonTapped), for: .touchDown)
    return button
}()

구분 기준

  1. 즉시 초기화 가능 여부:

    • 초기화 시점에 필요한 모든 정보를 사용할 수 있고, 값이 변하지 않는다면 let을 사용.
    • 초기화 시점에 필요한 정보를 사용할 수 없거나, 객체가 완전히 초기화된 후에 설정되어야 한다면 lazy var를 사용
  2. 초기화 비용:

    • 초기화 비용이 낮고 즉시 사용 가능하다면 let을 사용
    • 초기화 비용이 높거나, 초기화가 필요할 때만 수행되는 것이 좋다면 lazy var를 사용
  3. 클로저에서 self 사용 여부:

    • 초기화 클로저 내에서 self를 참조해야 한다면 lazy var를 사용

결론

  • let: 값이 변경되지 않고 초기화 시점에 모든 정보를 사용할 수 있을 때 사용
  • lazy var: 초기화 시점에 값을 설정할 수 없거나, 나중에 필요할 때 초기화되는 것이 더 효율적일 때 사용
profile
오늘 배운걸 까먹었을 미래의 나에게..⭐️
post-custom-banner

0개의 댓글