많이 들 어려우셨죵 최대한 쉽게 설명해서 깊은 내용까지 한번 들어가보려고 합니다!
델리게이트 패턴(Delegate Pattern)을 알아보자
잠깐 그전에!!!!!!
1. Any와 AnyObject란?
- Any
- "모든 타입" 을 저장할 수 있는 타입
- 값 타입(Struct, Enum), 참조 타입(Class, Closure) 모두 저장 가능
- ex) Int, Double, String, Bool, Struct, Class, Closure 다 담을 수 있음
- AnyObject
- "모든 클래스 타입" 만 저장할 수 있는 타입
- 오직 클래스 인스턴스만 가능 (구조체, 열거형, 클로저 ❌)
2. Any & AnyObject의 타입 캐스팅
- switch-case 패턴 매칭 (as)
- as를 이용해 switch 문 안에서 타입별로 분기 처리 가능
for thing in things { switch thing { case _ as Int: print("Int Type!") case _ as String: print("String Type!") case _ as Human: print("Human Class Type!") case _ as () -> (): print("Closure Type!") default: print("Unknown") } } - 다운캐스팅 (as?, as!)
- as?: 타입 변환 시도, 실패하면 nil
- as!: 타입 변환 강제, 실패 시 런타임 에러
var name: Any = "Sodeul" if let str = name as? String { print(str.count) // 성공하면 사용 가능 }
🔥 [심화] Any, AnyObject가 "런타임 시점에 타입이 결정된다"
Swift는 컴파일 시점에 타입을 엄격히 체크하는 언어야.
하지만 Any나 AnyObject로 선언하면, 어떤 타입이 들어올지 컴파일러가 미리 알 수 없어.
즉, "컴파일할 때 타입을 정확히 모른다" → "런타임(앱 실행 중) 때 실제 타입을 확인하고 처리한다" 는 뜻
let value: Any = "Hello"
value.count // 컴파일 에러! (Any 타입은 count를 모름)
- 여기서 value는 컴파일할 때 그냥 Any야.
- 그런데 실제 앱이 실행될 때, String이란 걸 알게 돼.
- 그래서 as?나 as!로 런타임에 타입을 확인해서 사용해야 함
if let stringValue = value as? String {
print(stringValue.count) // 런타임에 String임을 확인하고 count 사용
}
즉, Any나 AnyObject를 쓸 때는 런타임 타입 확인이 필수야.
(안 하면 크래시가 터지거나, 타입 추론이 안 돼서 메서드 호출도 못함.)
즉 정리를 다시 하면
- Any = "진짜 모든 타입 가능"
- AnyObject = "오직 클래스 인스턴스만 가능"
- 둘 다 컴파일 시점에는 정확한 타입을 몰라서 런타임 타입 확인이 필요하다!
자 그러면 본론으로 돌아와서
자자 델리게이트 패턴을 쉽게 알아봅시다
🧩 1. 델리게이트 패턴(Delegate Pattern) 이란?
Delegate는 어떤 객체가 "자신의 일을 다른 객체에 위임"하는 디자인 패턴이야.
- A 객체가 어떤 행동을 하고 싶지만, "직접" 하지 않고 B 객체에게 "너 대신 해줘"라고 맡기는 구조
- 대신, B 객체가 어떤 메서드를 구현했는지는 보장해야 하니까 protocol(프로토콜)을 이용해서 "너는 이 함수를 꼭 구현해야 해"라고 규칙을 정해.
정리:
A는 "Delegate 프로토콜"만 알고 있고, B는 그 프로토콜을 준수하면서 대신 행동해주는 거야.
우리는 순서를 좀 기억할 필요가 있음
- 자격증 만들기 (보통 일을 위임을 시키는 당사자 쪽에서 발급 한다고 생각)
- 너가 대한민국 변호사라면 해당 자격증을 채택해야해
- 자격증 안에는 판례 파악하기, 소송 절차 이해하기, 민사소송, 형사소송관련 법률적 근거 알고있기 등등
- 자격증 채택하기 (나는 변호사야 자격증을 채택했고 너가 말한 기능을 다 할줄 알아 언제든지 요청해줘!)
// 프로토콜 : 자격증(이 자격증을 가진애들은 다 올 수 있음) - 의존성 주입이라는 개념
protocol DataBindDelegate : AnyObject {
func dataBind(id : String) // 위임할 일을 정의
}
어 나 대한민국 판사인데 너 변호사 자격증 있냐? 있으면 이것좀 해봐라~~
어 나 정정욱인데 너 요아정 좋아하냐? 토핑좀 추가해봐라?
🔥 2. AnyObject를 왜 protocol에 붙이는가?
protocol DataBindDelegate: AnyObject {
func dataBind(id: String)
}
➡️ 이유는 "weak" 참조를 위해서야!
Delegate는 주로 weak으로 선언해.
weak var delegate: DataBindDelegate?
이유는?
- delegate는 순환 참조(Retain Cycle)를 방지하기 위해 약한 참조(weak)로 선언하는 게 "관례"야.
- 그런데 Swift 규칙상 weak은 클래스 타입(Reference Type)한테만 걸 수 있어.
- (Struct나 Enum은 Value Type이라 weak 적용이 불가능)
따라서 "Delegate는 클래스만 할 수 있어야 한다" 라고 제한을 걸기 위해서
protocol 정의할 때 : AnyObject를 붙이는 거야.
✅ 즉, AnyObject를 붙이면, "이 프로토콜을 따르는 타입은 클래스만 가능하다"는 뜻
순환참조 관점에서 나의 위임자가 없을수 있음 + 만약 weak이 아니라면 서로가 서로를 가리키는 상황 발생
코드로 알아보자
발급자 관점 (대리자에게 위임하는 쪽)
// 프로토콜 : 자격증(이 자격증을 가진애들은 다 올 수 있음) 발급
protocol DataBindDelegate : AnyObject {
func dataBind(id : String) // 위임할 일을 정의
}
class WelcomeViewController_DelegatePattern: UIViewController {
// MARK: - Property
var id: String?
weak var delegate : DataBindDelegate? // 프로토콜 타입(자격증) 채택한 애들은 다 올수가 있어
// MARK: - UIComponent
let mainImage: UIImageView = {
let Image = UIImageView(frame: CGRect(x: 120, y: 60, width: 150, height: 150))
Image.image = ImageLiterals.login
return Image
}()
.
.
.
//MARK: Life Cycle
override func viewDidLoad() {
super.viewDidLoad()
self.view.backgroundColor = .white
addViews()
}
}
extension WelcomeViewController_DelegatePattern{
@objc
private func backToLoginButtonDidTap() {
if let id = id {
delegate?.dataBind(id: id) // delegate 야야 너 델리게이트 이것좀 해봐라~
}
if self.navigationController == nil {
self.dismiss(animated: true)
} else {
self.navigationController?.popViewController(animated: true)
}
}
}
대리자 관점 (자격증을 채택해서 관련 기능(업무)를 처리)
//
// LoginViewController_DelegatePattern.swift
// Week_02
//
// Created by 정정욱 on 4/12/25.
//
import UIKit
class LoginViewController_DelegatePattern: UIViewController {
// MARK: - UIComponent
let titleLabel: UILabel = {
let label = UILabel(frame: CGRect(x: 69, y: 161, width: 236, height: 44))
label.text = "동네라서 가능한 모든것\\n당근에서 가까운 이웃과 함께해요."
label.font = UIFont.Pretendard.subhead1()
label.textColor = .black
label.textAlignment = .center
label.numberOfLines = 2
return label
}()
let idTextField: UITextField = {
let textField = UITextField(frame: CGRect(x: 20, y: 276, width: 335, height: 52))
textField.placeholder = "아이디"
textField.font = UIFont.Pretendard.subhead4()
textField.backgroundColor = UIColor.Gray200
return textField
}()
let passwordTextField: UITextField = {
let textField = UITextField(frame: CGRect(x: 20, y: 335, width: 335, height: 52))
textField.placeholder = "비밀번호"
textField.font = UIFont.Pretendard.subhead4()
textField.backgroundColor = UIColor.Gray200
return textField
}()
}
//MARK: Delegate
extension LoginViewController_DelegatePattern {
@objc
private func loginButtonDidTap() {
//presentToWelcomeVC()
pushToWelcomeVC()
}
private func pushToWelcomeVC() {
let welcomeViewController = WelcomeViewController_DelegatePattern()
welcomeViewController.delegate = self // 1. 어어 너의 대리자는 나야
welcomeViewController.id = idTextField.text
self.navigationController?.pushViewController(welcomeViewController, animated: true)
}
}
// 2. 너가 말한 자격증을 채택했고 너가 하라는 기능을 나는 할 수 있어
extension LoginViewController_DelegatePattern: DataBindDelegate {
func dataBind(id: String) {
passwordTextField.text = "\\(id) + 1234 "
}
}
#Preview{
LoginViewController_DelegatePattern()
}
🛠 3. 의존성 주입(Dependency Injection, DI)과의 관계?
Delegate 패턴은 의존성 주입의 일종이라고 볼 수 있어.
- A 객체가 "내 일을 대신할 B 객체"를 외부에서 주입받잖아?
- 직접 생성하거나 의존하지 않고, 외부에서 주입해서 결합도를 낮춘다는 점에서 DI(Dependency Injection) 개념이 들어가는 거야.
요약하면:
- 의존성 주입은 "필요한 객체를 외부에서 주입받는 것"
- 델리게이트 패턴은 "필요한 행동을 대신할 객체를 외부에서 주입받는 것"
- 델리게이트는 특히 행동(메서드 실행)을 외주화하는 특화된 의존성 주입이야.
🧹 한눈에 정리
구분 설명
| Delegate 패턴 | 어떤 행동을 다른 객체에 위임하는 패턴 |
| AnyObject 붙이는 이유 | Delegate를 weak로 갖기 위해 (클래스만 허용) |
| Delegate와 DI 관계 | Delegate는 의존성 주입의 한 종류 (외부 주입) |
| weak var delegate | 메모리 누수(Retain Cycle) 방지하려고 약한 참조 |
아니 그럼 이거 왜쓰는건데?.
🛠 Delegate 패턴을 언제 써야 해?
"객체끼리 소통이 필요한데, 직접 참조해서 결합도를 높이고 싶지 않을 때" 사용해.
- 어떤 A 객체가 특정 상황이 되면 B 객체에게 알려야 하는데
- A가 B를 직접 알아버리면 둘이 강하게 엮여서(=결합) 유지보수가 어려워져.
- 이걸 막으려고 "B가 Delegate 프로토콜을 구현하고", A는 그냥 delegate한테 "약하게(weak) 요청"하는것
💡 즉, "낮은 결합(Loose Coupling)"을 만들고 싶을 때 Delegate 패턴을 쓴다!
💥 실전 예시 (iOS)
1. UITableView, UICollectionView
- 테이블뷰나 컬렉션뷰를 쓰려면 반드시 UITableViewDelegate, UITableViewDataSource를 구현해야 해.
- 테이블뷰가 직접 데이터를 가지는 게 아니라, "DataSource가 셀 몇개 있는지 알려줘",
- "Delegate가 셀 클릭되면 뭐할지 알려줘" 이렇게 위임하는 구조
tableView.delegate = self
tableView.dataSource = self
테이블뷰는 데이터, 동작을 직접 몰라도 돼. (Delegate가 대신 처리)
2. Custom View 내부 이벤트를 외부로 전달할 때
예를 들어, CustomButton이 있는데,
눌렀을 때 특정 ViewController가 뭘 하게 하고 싶어.
- 버튼이 뷰컨을 "직접 모르면 좋겠어"
- 그래서 Delegate 프로토콜 만들어서 버튼이 그냥 "나 클릭됐어!"만 알리고
- ViewController가 Delegate를 통해 반응하는 거야.
protocol CustomButtonDelegate: AnyObject {
func didTapButton()
}
class CustomButton: UIButton {
weak var delegate: CustomButtonDelegate?
@objc func buttonTapped() {
delegate?.didTapButton()
}
}
버튼은 외부가 뭘 하든 신경 안 쓴다! (낮은 결합)
3. A 뷰컨 → B 뷰컨 데이터 전달
- A가 B를 present하거나 push 한 다음,
- B에서 사용자 입력을 받아서 다시 A로 "결과를 넘겨줄 때" Delegate를 쓴다.
protocol BViewControllerDelegate: AnyObject {
func didEnterName(name: String)
}
class BViewController: UIViewController {
weak var delegate: BViewControllerDelegate?
func doneButtonTapped() {
delegate?.didEnterName(name: "정정욱")
}
}
B는 A를 모르고, 그냥 delegate한테 "입력했다"고 알려주는 것뿐
왜 Delegate를 써야 함?
이유 설명
| 낮은 결합 (Loose Coupling) | 서로 직접 의존하지 않아서 유지보수 편해짐 |
| 유연성 (Flexibility) | 다른 객체로 쉽게 교체 가능 |
| 재사용성 (Reusability) | View나 컴포넌트를 다른 곳에서도 쓸 수 있음 |
| 메모리 안전 (Memory Safety) | weak 덕분에 순환 참조(Retain Cycle) 방지 가능 |
개발하다 델리게이트를 써야 하는 순간
- CustomView가 외부 이벤트를 전달해야 할 때
- 뷰컨끼리 결과 데이터를 주고받을 때 (특히 모달 dismiss할 때)
- 테이블뷰, 컬렉션뷰 같은 UIKit 컴포넌트를 쓸 때
- 네트워크 콜백 결과를 외부로 넘기고 싶을 때
Closure 방식의 값 전달 델리게이트와 비슷함
A -> B
A <- B
- Closure 같는쪽 (야야 나 너가 주는 이름 없는 함수를 받을 준비가 끝났어)
- Closure 보내는쪽 (어어 그래? 그러면 너거 적절하게 함수 실행만 시켜 내가 어떤 코드들을 실행 해야하는지는 내쪽에서 정의 할거야)
- Closure 같는쪽
class WelcomeViewController_Closure: UIViewController {
// MARK: - Property
var id: String?
var loginDataCompletion : ((String) -> Void)? // 1. 함수를 받을 준비
// MARK: - UIComponent
let mainImage: UIImageView = {
let Image = UIImageView(frame: CGRect(x: 120, y: 60, width: 150, height: 150))
Image.image = ImageLiterals.login
return Image
}()
.
.
}
//MARK: Action
extension WelcomeViewController_Closure{
@objc
private func backToLoginButtonDidTap() {
guard let loginDataCompletion else { return }
if let id = id {
loginDataCompletion(id) // 2. 어어 나 해당기능 끝나면 너가 준거 실행 시킨다! : 받는놈이 실행을 한다
/*
print() // ()붙으면 함수 실행. {}만 있으면 함수 정의(보내는 쪽)
*/
}
if self.navigationController == nil {
self.dismiss(animated: true)
} else {
self.navigationController?.popViewController(animated: true)
}
}
private func bindID() {
self.welcomeLabel.text = "\\(id ?? "")님 \\n반가워요!"
}
func setLabelText(id: String?) {
//self.id = id // 요놈은 왜 타입 표출 일어남? String? 로 선언 되어 있기 때문임
self.welcomeLabel.text = "\\(id ?? "")님 \\n반가워요!"
}
}
- Closure 보내는쪽
//
// LoginViewController_Closure.swift
// Week_02
//
// Created by 정정욱 on 4/12/25.
//
import UIKit
class LoginViewController_Closure: UIViewController {
// MARK: - UIComponent
let passwordTextField: UITextField = {
let textField = UITextField(frame: CGRect(x: 20, y: 335, width: 335, height: 52))
textField.placeholder = "비밀번호"
textField.font = UIFont.Pretendard.subhead4()
textField.backgroundColor = UIColor.Gray200
return textField
}()
.
.
.
//MARK: Life Cycle
override func viewDidLoad() {
super.viewDidLoad()
self.view.backgroundColor = .white
addViews()
}
}
//MARK: Action
extension LoginViewController_Closure {
// 함수를 미리 정의를 해서 던진다 그럼 실행은 어디서 할까? 받은 쪽에서 한다
private func pushToWelcomeVC() {
let welcomeViewController = WelcomeViewController_Closure() // 함수를 받을놈
welcomeViewController.id = idTextField.text
// 1. 함수를 미리 정의 => 던짐 (적절한 곳에서 실행 부탁해)
// 클로저 값 캡처 방지 [weak self] 이제는 아시죵?
welcomeViewController.loginDataCompletion = { [weak self] data in
print("클로저로 받아온 id가 뭐냐면", data)
guard let self else { return }
self.passwordTextField.text = data
} // 여기 까지 함수 정의
self.navigationController?.pushViewController(welcomeViewController, animated: true)
}
}
#Preview{
LoginViewController_Closure()
}
😸 Delegate vs Closure 비교
구분 Delegate Closure
| 기본 개념 | '대리'에게 맡긴다 (프로토콜 기반) | 코드 블록(함수)을 변수로 넘긴다 |
| 설계 방식 | 프로토콜(Interface)을 정의하고, 구현하는 객체를 외부에서 주입 | 함수(Closure)를 외부에서 직접 주입 |
| 사용 목적 | 여러 이벤트를 다뤄야 할 때 (복합적/다수) | 특정 1개의 작업만 넘길 때 (간단, 직관적) |
| 관계 | 1:N (여러 함수) | 1:1 (한 개 함수) |
| 코드 길이 | 길어짐 (프로토콜 선언 + 구현) | 짧아짐 (inline 가능) |
| 대표 예시 | 테이블뷰, 커스텀 뷰 이벤트 전달 | 버튼 클릭 시 단순 액션, 네트워크 응답 처리 |
Delegate 사용 예시
여러 동작을 넘기고 싶을 때
protocol ProfileViewDelegate: AnyObject {
func didTapFollowButton()
func didTapMessageButton()
}
class ProfileView: UIView {
weak var delegate: ProfileViewDelegate?
@objc func followButtonTapped() {
delegate?.didTapFollowButton()
}
@objc func messageButtonTapped() {
delegate?.didTapMessageButton()
}
}
"Follow 버튼" / "Message 버튼" 각각 다른 일을 Delegate한테 알림
Closure 사용 예시
딱 한 동작만 넘길 때
class ProfileView: UIView {
var followButtonAction: (() -> Void)? // 클로저 변수
@objc func followButtonTapped() {
followButtonAction?() // 클로저 실행
}
}
"Follow 버튼 클릭" 하나만 넘기고 끝낼 때 클로저로 깔끔하게 가능.
아니 그럼 언제 뭘 써야 해?
상황 쓰는 것 이유
| 버튼 한두개 클릭 처리 | Closure | 간단하고, inline 처리로 코드 짧음 |
| 여러 동작을 하나의 객체에 묶어야 함 | Delegate | 관리하기 편하고 확장성 좋음 |
| 복잡한 라이프사이클 관리 (ex: 화면 이동, 상태 변경) | Delegate | 명확하게 역할 분리 가능 |
| 네트워크 요청 결과 받기 | Closure | 결과 하나만 넘기면 되니까 간단하게 |
❤️ 한 줄 요약
Delegate는 여러 행동을 한 번에 넘길 때,
Closure는 단일 이벤트를 간편하게 넘길 때 쓴다.
마지막 요약
- Delegate = "A야, 어떤 상황되면 나 대신 알려줘" (Protocol + 구현)
- Closure = "A야, 이 코드 한 조각 받아서 실행해줘" (Function as variable)