Skip to content

Conversation

@0Hooni
Copy link
Member

@0Hooni 0Hooni commented May 11, 2025

📌 이슈

✅ 작업 사항

Note

PR이 고봉밥이라 죄송합니다 ㅠㅠ
조만간 라이브 리뷰하도록 하겠습니다!

1. 디자인 시스템 모듈화

현재 디자인시스템 컴포넌트들은 Presentation이 들고 있던 상황입니다.

하지만 SearchFeature를 모듈화하게 되면 공통 컴포넌트를 위해 Presentation 모듈을 의존하게 되고, 추 후 Presentation에 있는 Scene들 중에서 SearchFeature의 접근이 필요하게 되므로 의존성이 발생됩니다.

그렇게 되면 결국 둘 사이에 순환되는 의존이 발생하게 되기에, 이를 방지하고자 공통 컴포넌트를 갖고있을 DesignSystem 모듈이 필요하게 되었습니다.

현재의 DesignSystem은 단순히 Presentation에 있던 DesignSystem 폴더를 모듈화한 수준이고, 별다른 변경은 없습니다.

2. 검색 화면을 담당하는 모듈 구현

제가 처음 검색화면을 수정하는 과정을 겪으면서 느꼈던 문제점들이 있었습니다.

[FIX] 스크롤이 튕기는 문제 해결에서 우선적으로 문제를 해결했지만, 현재 검색 화면에서는 잦은 변경에서 매번 reloadData()로 View를 새롭게 부시고 그려야되는 문제점이 있었고, 이로 인해 스크롤 문제도 개선을 했지만 해결을 하진 못했습니다.

🔍 매번 다시 그리는 View

검색 화면에서는 변경이 잦습니다.

  • 키워드로 검색을 하면 상단 두개의 Section이 무너지고 보여지던 결과의 내용을 변경해서 보여줘야 했음.
  • 최근 검색어를 모두 지우면 최근 검색어 Section을 없애줘야 했음.
  • 카테고리의 변경에 따라 검색 결과는 지속적으로 업데이트 되어야 했음.
  • 필터(정렬방식, 팝업상태)에 따라 검색 결과가 업데이트 되어야 했음.

하지만 이전의 검색 화면에서는 매번 이렇게 변경되는 CollectionView의 Section을 다시 그리기 위해 reloadData()를 수행해주고 있습니다. 물론 일반적인 데이터소스에서는 이만한 방법이 없긴 합니다.

하지만 단순히 하나의 최근검색어 Tag만 지우거나, 카테고리 Tag만 지우는 등 목적에 대비해서 reloadData()를 이용하며 코스트가 심한 작업들도 존재했습니다.

🔍 View의 레이아웃을 ViewModel이 처리

이 부분에 대해서 많이 고민을 했었습니다.

현재 저희 프로젝트에서는 가장 크게 의존하고 있는 인터페이스가 있습니다. Sectionable입니다.

Sectionable은 여러개의 Section을 다양한 Layout으로 만들 수 있도록 이루어진 Compositional Layout를 보다 쉽게 구성하기 위한 인터페이스라 느꼈습니다.

다만 아쉬웠던 부분은 Sectionable과 Inputable의 의존성입니다.

ViewModel에서는 분명 뷰의 더 상세한 내용을 구성해줄 책임은 있습니다. 그리고 저희 프로젝트 대다수의 CompositionalLayout으로 이루어진 Cell의 내용을 완성해주기 위해서는 Inputable의 inject 메서드를 사용합니다.

하지만 이로 인해 결국 ViewModel에서는 Section과 Input 두가지 모두 갖고있어야 되는 상황이 발생했습니다.

View의 레이아웃은 과연 View의 정보인가 Model의 정보인가 고민을 좀 해봤지만 결국 View에 가까운 항목이라 판단하였습니다.

그리고 Sectionable은 Bottom-up 형태로 가장 하위 섹션부터 차근차근 뎁스를 갖고 올라오면서 최종적인 레이아웃을 구성하는데, 최종적으로 한 화면을 이루고 있는 레이아웃을 일괄적으로 파악하기에는 어려운 구조라고 생각했습니다.

💡LayoutFactory + DiffableDataSource

그래서 저는 이 두가지 문제를 해결하기 위해 두가지 대안책을 제시했습니다.

가장 먼저 변경이 잦은 화면을 위해 최적인 DiffableDataSource입니다. DiffableDataSource를 구성하는 Section과 SectionItem은 Hashable합니다. 그래서 DataSource는 이들을 내부적으로 비교하여, 변경이 필요한곳을 판단한 뒤 새롭게 View를 구성해줍니다.

또한 기존에 Section들을 Int값으로 raw하게 switch-case를 다루던 부분을 enum case로 구체화하여 직관적으로 화면이 어떻게 구성될지 보이도록 노력했습니다.

