본문 바로가기

Swift

[Swift] Enum 열거형 정복하기 1편 (Raw, Associated Values)

열거형(Enumeration)은 연관된 값들의 집합을 공통된 타입으로 정의하는 것입니다. 열거형은 type-safe 하게 코딩할 수 있는 방법이죠. 주로 요일과 같이 한정된 경우의 수를 정의할 때 사용합니다.

 

기본적인 문법은 아래와 같습니다.

 

enum Name {
    case firstCase
    case secondCase
}

 

열거형의 이름은 주로 단수형을 사용합니다. C나 Objective-C와는 다르게 각 case 에 기본적인 정수값(0, 1, 2 ...)이 할당되지는 않습니다.

Raw Values

열거형의 case 는 모두 독립적인 값이지만 내부에 또 다른 값을 저장할 수 있습니다. 이것을 원시값(raw value)이라고 합니다. 열거형을 정의할 때 원시값 저장은 필수사항이 아닙니다.

 

직접 만든 열거형에 원시값을 저장하는 경우는 상대적으로 적습니다. 그러나 개발할 때 주로 사용하는 프레임워크에는 정수형 원시값이 저장된 열거형을 사용하는 경우가 상당히 많습니다.

 

문법을 한 번 살펴보겠습니다.

 

enum Name: RawValueType {
    case caseName = value
}

 

먼저 열거형 이름뒤에 원시값의 타입을 지정합니다. 그리고 각 case 마다 원시값을 할당할 수 있습니다. 원시값 타입으로 올 수 있는 것은 String, Character, Number Type 으로 제한됩니다.

 

선언시점에 저장한 원시값은 나중에 바꿀 수 없습니다. 원시값을 저장하는 부분은 생략도 가능합니다. 생략할 경우, 자료형 마다 각각 원시값 규칙이 존재합니다.

Number Type

enum CompassPoint: Int {
    case north
    case south
    case east
    case west
}

 

원시값 할당을 생략하면 0부터 1씩 증가하며 차례대로 할당됩니다.

 

CompassPoint.north.rawValue // 0
CompassPoint.south.rawValue // 1
CompassPoint.east.rawValue // 2
CompassPoint.west.rawValue // 3

 

만약 원시값을 직접 할당하면 어떤 값이 저장되는지 보겠습니다.

 

enum CompassPoint: Int {
    case north
    case south
    case east = 100
    case west
}

 

.east 에만 100을 할당해 보았습니다. 그 결과,

 

CompassPoint.north.rawValue // 0
CompassPoint.south.rawValue // 1
CompassPoint.east.rawValue // 100
CompassPoint.west.rawValue // 101

 

증가될 때의 기준값은 이전 case 이므로 100에서 1증가한 101이 .west 에 저장됩니다.

이번에는 특별한 생성자를 알아보겠습니다.

 

CompassPoint(rawValue: 0) // north
// 동일한 rawValue를 가진 north가 생성됨

CompassPoint(rawValue: 200) // nil
// 이런 경우에는 nil을 리턴함

// nil을 리턴할 수 있기 때문에 생성자의 리턴형은 옵셔널(CompassPoint?)임

 

rawValue를 인자로 받는 생성자를 사용하게 되면 해당 원시값과 매칭된 case가 생성됩니다. 위의 경우네는 0과 매칭되는 .north 가 생성됩니다. 만약 매칭되는 case가 없다면 nil을 반환합니다. nil을 리턴할 수 있기 때문에 생성자의 리턴형은 옵셔널(CompassPoint?)임을 알 수 있습니다.

String Type

7개로 한정되는 요일을 열거형으로 만들어 보겠습니다.

 

enum Weekday: String {
    case sunday
    case monday
    case tuesday
    case wednesday
    case thursday
    case friday
    case saturday
}

Weekday.sunday.rawValue // "sunday"

 

원시값의 자료형을 문자열로 선언하고 원시값 할당을 생략하면 case 이름과 동일한 문자열이 원시값으로 저장됩니다. 당연히 원시값을 직접 할당할 수도 있습니다.

