본문 바로가기

iOS/Deep dive!!!

[iOS] collectionView(_:cellForRowAt:)에서 중복 선언 및 호출되는 로직 간소화 | POP와 OOP, 프로토콜 composition

 

안녕하세요.

 

지난 포스트에서는 최근 진행중인 프로젝트에서 재사용 가능한 cell의 UI에서 특정 영역에만 UI가 서로 다른 경우, 하나의 cell에 데이터가 들어올 때마다 데이터에 맞게 레이아웃을 잡는 경우 vs 서로 다른 UI별로 cell을 만드는 경우 중 뭐가 성능상 좋은지 실험(관련 포스트 링크) 후 리빌딩 했었습니다.

 

이번 포스트에선 지난 포스트에서의 리빌딩 과정 중 collectionView(_:cellForRowAt:) 델리게이트에서 중복 코드가 너무 많이 보여서 프로토콜 지향적 + 객체지향 개념을 활용해 리빌딩한 경험을(리빌딩한 코드 관련 커밋 링크) 소개하려고 합니닷!!

프로젝트 구조 간단 소개


진행중인 프로젝트의 피드 화면입니다.

 

 

자세히 보면, 회색 영역은 ui가 같지만 이미지 영역은 서버에서 받아오는 이미지 개수에 따라 1~5가지 경우까지 ui 배치 구성이 다 다르게 되어 있습니다. 그래서 저는 초기에 1개의 cell로 구현했다가 추후에 5개의 cell로 리빌딩을 했습니다(리빌딩 전/후 ui 성능 비교 포스트 링크).

 

기본적으로 공통적인 코드들(레이아웃, 프로퍼티들 데이터 주입 함수등)은 BasePostView에 있고 BasePostView를 프로퍼티로 갖고 섬네일 레이아웃을 구체타입으로 갖고있는 PostCellWithOneThumbnail, PostCellWithTwoThumbnails ... PostCellWithFiveThumbnails 셀 서로 다른 5개가 존재합니다. 이때 PostCellWIth*Thumbnails는 UICollectionViewCell입니다.

 

 

대표적으로 PostCellWithFourThumbnails의 코드 구성은 다음과 같이 이루어져 있습니다.

 

그리고 나머지 PostCellWith One, Two... Five Thumbnails 객체의 코드 또한 thumbnails의 구체타입만 다 다르고 나머지는 동일한 코드 구조를 갖고 있습니다. (BasePostCell을 만든 후 자식 객체에서 init시점에 basePostCell의 지정 생성자를 호출하려했는데, 시스템은 init(frame:) 만 호출하더라구요..ㅠㅠ)

중복 호출 코드 및 로직이 많은 collectionView(_:cellForRowAt:)

그래서 collectionView의 데이터소스 함수 collectionVIew(_:cellForRowAt:) 함수에 데이터를 주입할 때 코드는 다음과 같았습니다.

 

엇ㅋㅋ,, 중복되는 코드가 엄청 많은게 보이시나요.

 

 

1. 등록된 cell의 구체 타입은 다르지만, cell을 선언한다
2. cell 인스턴스를 활용하는 로직이 같다.
3. cell을 반환한다.

 

그래서 개선하기로 했습니다!!

 

 

아래 더보기는 makePostCell 코드입니다.

더보기

func makePostCell(_:cellForItemAt:with:)

 

초기 collectionView(_:cellForRowAt:)시점의 switch 분기 처리는 case 의 대상이 1,2,3,4,5 ..숫자였는데, PR을 통해 했갈릴 수 있겠구나를 판단했고 더 명확하게 알 수 있도록 PostThumbnailCountValue타입을 만들었습니다.


짜잔!! 결과적으로 depth를 줄이고 중복되는 코드들을 많~이 단축시켰습니다.

 

이게 가능한 이유는 서로 다른 5개의 cell 중 한 인스턴스를 reusable dequeue로부터 꺼내오지만, 그럼에도 계속 강조해왔던 노란색 영역처럼 모든 switch's case 전부 공통적으로 같은 로직을 수행하기 때문입니다.

 

