열거형과 익스텐션

DEVJUN·2022년 7월 2일
0

Swift 문법

목록 보기
13/14
post-thumbnail

1. 열거형

  열거형(Enumberation)이란 하나의 주제로 연관된 데이터들이 멤버로 구성되어 있는 자료형 객체를 말한다. 집단자료형(배열, 집합, 딕셔너리 등)은 언제든지 삭제하거나 변경할 수 있으며, 할당된 값 자체를 사용할 수 없어 필요한 자료형을 사용하려면 매번 인스턴스를 생성하고, 값을 할당해서 데이터를 구성한 다음에 사용해야 한다. 또한 객체를 정의할 때 값이 함께 정의되는 것이 아니므로 개별 아이템의 정보를 컴파일러가 미리 인지할 수 없다.

  반면에 열거형에서 데이터들은 열거형 객체를 정의하는 시점에 함께 정의된다. 따라서 데이터를 함부로 삭제하거나 변경할 수 없고, 변경하거나 삭제하려는 객체를 정의하는 구문을 직접 수정해야 한다. 열거형의 데이터 멤버들은 정의개념으로 작성되는 것이므로 타입으로 사용할 수 있고, 👍🏻컴파일러가 미리 인지할 수도 있다.

  컴파일러가 미리 인지할 수 있고, 없고의 차이는 런타임(Run-Time)오류와 컴파일(Compile)오류의 차이로 나타난다. 쉬운 말로 열거형을 이용하여 데이터 타입을 정의하고 오타나 실수가 발생했을 때 그 즉시 컴파일러가 오류를 찾아주지만 집단자료형에서는 실행을 한 다음에야 오류를 잡아준다는 것이다.

  열거형을 사용하는 경우는 다음과 같다.

  • 원치 않는 값이 잘못 입력되는 것을 막고 싶을 때
  • 입력받을 값을 미리 특정할 수 있을 때
  • 제한된 값 중에서만 선택할 수 있도록 강제하고 싶을 때



1 - 1 열거형 정의

	
	// 열거형 정의 예시
	enum Direction {
    	case north
        case south
        case east, west
    }
    
    // 점(.) 구문을 이용하여 사용!
    let N = Direction.north
    let E = Direction.east
    
    //열거형 객체 사용 예시
    var directionToHead = Direction.west
    directionToHead = .east
    
    

  열거형 객체를 정의할 때는 enum 키워드를 사용한다. 이는 컴파일러에게 정의하고자 하는 객체가 열거형이라는 것을 알려주기 위함이다. 중괄호 블록 내에는 데이터 멤버들이 case 키워드와 함께 정리된다. 코드에서 east, west처럼 이렇게 콤마로 한줄로 정리할수도 있다.

  위의 열거형 객체 사용 예시 코드에서 directionToHead 변수에서 굳이 타입 어노테이션을 명시하지 않더라도 데이터 타입 추론 메커니즘에 의해 Direction 타입으로 정의된다. 또한 마지막 줄에서 .east로만 작성할 수 있는 것은 directionToHead 변수가 Direction 타입으로 정의되었다는 것을 컴파일러가 알고 있기 때문이다.


  열거형 타입으로 정의된 변수는 switch 구문에서 열거형의 멤버와 비교하는 분기 구문을 사용할 수 있다.

	var directionToHead = Direction.west 
    
    switch directionToHead {
    	
        case Direction.north:
        	print("북쪽")
        case Direction.south:
        	print("남쪽")
        case Direction.east:
        	print("동쪽")
        case Direction.west:
	        print("서쪽")      
    }
    
    
    // 이렇게 간단하게 줄일 수도 있음👍🏻
    switch directionToHead {
    	
        case .north:
        	print("북쪽")
        case .south:
        	print("남쪽")
        case .east:
        	print("동쪽")
        case .west:
	        print("서쪽")      
    }
    
    // 서쪽 출력

  위 코드에서 switch 구문 앞에서 타입 어노테이션을 사용해서 변수를 선언했을 때와 마찬가지로, switch 키워드 다음에 입력받는 변수를 통해 비교 대상의 타입이 열거형인 것을 추론해 낼 수 있기 때문에 위 코드 밑에 부분처럼 간단하게 .north, .east 이런식으로 간단하게 줄일 수있는 것이다.