Character Type

Character의 경우에는 항상 원시값을 직접 할당해주어야 합니다.

 

enum ASCII: Character {
    case tab = "\t"
    case lineFeed = "\n"
    case carriageReturn = "\r"
}

 

원시값을 직접 할당해주지 않는다면 컴파일 에러가 발생합니다.

Associated Values

열거형 case에 원시값(raw value)을 저장하는 대신에 연관된 값을 저장할 수도 있습니다. 연관값(associated value)은 원시값의 한계를 해결할 수 있습니다.

원시값의 한계

  • 모든 케이스가 동일한 형식을 사용해야함
  • 케이스당 값을 하나밖에 저장할 수 없음
  • 원시값 문자열에 숫자가 포함되어 있을 경우 숫자만 사용하려면 따로 추출해야하는 번거로움이 있음

연관값의 문법을 먼저 알아보도록 하겠습니다.

enum Name {
    case caseName(Type)
    case caseName(Type, Type, ...)
}

 

원시값의 형식은 열거형 이름 뒤에 선언하지만 연관값은 케이스 이름뒤에 선언합니다. 선언 시점이 아닌 새로운 열거형을 생성할 때 값을 저장합니다. 보시다시피 튜플(Tuple)을 사용하여 하나의 케이스에 서로 다른 연관값들을 저장할 수 있음을 알 수 있습니다.

예시를 통해 연관값 활용방법을 알아보겠습니다.

 

enum AppleDevice: String {
    case iPhone = "X, 256GB"
    case iMac = "27, Pro, 300만원"
    case macBook = "Air, 1kg, 150만원"
}

 

애플 기기에 대한 정보를 원시값을 이용하여 문자열로 저장하고 있습니다. 각 기기마다 저장할 값들이 다르고 단순히 콤마(,)로 구분되어 있어 해당 값을 추출할 때 번거로워 보입니다. 이를 연관값을 활용하여 바꿔보겠습니다.

 

enum AppleDevice {
    case iPhone(model: String, storage: Int) // named tuple
    case iMac(size: Int, model: String, price: Int)
    case macBook(String, Int, Int) // unnamed tuple
}

 

상황에 알맞게 named 혹은 unnamed 튜플을 사용할 수 있습니다. 연관값을 확인한 후에 코드를 실행할 때는 주로 switch 문을 사용합니다.

 

var gift = AppleDevice.iPhone(model: "X", storage: 256)

switch gift {
case .iPhone(model: "X", storage: 256):
    print("iPhone X and 256GB")
case .iPhone(model: "X", _)
    // 와일드카드 패턴 사용 가능
    print("iPhone X")
case .iPhone:
    // 연관값 생략 가능
    print("iPhone")
case .iPhone(let model, let storage):
    // 블록 내부에서 연관값을 사용할 땐 상수로 바인딩
    // 값을 변경할 때는 var 로 변경가능
    print("iPhone \(model) and \(storage)GB")
case let .iMac(size, model, price):
    // 모든 연관값을 동일한 형태로 바인딩한다면
    // let 키워드를 열거형 케이스 앞에 표기하는 것도 가능
    print("iMac \(size), \(model), \(price)")
}

 

새로운 케이스를 할당할 경우 모두 새로운 값으로 교체됩니다.

 

gift = .macBook("Air", 1, 150)

if case let .macBook(model, weight, price) = gift {
    print("maccBook \(model), \(weight)kg, \(price)")
}

 

마지막에 사용한 if-case 문은 다음 포스팅에서 자세히 알아보도록 하겠습니다.

정리

  • 열거형에 원시값 혹은 연관값을 저장하는 방법을 알아보았습니다.
  • String, Character, Number Type 만 원시값으로 저장할 수 있습니다.
  • 각 타입마다 원시값 할당에 대한 규칙이 존재합니다.
  • 원시값의 한계를 연관값을 사용하여 해결하였습니다.
  • 원시값을 확인할 때는 주로 switch 문을 활용합니다.