본문 바로가기

Swift/Deep dive!!

[Swift] enum에서 사용되는 CaseIterable, RawRepresentable protocol 깊게 탐구하기!!

728x90


안녕하세요.

 

오늘은 enum에서 enum type의 모든 case를 Collect해서 배열로 반환할 수 있는 protocol인 CaseIterable을 소개하려고 합니다. 그리고 RawRepresentable도 소개하려고 합니다. Enum 타입으로 case에 rawValue, associated value 뿐 아니라 더 다양하게 활용할 수 있는 protocol이 제공되는 Swift가 정말 좋다고 느껴집니다: )

 

CaseIterable Protocol

그림1

 

CaseIterable은 protocol입니다.

Protocol 장점은 상속이 되지 않는 값 타입 자료형인 Enum에 여러 개의 protocol을 채택할 수 있습니다.

 

allCases의 타입은 Self.AllCases입니다. 이는 Collection을 준수하면서 [Self] 형태의 배열입니다. 그렇기에 배열에 관련된 함수도 사용할 수 있습니다.

 

https://developer.apple.com/documentation/swift/caseiterable/

 

CaseIterable은 enum에서 정의한 enum type's 모든 cases를 allCases 프로퍼티를 통해 반환합니다.

Enum case에 associated values가 있을 경우 모든 경우를 반환하기 위한 로직을 allCases의 연산프로퍼티(computed property)를 직접 정의해야 합니다.

@available에 따라 특정 case가 사용되는 경우 컴파일러는 자동적으로 allCases를 생성하지 않아 직접 정의해야 합니다.

 

 

그 이외의 경우는 enum case's assocaited value, 특정 case를 @available 어노테이션으로 제약하지 않은 경우엔 컴파일러가 자동으로 enum's cases에 대해서 allCases를 제공해줍니다.

 

CaseIterable Protocol 활용

 

https://developer.apple.com/documentation/swift/rawrepresentable#Enumerations-with-Raw-Values