1 - 2. 멤버와 값의 분리

  필요한 데이터 집합을 열거형의 멤버로 구성할 때, 데이터만으로도 의미 전달이 쉬울 때도 있지만, 대표적으로 HTTP 응답 코드와 같이 한눈에 구분하기 힘든 것들도 있다. 200, 304, 404 등등 생각보다 많기 때문에 한번에 알아보기는 쉽지 않을 것이다.

  실질적으로 필요한 HTTP 응답 코드들을 멤버에 값을 연계하여 사용한다면 훨씬 편리하고 좋은 자료형이 될 수 있다. 다음은 그 코드이다.

	enum HTTPCode: Int {
    	case OK = 200
        case NOT_MODIFY = 304
        case INCORRECT_PAGE = 404
        case SERVER_ERROR = 500
    }

  멤버에 위 코드처럼 별도의 값을 대입하기 위에서는 HTTPCode: Int와 같이 멤버에 대입할 값의 자료형을 열거형 타입의 선언 뒤에 타입 어노테이션으로 표기해야 한다는 것이다.

  열거형 객체의 멤버에 실질적인 값을 할당하는 것은 C와 오브젝티브-C에서도 제공하는 기능이지만 이 언어들에서는 각 멤버에 정수값만을 정의할 수 밖에 없고, 우리가 임의로 지정할 수 없다. 하지만 스위프트는 문자열, 정수, 실수, 다른 자료형까지 모두 멤버에 할당하여 사용할 수 있다.

	enum Rank: Int {
    	case one = 10 // 시작할 정수값 지정
        case two = 20
        case three, four, five
    }
    
    Rank.one.rawValue //10
    Rank.two.rawValue //20
    Rank.three.rawValue //21
    Rank.four.rawValue //22
    Rank.five.rawValue //23
    
    
    

  위 코드에서 시작하는 멤버에만 값 1을 지정해주고, 나머지 멤버에는 아무 값도 지정하지 않았지만 열거형은 첫 번째 멤버에 지정된 값을 기준으로 하여 나머지 멤버들도 차례대로 값을 가지게 된다. 또한 위에서 사용한 rawValue 속성은 열거형 멤버가 값을 저장하고 있을 때에만 사용할 수 있다.

  열거형 객체의 멤버와 같은 선언하는 시점에서 정의되지만, 사용하는 시점에서 멤버에 보조 값을 설정할 수 있는 방법도 있는데 이 설정된 값을 연관 값(Associated Values)이라고 한다.

  열거형은 클래스나 구조체처럼 내부에 연산 프로퍼티와 메소드를 정의할 수 있다. 열거형은 구조체나 클래스와는 달리 인스턴스를 만들 수 없지만, 열거형의 멤버를 모두 정의할 수 있다.

	enum HTTPCode: Int {
    	case OK = 200
        case NOT_MODIFY = 304
        case INCORRECT_PAGE = 404
        case SERVER_ERROR = 500
        
        var value: String {
        	return "HTTPCode number is \(self.rawValue)"
        }
        
        func getDescription() -> String {
        	switch self {
            	case .OK:
                	return "응답 성공."
            	case .NOT_MoDIFY:
                	return "변경된 내역 없음."
                case .INCORRECT_PAGE:
                	return "존재하지 않는 페이지."
                case .SERVER_ERROR:
                	return "서버 오류."
            }      
        }
        
        static func getName() -> String {
        	return "This Enumeration is HTTPCode"
        }
        
        
        //프로퍼티와 메소드 호출
        var resoibse - HTTOCode.OK
        response - .NOT_MODIFY
        
        response.value //"HTTPCode number is 304"
        response.getDescription() //"변경된 내역 없음."
        HTTPCode.getName() // "This Enumeration is HTTPCode"
    }

  위 코드 호출 부분에서 value와 getDescription()은 인스턴스 메소드의 성격이므로 열거형 객체의 멤버를 할당받은 변수 response에서 호출하지만, getName() 메소드는 타입 메소드❗️이므로 열거형 타입 자체에서 호출한다.

  열거형은 코코아 터치 프레임워크에서 아주 많이 사용되는 객체이다.

	enum UIImagePickerControllerSourceType: Int {
    	case photoLibrary
        case camera
        case savedPhotoAlbum
    }

  위 코드에서 이미지 피커 컨트롤러에서 이미지를 가져올 수 있는 곳을 포토라이브러리 카메라 저장앨범인데 이에 대한 소스타입을 지정할 때 사용한다. 이렇게 지정함으로써 개발자의 잘못된 입력이나 오타 등을 잡아 심각한 오류의 발생을 막아준다.

	
    let alert = UIAlertController(title: "알림",
    							  message: "액션 시트창", 
                                  preferredStyle: .action)
                                  
    let alert = UIAlertController(title: "알림",
    							  message: "경고창", 
                                  preferredStyle: .alert)
    
    

  또한 UIAlertController객체에서도 알림창이나 액션시트의 구분을 preferredStyle 매개변수의 속성값을 통해 제어한다. 이를 통해 알림창이 액션시트가 될지 경고창이 될지 결정하는 것이다.



