제목: You Probably Don't Want enumerated

스위프트 표준 라이브러리에서 한가지 종종 오용되는 부분은 시퀀스(Sequence)의 enumerated()함수이다. 이 함수는 새로운 시퀀스를 만들어주는 데, 이 시퀀스는 원래 시퀀스의 각 요소와 그 요소에 해당되는 번호를 가진다.

enumerated()는 잘못 사용되고 있다. 이 함수는 각 요소에 번호를 제공하기 때문에 그 번호 문제에대한 쉬운 해결법이 될 수 있다. 그러나 이런 번호 문제는 더 나은 방법으로 해결될 수 있다. 그런 경우가 무엇인지 보자. 우리가 어떤 실수를 하고 있는지 보고, 이것을 추상적인 수준에서 한번 해결해보자.

enumerated()를 사용할때 주된 이슈는 이 함수가 요소와 그에 해당하는 각 인덱스(index)를 반환한다고 생각해버리는 것이다. 이 함수는 모든 시퀀스에서 사용가능하지만, 시퀀스는 인덱스를 가진다는 것을 보장하는 녀석이 아니기 때문에, 우리는 이것이 인덱스가 아니라는 것을 기억해야한다. 코드에서 index라 부르지않고 offset이라 부르고 있는데, 이 네이밍 컨벤션은 글 마지막에 소개해 놓았다. 오프셋(offset)은 항상 0에서 시작하여 각 요소마다 증가하는 정수형태를 말한다. Array의 경우 인덱스와 일치하겠지만, 기본적으로 다른 타입에서는 그렇지 않다. 아래 예제를 한번 보자.
let array = ["a", "b", "c", "d", "e"]
let arraySlice = array[2..<5]
arraySlice[2] // => "c"
arraySlice.enumerated().first // => (0, "c")
arraySlice[0] // fatalError
arraySlice라는 변수는 당연하게도 ArraySlice이다. 그러나 startIndex는 특별하게도 0이아닌 2이다. 이때 enumerated()first를 호출하면 오프셋이 0인 튜플을 반환해주고, 그 첫번째 요소인 "c"를 반환한다.

다른 방법으로 예시를 보자.
zip(array.indices, array)
실제로는 이렇게 한다.
zip((0..<array.count), array)
그리고 Array가 아닌 다른 것과 작업을 하면 언제든지 틀린 동작 결과를 만들 것이다.

enumerated()를 사용하면서 (인덱스가 아닌) 오프셋을 사용것으로 생긴 이슈 말고도 다른 이슈들이 있다. enumerated() 사용에대해 여러번 생각해볼 수 있는데, enumerated()를 사용할때 얻을 수 있는 더 나은 이점이 있다. 조금 더 살펴보자.

내가 본 enumerated()의 가장 일반적인 사용은, 다른 배열로부터 일치하는 요소를 잡기위해 열거된(enumerated) 배열로부터 오프셋으로 사용하는 것이다.
for (offset, model) in models.enumerated() {\
     let viewController = viewControllers[offset]
     viewController.model = model
}
이 코드는, 배열이된 modelsviewControllers에 의존하는데, 이 배열은 0에서 시작하고 정수에의해 색인(index)된다. 그리고 이 배열의 길이가 같다는 것에도 의존하고 있다. 만약 models 배열이 viewControllers 배열보다 짧다면, 별다른 나쁜일이 일어나지 않겠지만, viewControllersmodels보다 짧다면 크레쉬가 일어날 것이다. 또한 큰 역할을 하고 있지도 않은 추가적인 offset 변수까지 가지고 있어야한다. 더 스위프트한 방법으로 다시 짜보면 아래처럼 될 수 있다.
for (model, viewController) in zip(models, viewControllers) {
     viewController.model = model
}
이 코드는 읽는이를 집중시키며, 모든 Sequence 타입에서 동작한다. 또한 배열의 길이가 일치하지 않는 것도 알아서 처리해준다. 더 나은 방법일 것이다.

다른 예제를 보자. 이 코드는 첫번째 imageView와 그 컨테이너 사이에 오토레이아웃 제약(constraint)를 추가하고 쌍의 이미지뷰 사이의 오토레이아웃 제약을 만든다.
for (offset, imageView) in imageViews.enumerated() {
     if offset == 0 {
          imageView.leadingAnchor.constraint(equalTo: containerView.leadingAnchor).isActive = true
     } else {
          let imageToAnchor = imageView[offset - 1]
          imageView.leadingAnchor.constraint(equalTo: imageToAnchor.trailingAnchor).isActive = true
     }
}
이 코드 예제도 비슷한 문제가 있다. 우리는 쌍의 요소가 필요하지만, 고수준에서 작업할때 enumerated()를 사용하는 것은 지긋지긋한 인덱스를 다뤄가며 필요한 번호를 뽑아내야 한다는 의미이다. 이 부분도 마찬가지로 zip이 도와줄 것이다.

먼저 첫번째 요소에서 컨테이너 제약을 다루는 코드를 작성하자.
imageViews.first?.leadingAnchor.constraint(equalTo: containerView.leadingAnchor).isActive = true
다음으로 이 쌍의 요소를 다루자.
for (left, right) in zip(imageViews, imageViews.dropFirst()) {
     left.trailingAnchor.constraint(equalTo: right.leadingAnchor).isActive = true
}
이제 됐다. 인덱스는 보이지 않고, 어떠한 (다중-전달) 시퀀스로 동작하므로 더 집중할 수 있게 되었다.

(한 익스텐션에 쌍으로 만드는(pairing) 코드를 넣을 수도 있고, 필요에 따라서는 .eachPair()을 호출할 수도 있다.)

enumerated()의 몇몇 유효한 사용이 있을 수 있다. 여러분이 얻어내고 있는 것이 인덱스가 아니라 그냥 정수이기 때문에 각 요소에 해당하는 (인덱스가 아닌) 번호로 작업해야 할 때가 바로 옳바른 사용 시점이다. 예를들어 여러 뷰들의 각 수직 좌표 y를 높이와 시퀀스의 오프셋의 곱으로 만들어야 한다면 enumerated()가 적절할 것이다. 아래에 구체적인 예시가 있다.
for (offset, view) in views.enumerated() {
     view.frame.origin.y = offset * view.frame.height
}
여기의 offset은 번호의 속성으로 사용되고 있기때문에 enumerated()가 잘 동작한다.

이제 간단하게 요약해보자면, enumerated()를 인덱스로 사용하고 있다면 그 문제를 해열하는데 더 좋은 방법이 있을 것이며, enumerated()를 번호로 사용한다면 좋아요를 표시한다.



이 블로그는 공부하고 공유하는 목적으로 운영되고 있습니다. 번역글에대한 피드백은 언제나 환영이며, 좋은글 추천도 함께 받고 있습니다. 피드백은 

으로 보내주시면 됩니다.



WRITTEN BY
tucan.dev
개인 iOS 개발, tucan9389

트랙백  0 , 댓글  0개가 달렸습니다.