[Swift] Enum의 연관값과 CaseIterable은 왜 사용할까?

Enum

먼저 Enum을 사용할 때 연관값과 CaseIterable을 왜 사용하는지를 이야기하기 위해서는 Enum이 몬지 알아야겠쬬.

Enum, 열거형은 서로 연관된 값들의 집합입니다. 열거형은 컴파일 최적화가 되어 있어 컴파일 타임에 어떤 요소가 해당 열거형에 속하는지가 미리 정해집니다.

열거형에는 연관값 말고도 원시값이라는 RawValue를 사용하는데, 이 원시값을 통해서 해당 열거형을 더욱 폭넓게 사용할 수 있습니다. 원시값은 해당 열거형 케이스의 별칭이라고 불러도 좋을 것입니다. 예를 들어 봅시다.

 

enum 음식점: String {
    case 파스타 = "pasta"
    case 김밥 = "gimbap"
}

 

이런 식으로 사용할 수 있겠군뇨!

만약에 원시값을 사용하겠다고 선언하고 위처럼 따로 지정해 주지 않는다면 `파스타.rawValue`를 선언했을 때, "파스타" 혹은 0이 나옵니다. 해당하는 값을 String 값으로 지정해 주거나 원시값으로 Int를 채택할 경우에는 위부터 차례대로 indexing을 해주기 때문이죵.

 

 

 

연관값

그렇다면 연관값은 무엇일까요?

연관값은 각각의 enum의 케이스에 새로운 값을 입력할 수 있습니다. 원시값의 경우, enum에 속한 모든 case들이 같은 타입의 원시값을 가져야 했지만, 연관값은 각 케이스마다 다른 종류의 타입은 물론이고, 그 갯수까지 다르게 설정할 수 있습니다. 

연관값이 컴파일 타임에 미리 정의된 요소라고 했지요! 우리는 enum을 연관값을 통해서 커스터마이징해서 사용할 수 있다고도? 말할 수 있을 것입니다.

연관값을 이용하는 예시는 많겠지만, 최근에 작성했던 Router Pattern이 떠오르네요!

 

 

import Foundation
import Moya

enum MoyaNetwork {
    // login
    case signUp(model: SignUpData)
    case login(model: LoginData)
    case emailValidation(model: ValidationData)
    case withdraw
    
    // refresh
    case refreshAccessToken
    
    // 게시글
    case writePost(model: PostModel)
    case fetchPost(nextCursor: String)
    case fetchSpecificPost(postID: String)
    case modifiyPost(postID: String, commentUsers: String)
    case deletePost(postID: String)
    
    // 댓글
    case writeComment(postID: String, content: String)
    case deleteComment(postID: String, commentID: String)
    
    // 좋아요
    case liked(postID: String)
    case fetchPostUserLiked
    
    case fetchMyPosts(userID: String, cursor: String)
}

extension MoyaNetwork: TargetType {
    var baseURL: URL {
        return URL(string: APIKeyURL.baseURL.rawValue)!
    }
    
    var path: String {
        switch self {
        case .signUp(_):
            return "join"
        case .login(_):
            return "login"
        case .emailValidation(_):
            return "validation/email"
        case .withdraw:
            return "withdraw"
        case .refreshAccessToken:
            return "refresh"
        case .writePost, .fetchPost:
            return "post"
        case .fetchSpecificPost(let postID):
            return "post/\(postID)"
        case .modifiyPost(let postID, _), .deletePost(let postID):
            return "post/\(postID)"
        case .writeComment(let postID, _):
            return "post/\(postID)/comment"
        case .deleteComment(let postID, let commentID):
            return "post/\(postID)/comment/\(commentID)"
        case .liked(postID: let postID):
            return "post/like/\(postID)"
        case .fetchMyPosts(let userID, _):
            return "post/user/\(userID)"
        case .fetchPostUserLiked:
            return "post/like/me"
        }
    }
    
    var method: Moya.Method {
        switch self {
        case .signUp, .login, .emailValidation, .writePost, .writeComment, .liked:
            return .post
        case .withdraw, .refreshAccessToken, .fetchPost, .fetchSpecificPost, .fetchMyPosts, .fetchPostUserLiked:
            return .get
        case .modifiyPost:
            return .put
        case .deletePost, .deleteComment:
            return .delete
        }
    }
    