기본적으로 associated type가 없을 경우 컴파일러가 자동으로 allCases를 구현합니다. (enum case's associated values or @avaliable 어노테이션이 없기 때문입니다.)

 

enum TravelThemeType: CaseIterable {
  case all
  case season
  case region
  case travelTheme
  case partner
}

let travelThemeType: [TravelThemeType] = TravelThemeType.allCases


TravelThemeType에서 준수한 CaseIterable protocol 덕에 자동으로 컴파일러가 구현한 allCases를 이용해 각각의 case 글자 길이가 6 이상인 case 개수를 출력하려고 합니다.

 

print(travelThemeType
  .map { String(describing: $0) } // ["all", "season", "region", "travelTheme", "partner"]
  .filter{ $0.count > 6 }
  .count) // 2 출력

 

방금은 String(describing:)을 사용했는데요.

 

Swift의 enum은 다른 언어의 열거형과 다르게 case에 대해 rawValue를 갖을 수 있는 정말 강력한 장점이 있습니다. (TMI: C언어로 테트리스 만들 때 문자열은 안되나,, 너무 아쉬웠었는데..)

RawValue를 지정하기 위해 enumeration에 raw type을 지정할 수 있고 String, Int와 같은 타입을 raw type로 지정할 수 있습니다.

 

RawRepresentable Protocol 활용

 

// Swift.Protocols 라이브러리
public protocol RawRepresentable<RawValue> {
  associatedtype RawValue
  init?(rawValue: Self.RawValue)
  var rawValue: Self.RawValue { get }
}


Enum에 String등의 rawType을 지정할 경우 컴파일러는 RawRepresentable protocol을 자동으로 준수합니다.

 

enum TravelThemeType: String, CaseIterable { ... } // raw type으로 String 지정

 

raw type String으로 enum에 지정했습니다. 그래서 이전 code snippet에서 map 고차함수에서 String(describing: $0)을 통해 변환하지 않아도 됩니다. 컴파일러가 RawRepresentable을 자동으로 준수하기 때문입니다.

 

print(travelThemeType
  .map { $0.rawValue } // ["all", "season", "region", "travelTheme", "partner"]
  .filter { $0.count > 6 }
  .count) // 2

 

그럼 위와 같이 rawValue로 case의 raw type에 대한 값을 얻어올 수 있습니다. 


반대로 RawRepresentable protocol을 준수해 rawValue computed property { get } 을 직접 정의 할 수 있습니다. 

 

// MARK: - RawRepresentable
extension TravelThemeType: RawRepresentable {
  var rawValue: String {
    switch self {
    case .all:
      return "전체"
    case .season:
      return "계절"
    case .region:
      return "지역"
    case .travelTheme:
      return "여행 테마"
    case .partner:
      return "동반자"
    }
  }
}

print(travelThemeType.map { $0.rawValue })
// 출력: ["전체", "계절", "지역", "여행 테마", "동반자"]

 

물론 이를 활용해 enum case에 associated values가 있는 경우 raw Type에 일치하는 특정 value만 반환해도되고 다양하게 정의할 수 있습니다.

CaseIterable Protocol 준수할 때, raw type과 associated value가 있을 경우

 

enum Season: String, CaseIterable {
  case spring = "봄"
  case summer = "여름"
  case fall = "가을"
  case winter = "겨울"
  
  static var count: Int {
    return Self.allCases.count // 컴파일러가 자동으로 allCases 구현했기에 이와 같이 활용 가능
  }
}

enum TravelThemeType: String, CaseIterable {
  case all
  case season(Season) // Error: Enum with raw type cannot have cases with arguments
  case region
  case travelTheme
  case partner
  
  static var allCases: [TravelThemeType] {
    return [
      .all,
      .season(Season.spring),
      .region,
      .travelTheme,
      .partner]
  }
}

 

우리나라 계절은 크게 봄 여름 가을 겨울이 있습니다. Enum 타입으로 지정한 후 raw type을 String으로 선언한 후 특정 case의 associated value를 지정한 경우 raw type과 case의 associated value를 같이 선언할 수 없습니다.

 

참고로 Season의 allCases를 다양하게 활용할 수 있습니다. allCases는 static이라는 점을 유의해야 합니다.

 

// MARK: - RawRepresentable
extension TravelThemeType: RawRepresentable {
  init?(rawValue: String) {
    switch rawValue {
    case "전체":
      self = .all
    case "계절":
      self = .season(Season.spring)
    case "지역":
      self = .region
    case "여행 테마":
      self = .travelTheme
    case "동반자":
      self = .partner
    default:
      return nil
    }
  }
  
  var rawValue: String {
    switch self {
    case .all: return "전체"
    case .season: return "계절"
    case .region: return "지역"
    case .travelTheme: return "여행 테마"
    case .partner: return "동반자"
    }
  }
}
print(travelThemeType.map { $0.rawValue }) // ["전체", "계절", "지역", "여행 테마", "동반자"]


이 경우 RawRepresentable을 extension으로 구현해야 합니다. 위에 코드처럼 RawRepresentable을 준수했기에, TravelThemeType에 선언했던 raw type인 String을 제거하면 case에 associated value가 있는 경우에도 rawValue를 직접 구현해  사용할 수 있습니다.

저는 allCases를 통해 TravelThemeType의 개수나 associated value를 제외한 case가 의미하는 rawValue를 얻어야 하기에 위와 같이 활용했습니다.

 

CaseIterable Protocol 과 , RawRepresentable 활용하기


앞으로 Season뿐 아니라 Region, Travel Theme등 여러 case를 enum으로 정의하고 때에 따라 카테고리 메뉴로 만들 것입니다. 이 enum들의 공통점은 CaseIterable 프로토콜을 준수하고, enum의 특징인 RawRepresentable의 rawValue를 이용해서 메뉴로 만들려고 합니다. 

extension Season {
  static var toKoreanList: [String] {
    Self.allCases.map { $0.rawValue }
  }

  static var count: Int { 
    Self.allCases.count
  }
}

// 17개 지역을 case로 지정한 enum
extension Region {
  static var toKoreanList: [String] {
    Self.allCases.map { $0.rawValue }
  }

  static var count: Int { 
    Self.allCases.count
  }
}

extension TravelThemeType {
  static var toKoreanList: [String] {
    Self.allCases.map { $0.rawValue }
  }

  static var count: Int { 
    Self.allCases.count
  }
}

...

 

이 enum들의 전부 공통점은 RawRepresentable, CaseIterable을 활용한다는 점입니다... static computed property들이 중복됨으로 차라리 CaseIterable을 extension해서 이들의 공통적인 특징을 기반으로 하는 static computed property를 적용해봤습니다.

 

extension CaseIterable where Self: RawRepresentable, Self.RawValue == String {
  static var toKoreanList: [String] {
    allCases.map { $0.rawValue }
  }
  
  static var count: Int {
    Self.allCases.count
  }
}


이 경우 CaseIterable을 채택헤 준수하는 타입(Self)이 RawRepresentable 타입인 경우, 대표적으로 enum이 있습니다. 이 경우에 toKoreanList, count라는 두 개의 static computed property를 사용할 수 있습니다. 그렇다면 이전에 각각의 enum에 exntesion으로 toKoreanList, count를 정의했던 중복 코드는 제거할 수 있습니다: ]

 

이번 포스트에선 enum에 대한 아주 짧은 개념과 CaseIterable, RawRepresentable을 소개했는데요. 틀린 부분 발견시 알려주신다면 정말 감사합니다.

728x90