Swift에서 Enum을 활용하는 10가지 방법



개발을 하고, 리팩토링을 하다보면 Enum을 썼을 때 좋은 결과를 얻은 경험이 많습니다. 그러나 너무 남발하게되면 또 코드가 더러워져서 어떻게 응용하면 좋을지 정리해 보도록 하겠습니다.

다음 아티클을 보고 가져온 코드도 있고, 이해했는지 검증하기 위해 새로 짠 코드들도 있습니다.

  • enum이 무엇인지 문법을 알지만 어떻게 활용해야 할지 모르는 분들
  • 미래의 나를 위해 작성했습니다

프로퍼티 활용방법

enum에는 stored properties를 만들 수 없습니다 즉 변수를 선언해서 할당할 수 없다는 뜻이죠. 하지만 computed properties는 선언해서 활용할 수 있습니다. 예제 코드를 보여드릴게요

enum Food {
    case korean
    case japanese

    var name: String {
        switch self {
        case .korean:
                return "kimchi"
        case .japanese:
                return "sushi"
        }
    }
}

let todaysLunch = Food.korean.name
print(todaysLunch)
// kimchi

위와 같이, 음식의 종류를 정했다면, 저장 프로퍼티인 name을 만들어서 각각의 값을 저장 해 줄 수 있습니다. 저는 enum에 String을 지정해서 return을 해주려고 했었는데 이 방법이 훨씬 깔끔한 것 같아요. 각각에 변수마다 필요한 값들을 내려주는 것이 명확하네요

메소드 활용방법

프로퍼티 말고 메소드도 enum에 정의해서 활용할 수 있습니다. 아래의 예시를 통해 설명 드리도록 하겠습니다.

enum Food {
    case korean
    case japanese

    func description() -> String {
        switch self {
        case .korean:
            return "\(self)은 한식입니다."
        case .japanese:
            return "\(self)은 일식입니다."
        }
    }
}

let todaysLunchDescription = Food.korean.description()
print(todaysLunchDescription)
// korean은 한식입니다.

메소드를 추가하여 분기를 태워 각각에 필요한 동작을 하는 코드를 짜서 활용하는 방법도 있습니다.

enum안의 enum 활용방법

enum안에 enum이 왜 필요할까요? 케이스를 외우고 찾으려 하니 잘 생각 나지 않아서, 근본적으로 enum이 필요한 경우를 생각해 보았습니다.
일반적으로 선택지가 제한되어 있을 때, 저는 enum을 사용합니다. 위에서도 음식이 한, 중, 일, 양식만 있다면, 굳이 여러개의 입력을 받는것이 아닌, 선택지 중에 선택을 한다면 혼동이 덜 할 것 입니다. 그렇다면 다시 처음의 질문으로 돌아가서 선택지 안에 또 선택지가 있다면, 우리는 enum안에 enum을 넣는 선택을 할 것입니다.

enum Character {
    enum Weapon {
        case bow
        case sword
        case axe
    }

    enum Armor {
        case chain
        case wooden
        case leather
    }

    case wrriror(weapon: Weapon, armor: Armor)
    case archor(weapon: Weapon, armor: Armor)

    func description() -> String {
        switch self {
        case let .wrriror(weapon, armor):
            return "검사의 무기는 \(weapon) 갑옷은 \(armor) 입니다"
        case let .archor(weapon, armor):
            return "검사의 무기는 \(weapon) 갑옷은 \(armor) 입니다"
        }
    }
}

let myCharactor = Character.archor(weapon: .bow, armor: .chain)
let supporterChartor = Character.wrriror(weapon: .sword, armor: .leather)

print(myCharactor.description(), supporterChartor.description())
//검사의 무기는 bow 갑옷은 chain 입니다 검사의 무기는 sword 갑옷은 leather 입니다

선택지에 갈림길이 있다면, enum안에 enum을 넣어 놓으면 코딩할 때 .만 찍어서 로직을 짤 수 있는 편리함이 있었습니다.

다만 수정할 떄 어디를 수정해야 하는지를 파악하는데 굉장히 힘들었습니다.

struct와 class에서 enum 활용하기

이전의 코드에서는 enum안에 enum을 넣어서 정리를 했습니다. 하지만 마지막에도 언급했듯이 무언가가 중첩되는 것은 이해하기 힘들고 또 헷갈리기 쉽습니다. 이럴 때 활용하면 좋은 방법입니다.