또한 LayoutFactory를 적용하여, 현재의 View가 어떻게 View를 구성하고 있는지에 대해 정리해주었습니다. 그래서 View는 그저 Fatory에게 만들어달라 요청만 하게 되죠.

Layout의 변경이 필요한 경우에는 그저 Factory만 수정해주면 됩니다.

💡 View와 ViewModel의 불분명한 경계 해소

현재 프로젝트 대다수에서는 ViewModel에게 Section의 레이아웃을 만들도록 하고, 또한 View의 이동을 위해 Controller를 직접적으로 전달받고 있었습니다.

이로 인해서 View와 Reactor는 직접적으로 따라가야 되는 상황이 발생하고, 변경에 취약한 부분이 있었습니다.

위에서 언급한 Layout의 분리와 별도의 Model을 두는 방식으로 둘 사이 결합도를 최대한 낮췄습니다.

이제 ViewModel은 비즈니스 로직만 수행하며, ViewController는 그저 사용자의 Action을 전달해주고, reactor의 state 변화에만 맞춰 View를 그려주기만 합니다.

3. 새로운 Custom modal 구현

기존에 PanModal을 이용한 화면에서 Modal을 띄우기 위한 별도의 Presenter를 만들었습니다.

화면을 구성하는 과정에서 PanModal이 내부 View의 사이즈를 정확히 계산해내지 못했던 문제인지 내부 View의 크기를 정확히 잡아줘도 Modal이 뜨지 않거나 뜨고 나서도 위치를 잘 못잡던 경우가 잦았습니다.

그래서 우선 패키지는 냅뒀지만 대체안을 제안하고자 새로운 PPModal을 만들어두었습니다.

기존에 PanModal을 사용하던 방식과 유사하게 구성하려 하였고, PanModalPresentable대신 PPModalPresentable을 채택해준 뒤 view.ppmodal을 해주면 끝입니다.

자세한 코드는 DesignSystem에 있는 UIViewController+.swift를 참고해주세요!

4. 모듈간 의존성 해소를 위한 인터페이스 및 팩토리 도입

가장 먼저 도입했던 디자인시스템에서도 언급했던 문제입니다.

모듈간 화면을 띄우기 위해서는 서로의 모듈에 있는 ViewController를 알아야했습니다. 하지만 그걸 위해 서로 의존하는 관계를 가지게 되면 의존성 순환이 발생하기 때문에 빌드를 할 수 없게됩니다.

그래서 Presentation 모듈과 SearchFeature 모듈이 서로의 화면을 띄우기 위한 방법이 필요했습니다.

그래서 각 모듈에 인터페이스 + 팩토리를 도입했습니다.

개념은 간단합니다. 화면의 생성은 팩토리가 담당하고, 화면이 필요한 곳에서는 그저 팩토리에서 make()를 실행시키면 됩니다.

여기서 더 나아가 팩토리는 구체적인 구현을 위해 해당 모듈 내부에 존재하고, 인터페이스 프레임워크를 추가해주어 인터페이스에서는 팩토리의 인터페이스를 정의해주었습니다.

이로서 두 모듈간에 직접적으로 의존하지 않고 오직 인터페이스만을 의존하여 순환 문제를 해결하였고, ViewController에서 주입해주어야 되는 여러 코드들도 뭉쳐둘 수 있게 됐습니다.

🚀 테스트 방식

검색 기능을 확인하기 위해서 그저 SearchFeatureDemo를 실행해주시거나, 북마크를 테스트하기 위해서 App을 실행한 뒤 평소와 같이 검색 화면으로 이동해주시면 됩니다.

검색 검색 취소 검색어 삭제
카테고리 선택 필터 + 페이지네이션

0Hooni added 30 commits May 2, 2025 10:25
- 별도의 DemoApp까지 추가
- 추후 diffableDataSource를 위함
- 하나의 CompositionalLayout과 diffableDataSource를 사용하는 방식으로 수정
- 다른 ViewController의 컨벤션과 동일하게 하나의 mainView를 사용하도록 함
- extension 변경으로 인한 관련 문제 대응
@0Hooni 0Hooni self-assigned this May 11, 2025
@0Hooni 0Hooni added 🔄 refactor 프로덕션 코드 리팩토링, 파일 삭제, 네이밍 수정 및 폴더링 🐛 fix 버그 수정, 잔잔바리 수정, 병합 시 충돌 해결 labels May 11, 2025
@0Hooni 0Hooni linked an issue May 11, 2025 that may be closed by this pull request
@0Hooni 0Hooni added the 🚀 feat 새로운 기능을 추가 label May 11, 2025
Copy link
Member

@dongglehada dongglehada left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