여기서 중요한 점은 protocol을 통해서 구체타입은 모르지만, 델리게이트 느낌처럼!! 51라인의 cell 인스턴스가 구체타입의 configure(with:)를 호출해준다는 것입니다. PostCellEdgeDividable 또한 마찬가지입니다. 그래서 dequeue로 꺼내올 때, PostCellWith... 객체들이 PostCellEdgeDividable, PostCellConfigurable을 준수할 떄, PostCellWith[One, Two...Five]의 class 상위 타입인 UICollectionVIew와(다형성) 구체타입을 몰라도 실행가능한 protocol들 (PostCellConfigurable, PostCellEdgeDividable) 의 조합으로 cell의 타입을 지정합니다.

 

그렇게 cell은 makePostCell(_:cellForItemAt:with:)는 런타임때 indexPath에 의해 특정 구체타입의 object 함수들을 수행할 수 있습니다.

 

여기서 "protocol이 왜,,?" 이해가 가지 않는다면 델리게이트 pattern을 복습하고 오면 좋을것 같습니다!! 

 

 

근데 여기서 가장 중요한 PostCellConfiguration은 사실 CellConfiguration protocol을 준수합니다.

 

protocol CellConfigurable {
  associatedtype Info
  func configure(with info: Info?)
}

 

그리고 collectionVIew(_:cellForRowAt:)에서 cell의 타입은 PostCellConfiguration이 아닌, CellConfiguration 타입으로 대체할 경우 문제가 발생됩니다....

 

Info타입이 cell타입의 Info타입이 되는데, 이때 CellConfigurable을 각각의 PostCellWith* 객체에서 제너릭으로 CellConfigurable's 연관타입을 제너릭으로 명확하게 지정 후 CellConfigurable을 준수한다고 해도 결국 cell타입은 ( any UICollectionVIew & CellConfigurable & PostCellEdgeDividable)?타입이기 때문에 CellConfigurable의 associatedType가 "컴파일 시점!"에 추론되기 어렵습니다.

 

// Cell타입이 (any UICollectionVIew & CellConfigurable & PostCellEdgeDividable)? 인 경우
{
  ...
  let cell = makePostCell(collectionView, cellForItemAt: indexPath, with: numberOfThumbnails)
  cell?.configure(with: postItem) // ERROR
  checkLastCell(cell, indexPath: indexPath)
  return cell ?? .init(frame: .zero)
}

 

컴파일 시점에는 cell이 어느 구체 타입인지 알 수 없기 때문입니다(any..) 그래서 associatedtype을 추론하지 못하는데,, 이때 제너릭을 사용해도 똑같습니다. 결국 제너릭을 사용했을땐 BasePostCell<>..을 만들어야 하지만, 위에서 소개했던것과 같이 thumbnailView의 구체타입을 init(frame:) 시점에 주입하지 못하는 아쉬움이 있습니다.

 

그럼 PostItem ( 타입: PostInfo ) 를 제너릭을 사용하지 않고 어떻게 명시적으로 대체할수있을까요?

 

(MVVM input/output을 소개할때도 사용했던) protocol에 where을 사용해서 해결했습니다.

 

protocol PostCellConfigurable: CellConfigurable where Info == PostInfo {}

 

 

이렇게 CellConfigurable을 준수할 때 associatedtype을 명확하게 where 키워드로 지정할 수 있습니다. (이때만 PostCellConfigurable을 사용할 수 있다!! == PostCellConfigurable을 사용할떈 associatedtype이 PostInfo-구체타입- 한정!!)

 

이번 주제 리빌딩 한 커밋 링크를 보면 중복되는 코드가 확 줄었다는 것을 알 수 있습니다 : ]

 

 

결론: collectionView(_:cellForRowAt:)등 reusable queue를 dequeued할 때 cell의 타입을 특정 구체타입으로 무조건 캐스팅할 수 있겠지만 중복되는 코드가 있을 경우 OOP + POP를 활용해 코드 길이를 단축할 수 있다는 것입니다.