본문 링크 (Original Link)

스위프트 팁 : compactMap으로 배열 만들기

2018.08.12

# • #

by Jesse Squires, translated by pilgwon

객체들의 목록을 만들어서 사용자에게 보여주는 등의 행동은 앱 개발에서는 흔한 시나리오입니다. 그러기 위해서는 보여줄 데이터를 데이터베이스에서 가져오거나 인터페이스에 보여줄 수 있는 공간을 만들어야 합니다. iOS 달력 앱을 예로 들어 보겠습니다. 새로운 일정을 추가하려면 제목, 위치, 날짜와 시간, 메모 등을 채워넣을 폼이 나타납니다. 그러나 이미 존재하는 일정에서 여러분이 볼 수 있는 것은 채워진 필드뿐이고, 안채워진 필드는 숨겨져있습니다.

저는 그러한 시나리오와 비슷한 코드를 작성중입니다. PlanGrid에서는 라이브러리를 사용해서 테이블과 컬렉션을 위한 사용자 인터페이스를 선언적으로 빌드합니다. 테이블 또는 컬렉션이 구성된 뷰 모델 목록을 생성한다면 달력 앱은 다음과 같을 것입니다.

protocol ViewModelProtocol { /* ... */ }

func generateTitleViewModel() -> ViewModelProtocol { /* ... */ }

func generateStartDateViewModel() -> ViewModelProtocol { /* ... */ }

func generateEndDateViewModel() -> ViewModelProtocol { /* ... */ }

func allViewModels() -> [ViewModelProtocol] {
    return [
        generateTitleViewModel(),
        generateStartDateViewModel(),
        generateEndDateViewModel(),
        // ...
    ]
}

그런데 몇몇 테이블 셀이 필수가 아니라면 어떨까요? 지금 일정 화면이 새로운 일정을 만드는 화면인지 이미 존재하는 일정을 보여주는 화면인지에 따라 우리는 어떤 셀을 숨겨할 수도 있습니다.

그렇다면 다음과 같을 것입니다.

func generateLocationViewModel() -> ViewModelProtocol? { /* ... */ }

func generateAlertViewModel() -> ViewModelProtocol? { /* ... */ }

func generateNotesViewModel() -> ViewModelProtocol? { /* ... */ }

// Return type has to be an array of optionals
func allViewModels() -> [ViewModelProtocol?] {
    return [
        generateTitleViewModel(),
        generateStartDateViewModel(),
        generateEndDateViewModel(),
        generateLocationViewModel(),
        generateAlertViewModel(),
        generateNotesViewModel(),
        // ...
    ]
}

위 코드는 반환하는 배열이 옵셔널의 배열이기 때문에 최적의 코드는 아닙니다. 이는 객체 그래프를 복잡하게 만들기 때문에 우리가 원하는 방법이 아닙니다. 예를 들면 viewModels.count는 더 이상 테이블 셀의 수를 세는데에 적절하지 않습니다. 가능한 한 객체 그래프와 옵셔널을 멀리 유지하는 것이 좋습니다. 배열에 넣기 전에 nil 검사를 해서 이러한 문제를 피할 수 있습니다.

func allViewModels() -> [ViewModelProtocol] {
    var viewModels = [generateTitleViewModel()]

    if let locationModel = generateLocationViewModel() {
        viewModels.append(locationModel)
    }

    viewModels.append(generateStartDateViewModel())
    viewModels.append(generateEndDateViewModel())

    if let alertModel = generateAlertViewModel() {
        viewModels.append(alertModel)
    }

    if let notesModel = generateNotesViewModel() {
        viewModels.append(notesModel)
    }

    return viewModels
}

옵셔널을 삭제한 더 발전된 버전입니다. 작동은 하지만 몇 가지 문제가 있습니다. 각 상태를 다 체크해야하며 간단하지도 않습니다. 게다가 변수의 배열을 관리해야 합니다. 새로운 필드를 추가하거나 이미 존재하는 것의 필수여부를 바꾸려고 할수록 이 코드는 점점 더 유지하기 어려워지고 결국 에러로 이어질 것입니다. 이러한 방법 대신에 하나의 배열만 선언하고 compactMap()을 사용하여 옵셔널을 삭제하는 방법을 사용해보겠습니다.

func allViewModels() -> [ViewModelProtocol] {
    return [
        generateTitleViewModel(),
        generateLocationViewModel(),
        generateStartDateViewModel(),
        generateEndDateViewModel(),
        generateAlertViewModel(),
        generateNotesViewModel(),
        ].compactMap { $0 }
}

위 코드는 모델들을 간결하고 선언적이게 설명하며 순서대로 제공합니다. 어떤 함수가 nil 모델을 반환하면 compactMap()이 삭제해 줄 것입니다. 저는 이 방법이 우아하고 읽기 좋으며 유지하기에도 좋은 것을 알게되었습니다. compactMap()을 통해 우리는 이전 코드에 있던 조건문뿐만 아니라 그 코드를 이해하는데 드는 간접적인 부하까지 제거해버렸습니다. 위 코드는 필드의 순서가 어떤지 명확하게 보여줄 뿐만 아니라 더 이상 if-let 바인딩이 없고 함수를 옵셔널(optional) 또는 비 옵셔널(non-optional)로 반환해도 코드를 변경할 필요가 없을 것입니다.

다음 번에 예제와 같은 코드를 작성할 때는 compactMap()을 사용해서 단순화 할 수 있는지 고려해보세요.