저어어어엉 마아아아아알~!!! 수고 많으셨습니다!!!!!!! 🙇 🙇🙇 궁금한 부분들은 댓글로 남겨두었고 View에 사용되는 상수들이 Constant로 빼기로 한 부분이 생각나서 체크해보면 좋을 것 같습니다! PR이 볼륨이 큰 만큼 빠르게 병합하는게 좋을 것 같습니다 :>

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

UserDefaultService를 key별로 사용할 수 있군요!!

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

저희가 사용하는 프로퍼티의 타입이 Int로 수정된 부분 확인하였습니다!


public func fetchCategoryList() -> Observable<[CategoryResponse]> {
let endPoint = CategoryAPIEndpoint.getCategoryList()
return provider.requestData(with: endPoint, interceptor: TokenInterceptor()).map { responseDTO in
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

tokenInterceptor를 class에서 생성하지 않고 사용할 때 마다 사용하는 이유가 궁금한데 이유를 알 수 있을까요?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

의존성 주입 개념으로 넣어두었고, 추후 인터페이스 추상화를 통해 주입해주면 좋겠다 싶어 우선적으로 저렇게 뒀었습니다☺️


let request = GetSearchPopupStoreRequestDTO(query: query)
let endPoint = SearchAPIEndPoint.getSearchPopUpList(request: request)
return provider.requestData( // 실패했을때는 키워드 저장이 안되도록 수정
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

해당 주석은 나중에 적용하기 위해 남겨주신 것 맞을까요?

Copy link
Member Author

@0Hooni 0Hooni May 15, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

아 지금은 do로 아래 체이닝을 해두어서 적용이 되어있는 상태입니다!

적용 당시 커밋은 해당 커밋을 참고해주시면 좋을거 같아요 ☺️

주석 제거의 경우는 해당 커밋에서 수정 적용하였습니다

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

개별 UseCase -> excute 의 형태!! 확인하였습니다

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

깔끔 스무스 ㅎ

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

뭔가 나중에는 PopupSearchLayout이 아니라 전역적으로 사용할 수 있는 LayoutProvider 같은느낌도 만들 수 있으면 좋을 것 같다고 생각이 들어요!! 지금 상태에서는 베리 구웃 입니다 :>

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

아 이부분도 매우 격하게 공감하고, 실제로 작업중에 있습니다 ☺️

앱 전역적으로 같은 레이아웃을 쓰는 곳들이 많기에 추후 디자인시스템에 레이아웃 구조체를 가져다 쓰기만 해도 되는 방향으로 만들어볼까 싶어요

case present(target: PresentTarget)
}

@frozen
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

프로즌.. 처음봤는데 변경하지 않을 때 사용하는.. 메모 메모..

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

아마 앱 기획이 바뀌지 않는한 변경되지 않을 케이스라 생각해서 선언해두었습니다.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

메모메모...22

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

.withUnretained(self)가 사용되지 않을 때에는 제거하는 방향도 좋을 것 같습니다 :>

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@dongglehada

현재 파일 전체에 리뷰를 주신것 같아서 확인해봤는데, 우선 Reactor에서는 모두 weak self가 필요한 상황으로 보여요!

혹시 언급주신 이유에 해당하는 코드에 표시해서 리뷰주시면 감사하겠습니다 👍

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

자체 구현해주신 Modal!! 확인하였습니다 :>

0Hooni added 5 commits May 15, 2025 13:23
- extension에서 제공되는 기본 identifiers 사용하도록 변경
- UICollectionViewCell은 UICollectionReusableView를 상속하는 객체
- UICollectionReusableView에 이미 identifier가 있으므로 UICollectionViewCell에서는 extension으로 별도로 identifier를 만들지 않아도 사용 가능함
- 추가로 Sectionable에서 사용하는 SupplementaryItem의 indentifier를 static 인스턴스를 가져다 쓰도록 수정
- 핵심이었는 TagCollectionHeaderView의 Identifier는 제거
- 사용하던 곳에서는 extension의 identifiers를 사용하도록 변경
Copy link
Contributor

@zzangzzangguy zzangzzangguy left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

전반적으로 Reactorkit 쓰신부분도 의도에 맞게 잘사용되있는것 같아보이고 팩토리패턴 적용하신부분도 보면서 많은 참고가 되었습니다 ... 수고하셨습니다 🙇‍♂️🙇‍♂️🙇‍♂️🙇‍♂️

case present(target: PresentTarget)
}

@frozen
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

메모메모...22

@0Hooni 0Hooni merged commit a6ca1bf into develop May 16, 2025
2 checks passed
@0Hooni 0Hooni deleted the feat/#131-search-feature-module branch May 16, 2025 14:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

🚀 feat 새로운 기능을 추가 🐛 fix 버그 수정, 잔잔바리 수정, 병합 시 충돌 해결 🔄 refactor 프로덕션 코드 리팩토링, 파일 삭제, 네이밍 수정 및 폴더링

Projects

None yet

4 participants