struct Character {
    enum CharacterType {
        case thief
        case warrior
    }

    enum Weapon {
        case bow
        case sword
        case dagger
    }

    enum Armor {
        case chain
        case wooden
        case leather
    }

    let type: CharacterType
    let weapon: Weapon
    let armor: Armor
}

let myCharacter = Character(type: .warrior, weapon: .sword, armor: .leather)
//warrior has sword and leather

이런식으로 선택할 수 있는 값들이 있다면, 클래스나 구조체 안에서 정의하고 사용하는 사람은 값을 입력하는게 아닌, 선택해서 사용할 수 있게 만들면 좋을 것 같습니다.

Mutating Method 활용방법

enum으로 정의된 타입을 변경할 수 있는 메소드를 작성해서 상태값을 변경해 줄 수 있습니다. struct나 enum의 값을 변경 해줄 때는 mutating 키워드를 붙여야 합니다. 간단한 방법이니 아래 코드를 통해서 이해해보도록 하겠습니다.

enum BatteryStateSwitch {
    case discharge
    case low
    case high
    case full
    mutating func charge() {
        switch self {
            case .discharge:
                self = .low
            case .low:
                self = .high
            case .high:
                self = .full
            default:
                print("boom")

        }
    }
}

var batteryState = BatteryStateSwitch.discharge
print(batteryState) // discharge
batteryState.charge()
print(batteryState) // low
batteryState.charge()
print(batteryState) // high
batteryState.charge()
print(batteryState) // full
batteryState.charge() // boom

Static Method 활용방법

enum안에 static method를 정의하여 활용하는 방법도 있습니다. 활용 케이스는 struct에서 static method가 필요한 경우와 같을 것 같습니다. 아래는 이름을 입력해서 디바이스의 타입을 반환 받는 메소드 입니다.

enum Device {
    case iPhone
    case iPad

    static func getDevice(name: String) -> Device? {
        switch name {
            case "iPhone12":
                return .iPhone
            case "iPhone8":
                return .iPhone
            case "iPad":
                return .iPad
            case "iPadPro":
                return .iPad
            default:
                return nil
        }
    }
}

if let device = Device.getDevice(name: "iPhone12") {
    print(device)
    //prints iPhone
}

Custom Init 활용 방법

enum은 기본적으로 init이 필요하지 않습니다. 하지만 init을 구현하여 원하는 타입을 반환하도록 할 수 있습니다. 마치 값을 입력받아 해당하는 선택을 반환하도록 말이죠.

enum HTTPStatus {
    case continueStatus
    case ok
    case multipleChoice
    case badRequest
    case internalServerError
    case error

    init(statusCode: Int) {
        switch statusCode {
        case 100..<200 :
            self = .continueStatus
        case 200..<300:
            self = .ok
        case 300..<400:
            self = .multipleChoice
        case 400..<500:
            self = .badRequest
        case 500..<600:
            self = .internalServerError
        default:
            self = .error
        }
    }
}

let networkStatus = HTTPStatus(statusCode: 404)
print(networkStatus)
//badRequest

Protocol Oriented Enum

enum이 프로토콜을 채택해서, 프로토콜 지향적으로 사용되는 방법입니다. 전혀 들은 적도 없고 고민해 본 적도 없기 때문에 게임을 하나 만들면서 설명해 보도록 하겠습니다.

protocol LifeSpan {
    var numberOfHearts: Int { get }
    mutating func getAttacked() // heart -1
    mutating func increaseHeart() // heart +1
}

LifeSpan이라는 프로토콜 입니다. 게임에서 HP를 관리하는 인터페이스 입니다. 에너지 게이지를 저장하는 프로퍼티와와, 공격받았을 때 에너지를 깎고, 에너지를 회복해주는 mutating 메소드가 있습니다.

enum Player: LifeSpan {
    case dead
    case alive(currentHeart: Int)

    var numberOfHearts: Int {
        switch self {
            case .dead: return 0
            case let .alive(numberOfHearts): return numberOfHearts
        }
    }

    mutating func increaseHeart() {
        switch self {
            case .dead:
                self = .alive(currentHeart: 1)
            case let .alive(numberOfHearts):
                self = .alive(currentHeart: numberOfHearts + 1)
        }
    }

    mutating func getAttacked() {
        switch self {
            case .alive(let numberOfHearts):
                if numberOfHearts == 1 {
                    self = .dead
                } else {
                    self = .alive(currentHeart: numberOfHearts - 1)
                }
            case .dead:
            break
        }
    }
}

