일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | ||||
4 | 5 | 6 | 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | 15 | 16 | 17 |
18 | 19 | 20 | 21 | 22 | 23 | 24 |
25 | 26 | 27 | 28 | 29 | 30 | 31 |
- 템플릿메서드
- WKWebView
- 상태패턴
- 전략패턴
- 컴파운드패턴
- cocoapods
- Xcode
- Scenedelegate
- 커맨드패턴
- 데코레이터패턴
- SWIFT
- RxSwift
- Mobile
- 디자인패턴
- 파사드패턴
- ViewController
- DispatchQueue
- 팩토리메서드패턴
- 옵저버패턴
- 추상팩토리패턴
- Lifecycle
- 프록시패턴
- 컴포지트패턴
- unowned
- 이터레이터패턴
- 스트래터지패턴
- 스테이트패턴
- ios
- 싱글턴패턴
- 어댑터패턴
- Today
- Total
ios dev kangwook.
iOS) Protocol에 대한 고찰 본문
일을 하다보면, 그러니까 점점 비대해지는 이 프로젝트를 보고 있자면 가끔씩 갑갑한 마음이 들 때가 있다.
그럴 때 마다 아 좀 더 유연하게 코드를 짤 수 없을까 라는 생각이 들기 마련이고 이럴 때 활용할 수 있는게 바로 프로토콜이 되겠다.
나도 완전히 능숙하게 사용하지는 못하지만, 최대한 protocol을 활용하려고 노력 중이고, 왜 강력한 도구인지 소개하려고 한다.
일단 Swift에서 기본적으로 제공하는 프로토콜들을 한 번 살펴보자
Equatable
Equatable 프로토콜은 두 인스턴스를 비교할 수 있도록 한다. == 연산자를 사용해서 두 객체가 같은지 비교할 때 사용한다. 직접 정의한 Classsk Struct에 Equatable 프로토콜을 추가하면, 해당 타입의 인스턴스들을 비교하는 로직을 구현할 수 있다.
예를 들어 User라는 구조체를 정의하고 Equatable 프로토콜을 추가한다면 두 Person 인스턴스의 name과 age를 비교하여 동일한 사람인지 판별할 수 있다.
struct Person: Equatable {
let name: String
let age: Int
static func == (lhs: Person, rhs: Person) -> Bool {
return lhs.name == rhs.name && lhs.age == rhs.age
}
}
보통은 id값을 두어서 이거로 비교하는 방법을 주로 사용하는 것 같다.(내가 개발하는 프로젝트의 경우 idx로 사용한다...)
Comparable
Comparable 프로토콜은 인스턴스들을 순서대로 정렬할 수 있도록 한다. <, >, <=, >= 연산자를 사용해서 객체의 순서를 비교할 때 사용한다. Comparable 프로토콜을 구현하면, 해당 타입의 인스턴스들을 정렬하는 로직을 구현할 수 있다.
예를 들어 Task라는 구조체를 정의하고 Comparable 프로토콜을 추가한다면, priority를 기준으로 작업의 우선순위를 비교하여 정렬할 수 있다.
struct Task: Comparable {
let title: String
let priority: Int
static func < (lhs: Task, rhs: Task) -> Bool {
return lhs.priority < rhs.priority
}
}
Hashable
Hashable 프로토콜은 인스턴스를 hash 가능하도록한다. 여기서 hash란 뭐냐. 객체를 고유하게 식별할 수 있게하는 정수 값이라고 보면 된다. Set이나 Dictionary와 같은 컬렉션 타입에서 객체를 효율적으로 저장하고 검색하는데에 사용한다.
Hashable 프로토콜을 구현하면 객체를 해싱하는 로직을 구현할 수 있다.
예를 들어, Product 라는 구조체를 정의하고 Hashable 프로토콜을 추가한다면, id 프로퍼티를 기반으로 해시값을 생성하여, Set을 사용하여 중복된 상품을 제거할 수 있다.
struct Product: Hashable {
let id: Int
let name: String
func hash(into hasher: inout Hasher) {
hasher.combine(id)
}
}
Codable
많이 사용하기도 하고, 나 또한 사용하고 있는 Codable 프로토콜은 인스턴스를 JSON이나 XML등으로 encoding / decoding 할 수 있도록 한다. 데이터를 저장하거나 네트워크를 통해 전송할 때 꽤나 유용하게 사용하고, 이 Codable을 구현하면 특정 데이터 형식으로 변환하는 로직을 구현할 수 있다.
예를 들어, Message라는 구조체를 정의하고 Codable 프로토콜을 추가한다면, JSON형식으로 메시지를 인코딩하여 서버로 전송하거나 JSON 데이터를 디코딩하여 Message 객체를 생성할 수 있다.
struct Message: Codable {
let sender: String
let content: String
}
자 그럼 이런 기본적인 프로토콜들은 알겠다 이거다. 그럼 내가 프로토콜을 만들어야 될 상황은 어떤 상황일까?
바로 Delegate 패턴을 들 수 있겠다.
Delegate 패턴은 특정 이벤트가 발생했을 때, 다른 객체에게 작업을 위임하는 데 사용한다.
쉽게 설명하기 위해 예를 들자면, UITableView는 스스로 모든 것을 처리하기보다는, 다른 객체에게 "테이블 뷰에서 이런 이벤트가 발생했는데, 어떻게 처리할래?" 라고 묻는 방식을 사용한다. 이 때, "어떻게 처리할래?"라는 질문에 답하는 녀석이 바로 UITableViewDelegate 프로토콜을 준수하는 객체다.
예를 들어 UITableView에서 특정 행을 선택했을 때 어떤 작업을 수행해야할까? 라는 질문에 답하기 위해 UITableViewDelegate프로토콜은 tableView(_:didSelectRowAt:) 메서드를 제공한다.
protocol UITableViewDelegate: NSObjectProtocol {
// ... (다른 메서드들)
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath)
}
UITableView는 특정 행이 선택되면 delegate 객체의 tableView(_:didSelectRowAt:) 메서드를 호출한다. 이 메서드를 구현하는 쪽에서는 선택된 행의 정보를 이용해서 원하는 동작을 수행할 수 있다.
class MyViewController: UIViewController, UITableViewDelegate {
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
// 선택된 행의 데이터를 가져와서 처리한다.
let selectedData = data[indexPath.row]
// 예를 들어, 선택된 데이터를 사용하여 다른 뷰 컨트롤러로 이동한다.
let detailViewController = DetailViewController(data: selectedData)
navigationController?.pushViewController(detailViewController, animated: true)
}
}
사실 이 Delegate 부분은 쉽게 이해를 시키기 위해 UITableView의 예를 든거지만, 직접 자신의 커스텀뷰에 대한 동작을 Delegate로 만들어서 다른 객체에 위임하는 식으로 Delegate 패턴을 이용한 프로토콜을 만들 수 있다.
처음에는 생각보다 뭔소리야? 라는 생각이 들 수 있지만, 계속 사용하고 이해하다보면 굉장히 익숙해지는게 프로토콜을 이용한 delegate패턴이라고 볼 수 있겠다.
자 그럼 한 번 간단한 프로토콜을 만들어 보도록 하자.
프로젝트를 하다보면, 새로고침 버튼을 누른다던가, 위로 스크롤해서 새로고침을 한다던가, 여러가지 방법으로 refresh를 해야할 경우가 생긴다. 그런데 모든 뷰나 뷰 컨트롤러에서 필요한 것은 아니다.
그래서 필요한 부분에서 적절한 동작을 할 수 있게 Refreshable이라는 프로토콜을 만들도록 해보자
이 프로토콜은 UIViewController 뿐만 아니라, View를 포함한 다양한 곳에 적용될 수 있다는 점이 핵심이다.
예를 들어, 데이터를 표시하는 DataViewController와 DataView가 있다고 가정해보자.
class DataViewController: UIViewController, Refreshable {
// ...
func refresh() {
// 데이터를 다시 불러와서 뷰를 업데이트 한다.
}
}
class DataView: UIView, Refreshable {
// ...
func refresh() {
// 데이터를 다시 불러와서 뷰를 업데이트 한다.
}
}
Refreshable 프로토콜을 통해 DataViewController와 DataView가 refresh 기능을 지원한다는 것을 명확하게 알 수 있다.
이 프로토콜을 활용하는 다른 객체들은 DataViewController와 DataView의 구체적인 타입을 알 필요 없이, refresh() 메서드를 호출하여 데이터를 새로 고칠 수 있다.
이를 보면 알 수 있듯이, Refreshable 프로토콜은 코드의 의존성을 줄이고, 유연성을 높이는데 기여한다고 할 수 있겠다.
조금 더 응용해보자면, extension을 통해 기본적인 구현을 제공하는 거다.
UIView와 UIViewController는 명확하게 다른 class이기 때문에, 각각 뷰를 다시 그리는 방법도 다르다. 따라서 그에 대한 기본적인 구현을 작성해보는 거다.
extension Refreshable where Self: UIViewController {
func refresh() {
for child in children {
(child as? Refreshable)?.refresh()
}
}
}
extension Refreshable where Self: UIView {
func refresh() {
setNeedsDisplay()
}
}
뭐.. 그냥 이런식으로 각각 기본적인 구현을 protocol의 extension에서 할 수 있다는 얘기다
정말 정말 간단하게 보여주기 위해서 위와 같이 한거고, 이를 응용해서 더욱 더 확장해 나가면 된다.
여기서 더 확장해 나가면 예를 들어 NetworkRefreshable: Refreshable이라는 protocol을 채택하는 protocol을 하나 더 생성해서 더욱 구체화 시킬 수도 있고, Delegate와 조합하여 더욱 상세한 동작을 할 수 있게 만들 수도 있겠다.
이와 같이, 프로토콜은 특정 기능을 구현하도록 강제하는 도구인 와중에 코드를 유연하게 만들어 재사용성을 높이는 Swift의 강력한 장점이라고 할 수 있겠다.
이러한 프로토콜을 잘 활용하면 Network 레이어의 동작이라던지, 각종 Delegate의 작동 방식에 대해서 더욱 더 잘 이해하고 활용할 수 있을 거 같다. (물론 나도 아직 멀었다)
이 글을 보고 조금 더 프로토콜이랑 친해질 수 있는 계기가 되길 바라며... 이만 가보겠슴다
빠이염
'iOS' 카테고리의 다른 글
iOS) CALayer, Shadow, Border 그리고 CornerRadius (1) | 2024.11.15 |
---|---|
iOS) 각종 Extensions 소소한 팁 (2) (0) | 2024.05.13 |
iOS) 각종 Extensions 소소한 팁 (1) (0) | 2024.05.08 |
iOS) Open Source들의 License 명시하기 (0) | 2024.05.07 |
iOS) Alamofire 4 -> Alamofire 5 Migration (0) | 2024.04.02 |