2. 익스텐션

  익스텐션(Extensions)은 이미 존재하는 클래스나 구조체, 열거형 등의 객체에 새로운 기능을 추가하여 확장해주는 구문이다. 익스텐션은 라이브러리나 프레임워크에 포함되어 소스 코드에 직접 접근할 수 없는 객체라 할지라도 익스텐션을 이용하면 거의 제약 없이 새로운 기능을 추가할 수 있다는 점이 익스텐션의 강점이다.

  익스텐션을 통해 구현할 수 있는 것들은 다음이 있다.

  • 새로운 연산 프로퍼티를 추가할 수 있다.
  • 새로운 메소드를 정의할 수 있다.
  • 새로운 초기화 구문을 추가할 수 있다.
  • 기존 객체를 수정하지 않고 프로토콜을 구현할 수 있다.

  헷갈리지 말아야 할 점은 익스텐션은 extension이라는 키워드만 사용할 뿐 독립적인 객체를 생성하는 구문이 아니고 이미 정의되어 있는 기존 객체의 기능을 추가해주는 것일 뿐이다.

2 - 1 익스텐션과 연산 프로퍼티

  익스텐션을 이용하면 기존 객체에 프로퍼티를 추가할 수 있다. 단 연산 프로퍼티로 제한된다. 저장 프로퍼티는 익스텐션을 통해 추가할 수 없다.

	extension Double {
    	var km: Double { return self * 1_000.0 }
        var m: Double { return self }
    	var cm: Double { return self / 100.0 }
        var mm: Double { return self / 1_000.0}
        var description : String {
        	return "\(self)km 는 \(self.km)m, \(self)cm는 \(self.cm)m, \(self)mm는 \(self.mm)m"
        }
    }
    
    2.km // 2000m
    5.5.cm // 0.055m
    125.mm // 0.125m
    7.0.description // "7.0km는 7000.0m, 7.0cm는 0.07m, 7.0mm는 00.007m"

  위 예제는 기본자료형인 Double타입을 extension키워드를 통해 확장한 코드이다. 이런식으로 자료형의 기능을 확장해놓으면 각 단위별로 값을 연산해야 할 때 매우 편리하다.

2 - 2 익스텐션과 메소드

  익스텐션을 이용하면 기존 객체에 새로운 인스턴스 메소드나 타입 메소드를 정의할 수 있다. 매개 변수 타입을 달리하면 서로 다른 메소드가 되는 메소드 오버로딩 특성을 이용해서 새로운 메소드를 정의할 수도 있고, 메개변수명을 변경하여 새로운 메소드를 작성할 수 도 있다.

  하지만 기존 객체에서 사용된 같은 메소드를 익스텐션에서 재정의하는 것은 안된다. 이는 오버라딩을 뜻하므로 클래스 객체에서 상속으로만 할 수 있는 기능이기 때문이다.

	
    // ex1
	extension Int {
    	func repeatRun(task: () -> Void) {
        	for _ in 0 ..< self {
            	task()
            }
        }
    }
	
    let d = 3
    d.repeatRun(task: {
    	print("반갑습니다!")
    })
	
    // 반갑습니다!
    // 반갑습니다!
    // 반갑습니다!
    
    // ex2
    extension Int {
    	mutating func square() {
        	self = self * self
        }
    }
    
    var value = 3
    value.square() // 9
    

  위 첫번째 예제에선 Int 구조체를 확장하였고 repeatRun이라는 이름으로 메소드를 정의했다. 추가적으로 앞서 공부했던 클로저 관련 부분인데 입력받은 함수는 Int 자료형에 할당된 값만큼 반복해서 실행하도록 구문이 작성되어 있다. 함수를 인자값으로 사용한다는 것은 대신 클로저(익명함수)를 인자값으로 사용할 수 있다는 뜻이기도 하다.

  두 번째 예제에선 구조체나 열거형에선 정의된 메소드가 자기 자신의 인스턴스를 수정하거나 프로퍼티를 변경해야 할 때 mutating❗️ 키워드를 사용하는데, 익스텐션이 구조체나 열거형을 확장의 대상으로 삼았을 때가 이에 해당한다.

  또한 주의할 점은 해당 메소드가 인스턴스 자체의 값을 변경하고 있으므로 let value = 3과 같이 상수에 값을 할당하면 오류가 발생한다.