    var task: Moya.Task {
        switch self {
        case .signUp(let model):
            return .requestJSONEncodable(model)
            
        case .login(let model):
            return .requestJSONEncodable(model)
            
        case .emailValidation(let model):
            return .requestJSONEncodable(model)
            
            
            // refresh token
        case .refreshAccessToken:
            return .requestPlain
            
            
            // 포스트 작성
        case .writePost(let model):
            let productID = MultipartFormData(provider: .data(model.productID.data(using: .utf8)!), name: "product_id")
            
            let content = MultipartFormData(provider: .data(model.content.data(using: .utf8)!), name: "content")
            let content1 = MultipartFormData(provider: .data(model.nickname.data(using: .utf8)!), name: "content1")
            
            var multipartData: [MultipartFormData] = [productID, content, content1]
            
            if let images = model.file {
                var data: [MultipartFormData] = []
                images.forEach { data.append(MultipartFormData(provider: .data($0), name: "file", fileName: "\($0).jpg", mimeType: "image/jpg")) }
                multipartData.append(contentsOf: data)
                
                let content3 = MultipartFormData(provider: .data(model.ratio.data(using: .utf8)!), name: "content3")
                multipartData.append(content3)
            }
            
            return .uploadMultipart(multipartData)
            
        case .modifiyPost(_, let commentUsers):
            let content2 = MultipartFormData(provider: .data(commentUsers.data(using: .utf8)!), name: "content2")
            let multipartData: [MultipartFormData] = [content2]
            return .uploadMultipart(multipartData)
            
        case .fetchPost(let cursor), .fetchMyPosts(_, let cursor):
            let params: [String: String] = [
                "next": cursor,
                "limit": "20",
                "product_id": "MobbieFeed"
            ]
            return .requestParameters(parameters: params, encoding: URLEncoding.queryString )
            
            // comment
        case .writeComment(_, let content):
            let model = CommentModel(content: content)
            return .requestJSONEncodable(model)
            
        default: return .requestPlain
        }
    }
    
    
    var headers: [String : String]? {
        switch self {
        case .signUp, .login, .emailValidation:
            [
                "Content-Type": "application/json",
                "SesacKey": APIKeyURL.APIKey.rawValue
            ]
        case .refreshAccessToken:
            [
                "Authorization": UserDefaultsHelper.shared.accessToken,
                "SesacKey": APIKeyURL.APIKey.rawValue,
                "Refresh": UserDefaultsHelper.shared.refreshToken
            ]
        case .writePost, .modifiyPost:
            [
                "Authorization": UserDefaultsHelper.shared.accessToken,
                "Content-Type": "multipart/form-data",
                "SesacKey": APIKeyURL.APIKey.rawValue
            ]
        case .withdraw, .fetchPost, .fetchSpecificPost, .deletePost, .deleteComment, .liked, .fetchMyPosts, .fetchPostUserLiked:
            [
                "Authorization": UserDefaultsHelper.shared.accessToken,
                "SesacKey": APIKeyURL.APIKey.rawValue
            ]
        case .writeComment:
            [
                "Authorization": UserDefaultsHelper.shared.accessToken,
                "Content-Type": "application/json",
                "SesacKey": APIKeyURL.APIKey.rawValue
            ]
        }
    }
    
    var validationType: ValidationType {
        switch self {
        case .signUp, .login, .emailValidation, .withdraw:
            return .none
        default:
            return .successCodes
        }
    }
}

 

코드가 보기 좋을 만큼 깔끔하지는 않지만 ,, ^__^ ;;

enum의 연관값을 통해서 해당하는 값을 만들고, enum의 extension을 통하여 path와 method, task와 같은 데이터 통신에 필요한 요소들을 정의해줄 수 있었습니다.

만약에 연관값을 사용하지 않는다면 어떻게 했을까요?

함수를 만들거나 singleton Pattern의 다른 클래스를 만들어 일일이 관리해 주어야 했을 겁니다.

뭐, 그래도 좋겠지만 class보다 Enum이 컴파일 최적화가 되어 있기 때문에 연관값을 사용한 enum을 활용하는 것이 더 좋지 않을까? 하는 생각이 드네용.

 

 

 

 

 

 

 

CaseIterable

다음으로 CaseIterable입니당.

열거형을 사용할 때 각각의 케이스만 사용하는 경우도 있지만 종종 모든 경우의 case가 필요할 때가 있습니다. CaseIterable은 그럴 때 사용하는 프로토콜로, 각각의 케이스의 enum을 하나의 배열로 묶어 배열로 표현할 수 있게 해줍니다.

 

공식 문서의 예제를 가져와 보자면... ...

 

 

 

enum CompassDirection: CaseIterable {
    case north, south, east, west
}


print("There are \(CompassDirection.allCases.count) directions.")
// Prints "There are 4 directions."
let caseList = CompassDirection.allCases
                               .map({ "\($0)" })
                               .joined(separator: ", ")
// caseList == "north, south, east, west"

 

이런 식으로요!

 

 

 

아무튼, 결론을 내려보자면 연관값과 CaseIterable은 enum을 조금 더 폭넓게, 손쉽게 사용하기 위해서 쓰는 것 같습니다.

연관값의 경우에는 이미 정해져 있는 enum의 case를 더욱 잘 활용하기 위해 열거형의 case 이외에도 다른 타입의 값을 유동적으로 사용하기 위해 필요합니다.

CaseIterable은 앞서 말했듯이 열거형의 케이스를 배열처럼 손쉽게 사용하기 위해서 필요하구요!

열거형은 편리한데다가 컴파일 최적화까지 되어 있다는 게 정말 큰 장점인 것 같습니다 ^___^ 연관값과 CaseIterable, rawValue 또한 그 편리함을 더해주고용.

 

 

아무튼! 이만 마쳐보겠습니다앙.

 

 

 

 

참고 자료

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

 

CaseIterable | Apple Developer Documentation

A type that provides a collection of all of its values.

developer.apple.com

 

https://bbiguduk.gitbook.io/swift/language-guide-1/enumerations

 

열거형 (Enumerations) - Swift

Note Swift 열거형 케이스는 C와 Objective-C 처럼 기본적으로 정수값을 설정하지 않습니다. 위 예제 CompassPoint 에 north, south, east, west 는 0, 1, 2, 3 과 같지 않습니다. 대신 다른 열거형 케이스는 CompassPoint

bbiguduk.gitbook.io

https://babbab2.tistory.com/116

 

Swift) 열거형(Enumeration) 기초편

안녕하세요 :) 소들입니다!!! 오랜만에 재밌는 문법 포스팅~~~.~~~~ 오늘 공부해볼 것은 열거형이라는 것이에요!!!!! Enum, 풀네임은Enumeration이라는 것인데, Swift에선 이 열거형을 처음쓸 때 되게 헷갈

babbab2.tistory.com