Player는 두개의 상태를 가지고 있습니다. 살아있음과 죽음. 그리고 LifeSpan라는 프로토콜을 채택했기 때문에 프로퍼티와, 메소드를 구현해줍니다. numberOfHeartsself의 상태에 따라 다른 수를 반환합니다.

increaseHeart,getAttacked은 현재 상태에 따라 불렸을 때 numberOfHearts값을 변경해 줍니다.

한 번 게임을 해 볼까요? 위와 같이 코드를 짜게 되면 다음과 같이 사용할 수 있습니다.

var player = Player.dead // .dead

player.increaseHeart()  // .alive(currentHeart: 1)
print(player.numberOfHearts) //prints 1

player.increaseHeart()  // .alive(currentHeart: 2)
print(player.numberOfHearts) //prints 2

player.getAttacked()  // .alive(currentHeart: 1)
print(player.numberOfHearts) //prints 1

player.getAttacked() // .dead
print(player.numberOfHearts) // prints 0

Extensions의 활용방법

classstruct처럼 enum도 확장해서 사용할 수 있습니다. 확장해서 사용하는 경우는, 확장이라는 개념 보다는 enum하나에 너무 많은 프로퍼티와 메소드들이 추가 될 경우 가독성이 떨어지기 때문에 분리를 위해 사용합니다.

예시코드는 아래와 같습니다. enum 본래의 기능에 맞게 열거형을 먼저 나열 해줍니다.

enum Entities {
    case soldier(x: Int, y: Int)
    case tank(x: Int, y: Int)
    case player(x: Int, y: Int)
}

그리고, 상태를 변경하는 mutating메소드나, 프로퍼티를 추가한 extension을 작성합니다.

extension Entities {
    mutating func attack() {}
    mutating func move(distance: Float) {}
}

물론 하나의 enum안에 다 넣어도 됩니다. 위에 코드들도 그렇게 작성되어있습니다. 하지만 코드를 처음 보는 사람의 입장에서 enum 안에 메소드와 프로퍼티들이 있으면, 코드를 이해하기 어려울 수 있습니다. 그러므로 extension으로 분리 해줍니다.

또한 명시적으로 어떤 프로토콜을 채택해서 기능을 구현하였을 때도 extension으로 구현해주면 읽기 편해집니다. 위의 코드들도 한 번에 구현되어 있습니다.

extension Entities: CustomStringConvertible {
    var description: String {
        switch self {
            case let .soldier(x, y): return "Soldier position is (\(x), \(y))"
            case let .tank(x, y): return "Tank position is (\(x), \(y))"
            case let .player(x, y): return "Player position is (\(x), \(y))"
        }
    }
}

let player = Entities.player(x: 3, y: 3)
print(player.description)
//Player position is (3, 3)

Generic Enums활용방법

enum의 타입을 정의 할 수 있는것에 대해서는 많이들 아실꺼라고 생각합니다. 당연한 이야기지만 Generic을 활용해서도 정의할 수 있습니다.

아래 예제 코드를 통해 알아보도록 하겠습니다.

enum Information<T1, T2> {
    case name(T1)
    case website(T1)
    case age(T2)
}

위와 같이 T1,T2를 입력 받아 사용할 수 있도록 코드를 정의 해 보겠습니다.

let info = Information.name("Bob") // error
let info = Information<String, Int>.name("Bob")
print(info)

위와 같이 Information은 2개의 타입이 필요하기 때문에, 바로 사용할 수 없습니다. 2번째 줄 처럼 2개의 타입을 입력 해주고, 다음에 case를 선택 해주면, 입력 받을 타입이 자동으로 입력됩니다. 코딩을 할 때 내가 무엇을 해야하는지 알려주는 코드를 사용하면 너무 즐겁습니다.

정리

enum의 정의도 알고 있고, 충분히 활용하고 있다고 생각하고 있었는데 이렇게 활용 방법을 정리하고 나니 코드에 리팩토링 해야 하는 부분들이 보이기 시작합니다. 너무 리팩토링에 목메는 것은 안좋지만 이렇게 내가 짠 코드를 쓸 때 명확하게 만드는 작업은 중요하다고 생각합니다.

댓글

이 블로그의 인기 게시물

[IOS] AppDelegate는 뭐하는 녀석이지?

[git] Github 이슈 라벨(issue labels)

[git] git의 upstream과 origin 헷갈리는 사람 손!