2 - 3 익스텐션을 활용한 코드 정리

  기존 소스 코드를 건드리지 않고도 원하는 내용을 확장할 수 있다는 특징 때문에, 익스텐션은 주로 라이브러리에 정의된 클래스의 기능을 추가할 때나 오버라이드할 때 사용된다.

  스위프트로 작성된 각종 라이브러리 코드를 보면 익스텐션의 특이한 사용법이 발견된다. 커스텀 클래스(개발자가 직접 만든 클래스)를 작성할 때 익스텐션을 활용하는 것인데, class 키워드를 사용하여 커스텀 클래스의 기본 코드만 정의하고는 이어서 다시 익스텐션을 사용하여 나머지 코드를 보완하는 경우이다.

	import UIKit
    
    public class DataSync {
    	public func save(_ value: Any, forKey: String) { ... }
        public func load(_ key: String) -> Any { ... }
        public func remove(_key: String) {...}
    }
    
    
    extension DataSync {
    	public func stringToDate(_ value: String) -> Date { ... }
        public func dateToString(_ value: Data) -> String { ... }
    }
    

  위 코드에서 커스텀 클래스인 DataSync를 정의하고, 익스텐션을 이용하여 나머지 메소드를 추가하고 있다. 커스텀 클래스에 다 때려박으면 되는데 왜❗️ 익스텐션을 쓰는지 의아하다🤔. 이는 익스텐션의 또 다른 용법이다. 오브젝티브-C에서는 #pragma mark라는 주석이 있다.이 주석의 특징은 '주석 이후에 작성된 코드를 의미적으로 그룹화하여 다른 코드로부터 구분'하는 데에 있다. 이 기능은 스위프트에서 지원되지 않아 불편했지만 익스텐션을 활용하면 기존의 클래스 정의와 구분되어 점프 바에서 별도의 그룹으로 표시되는 특징이 있다.

  위 사진과 같이 익스텐션을 통해 메소드를 나름의 기준으로 나누었다. 커스텀으로 내가 추가한 메소드는 익스텐션 블록으로 옮겼고 이를 통해 분리할 수 있다는 장점이 있다. 하지만 액션 메소드(@IBAction 어트리뷰트)가 붙는 메소드는 익스텐션으로 작성할 수 없다.

  익스텐션의 장점은 이뿐만 아니라 진정한 장점은 ⭐️델리게이트 패턴의 구현 위임💥이다

  iOS 인터페이스를 구현하다보면 델리게이트 패턴을 많이 사용하게 되는데 델리게이트 패턴 구현에는 프로토콜이 사용되기 때문에 클래스 소스코드가 프로토콜 관련 코드들로 인해 어지러워😱 진다.

	class ViewController: UIViewController, UITableViewDataSource, 
    UITableViewDelegate,UIImagePickerContorllerDelegate, 
    UINavigationControllerDelegate, UITextViewDelegate,
    UISearchBarDelegate {
    	// ...
    }
    
    			⬇️
    
    
    class ViewController: UIViewController {
    	// UIViewController에 대한 오버라이드 메소드 및 액션 메소드
    }
    
    // MARK: 테이블 뷰를 위한 프로토콜 델리게이트 구현
    extension ViewController: UITableViewDataSource, UITableViewDelegate {
    	// 테이블 뷰 구현을 위한 델리게이트 메소드 구현
    }
    
    extension ViewController: UIImagePickerControllerDelegate, UINavigationControllerDelegate {
    	// 이미지 피커를 위한 델리게이트 메소드 구현
    }
    
    .
    .
    .
    

  하지만 위에서 프로토콜을 성격에 맞게 여러 개의 익스텐션으로 나누어 구현하면 정말 깔끔한 코드를 만들어 낼 수 있다. 또한 // MARK: 텍스트 뷰 관리 등과 같이 MARK 주석을 사용하면 위에서 보았던 사진과 같이 저런식으로 주석이 나타난다. 이를 통해 코드를 간결하게 유지하고 유지보수를 쉽게 하는 데에 큰 도움이 된다😊.

profile
🧑🏻‍💻iOS

0개의 댓글