Swift enum을 이용해서 사소하게 코드를 개선해보자
김주희
2021-09-01
1~2명에서 운영하던 프로젝트가 시간이 지날수록 점점 커지고, 3명 이상 같이 협업하게 되면서 방대한 양의 코드가 계속 쌓이게 됩니다. 빠른 개발로 인해 각 기능을 각자 구현하다 보면 다른 개발자가 코드를 바로 이해하기에 시간이 걸릴 수 있고, 때로는 레거시 코드로 인해 구현하다가 발목이 잡히게 될 수도 있습니다.
이런 문제를 해결하고자 작년부터 레거시 코드 개선과 코드를 작성할 때 읽기 쉬운 코드로 작성하도록 노력해봤습니다. 작업하면서 스위프트의 열거형(enum)을 이용해서 사소하게 코드를 개선한 부분들을 소개하겠습니다.
1. 행위 타입 지정
종류를 구분하거나 연관된 값들을 하나의 그룹으로 만들어 사용할 때 enum을 사용하는 경우가 있습니다. 예를 들어 책의 장르를 enum 타입으로 지정하거나 요일을 enum 타입으로 만들어 사용할 수 있습니다. 저는 여기서 행위를 하나의 그룹으로 만들어 사용하고 있습니다.
뜻이 헷갈릴 수 있는 코드
상품 혹은 스토어가 찜이 되어 있는지 아닌지에 따라 해야 하는 작업이 다릅니다. 찜이 안 되어 있으면 찜 추가 관련 네트워크 작업 혹은 데이터베이스 작업을 수행합니다. 반대도 마찬가지입니다. 그래서 찜이 되어 있는지 안 되어 있는지 등과 같이 true
혹은 false
로 판단할 때 Bool
값을 가지고 핸들링하는 경우가 많습니다.
if store.isBookmarked {
service.bookmarkStoreDelete()
store.count -= 1
...
} else {
service.bookmarkStoreAdd()
store.count += 1
...
}
얼핏 bool 값으로 판단해서 처리하기 때문에 깔끔해 보일 수 있습니다. 하지만 뜻이 헷갈리거나 작업하다 보면 반대의 상황이 발생하는 등 혼란이 있었던 적이 있습니다. ‘찜이 되어 있으니까 삭제를 해야 하는가? 아니면 추가를 해야 하는가?’, ‘isBookmarked가 이제 true니까 찜 동작하는 거겠지? 아니면 찜이 되어 있으니까 삭제를 하는 동작이겠지?’ 등과 같이 isBookmarked 단어 혹은 값으로 판단할 때 헷갈릴 수 있습니다. 실제로 레거시 코드를 수정하다 보니 위와 같이 헷갈려 하면서 작업했는데, 실제로는 반대로 작업해야 하는 상황이 발생했었습니다.
액션 전용 타입
앞서 행위를 하나의 그룹으로 만들어 사용했던 것이 바로 헷갈리는 상황을 방지하기 위해서였습니다. 찜하는 행위가 추가 혹은 삭제, 두 가지 밖에 없으니 enum으로 만들어서 사용했습니다. 명확하게 어떤 행위를 하는 건지 단어로만 봐도 충분하므로 헷갈리는 소지를 덜 수 있었습니다. 또한 switch-case문과 같이 사용하면 빠짐없이 모든 액션에 대해서 대응하고 작업할 수 있었습니다.
enum BookmarkActionType {
case add
case delete
}
let action: BookmarkActionType
switch action {
case .add:
// 스토어를 찜했을 때 동작하는 작업
case .delete:
// 찜한 스토어를 삭제했을 때 동작하는 작업
}
2. Namespace으로 사용
Namespace는 프로그래밍 구성체로, 하나의 이름으로 관련 있는 것들을 그룹화할 수 있습니다. 스위프트는 다른 언어들과 달리, namespace
라는 키워드가 없습니다. 하지만 enum을 이용해서 이와 비슷하게 만들어 사용할 수 있습니다.
작업하다 보면 하나의 그룹 개념 안에서 다양한 타입들을 만들어 사용하는 일이 생길 수 있습니다. 예를 들어 쿠폰일 경우 쿠폰의 상태를 타입별로 나타낼 수 있고, 쿠폰이 정액인지 정률인지 타입으로 나타낼 수 있습니다. 보통 타입별로 enum을 만들어서 사용할 수 있습니다.
enum TypeCouponSale {
case price
case rate
}
enum TypeCouponState {
case available
case downloaded
case used
case expired
...
}
사용하는 데는 큰 문제는 없지만 하나의 그룹 안에서 사용하면 좋지 않을까 하는 생각이 들었습니다. 예를 들어 coupon이라는 그룹 안에 SaleType이 있고 State가 있는 것입니다. 이를 해결할 수 있는 방법 중 하나는 coupon이라는 namespace를 만들고, 그 안에 SaleType과 State 타입을 만들어 사용하는 것입니다.
enum Coupon { } // namespace
extension Coupon {
enum State: String {
case available
case downloaded
case used
case expired
}
enum SaleType: String {
case price
case rate
}
}
이제 실제로 코드에 적용해보면 조금 더 coupon이라는 그룹 안에 있는 타입을 사용하는 것으로 보일 수 있습니다. 만약 coupon이랑 연관된 것들이 생기면 해당 coupon 네임스페이스 안에 추가해서 사용하면 됩니다.
// Old
var couponSaleType: TypeCouponSale
coupon.state = TypeStateCoupon.downloaded
// New
var couponSaleType: Coupon.SaleType
coupon.state = Coupon.State.downloaded
3. Enum 기반 로그 시스템
서비스 개선할 수 있는 방법 중 하나는 유저들이 남긴 로그들을 수집하여 데이터 분석하는 방법이 있습니다. 실제로 유저들이 앱을 얼마나 어떻게 사용하는지 파악할 수 있으며, 이를 바탕으로 유저들에게 좋은 경험을 주거나 프로젝트 개선에 도움을 줄 수 있습니다.
처음 개발했을 때는 필요한 로그가 많지 않아 공통 로그를 수집하는 함수와 특정 로그를 수집하는 함수를 따로 만들어서 사용하고 있었습니다. 그리고 필요한 프로퍼티들을 Analytics 담당 클래스 안이 아닌 바깥에서 만들어 파라미터로 넘기는 방식으로 되어 있거나, 데이터를 파라미터로 넘겨 Analytics 클래스 안에 있는 함수 내부적으로 필요한 항목을 뽑아서 설정하는 방식으로 구현되어 있었습니다.
// Old Style
// 필요한 로그가 많지 않아 각 고유의 로그 이벤트 수집하는 함수
func sendLogEventWithProductProperties(eventName: String, product: Product)
// 로그 이름과 필요한 데이터를 파라미터로 넘겨서 보내는 방법
func logEvent(eventName: EventName, properties: [EventPropertyKey: Any]?)
// 각 로그의 이름 목록
enum EventName {
...
}
// 각 로그에 필요한 프로퍼티 목록
enum EventPropertyKey {
...
}
// Usage
class ViewModel {
...
func sendBookmarkProductLogEvent() {
let properties: [EventPropertyKey: Any] = [
.PRODUCT_NAME: product.name,
.PRODUCT_PRICE: product.price,
...
]
analytics.logEvent(eventName: .BOOKMARK_PRODUCT, properties: propertiesForBraze)
}
func productSelected(at index: Product) {
...
analytics.sendLogEventWithProductProperties(eventName: "상품클릭", product: product)
}
}
로그가 점점 많아지면서, 로그의 이름과 프로퍼티 목록은 점점 많아지고 특정 로그에 대한 함수들을 수정하거나 다시 만들어서 사용하는 경우가 생기기 시작했습니다. 그래서 각 로그에 대한 함수를 만들지 않고 공통으로 아우르고, 로그와 함께 필요한 항목들도 보내는데 편리한 방법으로 개선할 수 없을지 고민했습니다. 마침 로그 수집하는 부분 관련해서 대대적인 개편 작업이 있었는데, 해당 작업을 하면서 코드를 개선해보는 작업도 같이 고려하여 작업했습니다.
로그 하나로 이름과 메타 데이터 정의
먼저 각 로그의 이름과 메타 데이터를 분리해서 사용하는 것을 하나로 통합해서 정리하는 식으로 정리했습니다. 스위프트 enum을 사용할 때, 각 케이스별 관련 있는 값(associated value)을 가질 수 있습니다. 그래서 이 특징을 이용해서 로그를 한 케이스로 보고, 관련 메타 데이터를 연관 값으로 사용할 수 있도록 아래와 같이 정리했습니다.
enum Event {
case homeScreenView
case productSelect(index: Int)
case productScreenView(product: Product)
...
}
Enum의 associated value를 이용해서 해당 로그와 연관된 값을 지정할 수 있게 되었습니다. 두 개의 enum으로 관리했던 로그들을 하나의 enum으로 정리되었습니다.
Enum은 구조체와 클래스처럼 메서드와 프로퍼티를 선언할 수 있습니다. 그래서 로그의 이름과 메타 데이터들을 enum의 프로퍼티를 만들어서, 각 로그 케이들을 다 정리할 수 있도록 했습니다. Enum은 switch-case문과 같이 사용할 때 강력하기 때문에, switch-case문을 이용해서 모든 로그에 대해 빠짐없이 값을 설정할 수 있습니다.
extension Event {
var name: String {
switch self {
case .homeScreenView: return "홈"
case .productSelect: return "상품선택"
case .productScreenView: return "상품상세"
...
}
}
extension Event {
var metadata: [String: String] {
switch self {
case .homeScreeView:
return [:]
case .productScreenView(let product):
return ["상품이름": product.name]
}
}
}
로그 보내는 하나의 함수
이제 새롭게 정리한 로그들을 보낼 수 있는 함수를 만들어야 합니다. 이전에는 로그별 함수를 만들면서 이름과 메타 데이터를 파라미터로 받았었는데, 이제는 하나의 enum 케이스를 만든 로그를 받아서 처리하도록 변경해보았습니다.
// Enum으로 만든 Event 하나만 받으며
// 안에서 name과 metadata를 맞춰서 보내면 됩니다
func log(_ event: Event) {
analytics.sendAnalyticsEvent(name: event.name, metadat: event.metadat)
}
사용
각 로그별 만들어서 사용했던 함수들을 제거하고 새롭게 만든 log 함수를 적용해보면 코드 줄 수도 줄고 간편해진 것을 확인해볼 수 있었습니다.
class ViewModel {
private let analytics: AnalyticsManager
...
func bookmarkProduct() {
...
analytics.log(.bookmarkProduct(product: product))
}
func productSelected(at index: Int) {
...
analytics.log(.productSelect(index: index))
}
}
결론
코드 개선 관련해서 고민하면서 생각보다 스위프트의 enum이 굉장히 많은 도움을 준 것을 깨달았습니다. 특히나 목록으로 사용하거나 switch-case문과 같이 사용하면 굉장히 강력하게 사용할 수 있습니다. 물론 모든 문제를 해결할 수 있는 만능 개념이 아니기 때문에, 사용하려는 부분에 적합한지 확인해서 적용해보면 좋을 것 같습니다. 스위프트의 enum을 다시 보는 계기가 되거나 도움이 되었으면 좋겠습니다.