Skip to content

Conversation

@MinwooJe
Copy link
Member

@MinwooJe MinwooJe commented Mar 23, 2025

배경

스크린샷 2025-03-23 오후 6 28 34

#46 전체보기 화면을 구현합니다.
#45 메인화면에서 팝업스토어 셀 선택 시 상세화면으로 전환합니다.

작업 내용

1. 관심사 관련 도메인 모델 수정

관심사를 나타내는 도메인 모델인 InterestCategory는 아래 코드와 같이 데이터 레이어/ 프레젠테이션 레이어에 의존하고 있었습니다.
따라서 의존성을 제거하기 위해 리팩토링을 하였습니다.

enum InterestCategory: String {
    case fashion = "패션"                    // ⭐️ 프레젠테이션 레이어에 표시되는 텍스트 의존
    case beauty = "뷰티"
    ...

    init?(serverValue: String) {            // ⭐️ 서버가 주는 관심사 문자열에 생성자가 의존
        switch serverValue {
        case "FASHION": self = .fashion
        case "BEAUTY": self = .beauty
        ...
        }
    }
}

// MARK: - InterestCategory 서버 매핑
extension InterestCategory {
    var serverValue: String {
        switch self {                       // ⭐️ 서버가 주는 관심사 문자열에 의존
        case .fashion: return "FASHION"
        case .beauty: return "BEAUTY"
        ...
        }
    }
}

데이터 레이어 의존성 제거

InterestCategory가 데이터 레이어에 의존하고 있던 이유는 서버에서 내려준 영문 String을 카테고리 열거형으로 변환하기 위함입니다.
따라서 변환하는 로직을 도메인 모델인 InterestCategory가 수행하는 것이 아닌, DTO가 수행하도록 변경하였습니다.

우선 서버와 관심사를 주고받는 상황은 크게 다음 두 가지 상황입니다.

  • 회원가입 시 관심사 지정 -> 서버에게 관심사를 전송
  • 메인화면 데이터를 받아올 때 -> 서버로부터 관심사를 받아옴.

따라서 관심사 데이터는 서버에서 사용되는 영문으로 변경이 가능해야 되고, 서버에서 받은 영문을 InterestCategory로 변환이 필요합니다.

struct InterestCategoryDTO: Codable, Hashable {
    let category: String

    init(from entity: InterestCategory) {
        switch entity {
        case .fashion:
            category = "FASHION"
        case .beauty:
            category = "BEAUTY"
        ...
        }
    }
}

extension InterestCategoryDTO {
    func toEntity() -> InterestCategory? {
        return switch category {
        case "FASHION": InterestCategory.fashion
        case "BEAUTY": InterestCategory.beauty
        ...
        }
    }
}

위 코드와 같이 도메인 모델 -> DTO로의 변환은 생성자를 통해 수행하도록 하였고, DTO -> 도메인 모델 변환은 toEntity 메서드를 통해 수행하도록 구현하였습니다.

프레젠테이션 레이어 의존성 제거

도메인 모델인 InterestCategory를 프레젠테이션 레이어에 필요한 데이터(한글 String)로의 변환도 필요합니다.
따라서 CategoryMapper라는 static 객체를 통해 변환을 수행하도록 하였습니다.
InterestCategory외에도 메인 화면의 찜목록, 관심사, 지금 놓치면 안될 팝업스토어를 나타내는 PopupSectionCategory의 변환도 수행하도록 구현하였습니다.
주석을 나름 상세히 달아두었기에 자세한 설명은 생략하겠습니다!

struct CategoryMapper {
    /// PopupSectionCategory → 프레젠테이션용 문자열 변환
    /// - Parameter: 메인화면의 팝업 섹션 카테고리
    static func mapToTitle(_ category: PopupSectionCategory) -> String {
        switch category {
        case .todayRecommend: return ""
        case .userPick: return "찜 목록"
        case .userInterest(let interest): return mapToUserInterestedTitle(interest)
        case .closingSoon: return "지금 놓치면 안 될 팝업스토어"
        }
    }

    /// InterestCategory → 프레젠테이션용 문자열 변환
    /// - Parameter: 관심사 카테고리
    static func mapToUserInterestedTitle(_ interest: InterestCategory?) -> String {
        switch interest {
        case .fashion: return "패션"
        case .beauty: return "뷰티"
        ...
        }
    }

    /// 프레젠테이션용 문자열 → PopupSectionCategory 변환
    static func mapStringToPopupSectionCategory(_ title: String) -> PopupSectionCategory? {
        switch title {
        case "찜 목록": return .userPick
        case "지금 놓치면 안 될 팝업스토어": return .closingSoon
        default: return .userInterest(mapStringToInterestCategory(title))
        }
    }

    /// 프레젠테이션용 문자열 → InterestCategory 변환
    static func mapStringToInterestCategory(_ title: String) -> InterestCategory? {
        switch title {
        case "패션": return .fashion
        case "뷰티": return .beauty
        ...
        }
    }
}

2 전체보기 화면으로의 화면전환 구현

1) 헤더 탭의 전체보기 버튼을 이미지로 변경

스크린샷 2025-03-23 오후 6 13 55

헤더 탭의 전체보기 UI는 버튼으로 구현했습니다.
그러나 사용자가 편하게 조작하기 위해 버튼 보다 크기가 큰 헤더를 누를 때 전체 보기 화면으로 이동하는 것이 좋다고 생각했습니다.
따라서 헤더에 UITapGestureRecognizer를 추가하여 화면전환을 구현하였습니다.

그러나 버튼을 누르니 화면 전환을 발생하지 않는 이슈가 발생했습니다.
헤더의 서브 뷰인 UIButton이 터치 이벤트를 처리하여 UITapGestureRecognizer가 동작하지 않았기 때문입니다.

이를 해결하기 위해 아래 두 가지 방법을 떠올렸습니다.

  • 버튼에 터치 이벤트 발생 시 헤더로 이벤트를 전달하는 방법
  • 버튼에서 이미지로 변경하여 간단하게 처리

그러나 첫 번째 방법은 추가적인 코드와 불필요한 복잡성을 늘리는 것이라 생각하여, 아래 방식으로 간단하게 변경하였습니다.

테스트 방법

  • 메인화면에서 전체 보기, 셀 선택시 상세화면으로의 화면 전환이 정상적으로 동작하는지 확인
  • 전체 보기 화면이 정상적으로 동작하는지 확인

리뷰 노트

관심사 관련 도메인 모델 리팩토링을 진행하며 401c481 refactor: InterestCategory의 다른 레이어 의존성 제거 커밋에서 성훈님 작업 내용을 수정하였습니다!
convertInterestToEnglish에서 임시방편으로 하드 코딩으로 관심사 한글 -> 관심사 영문 변환을 진행했습니다.

CategoryMapper를 이용하여 제가 수정하려고 했으나, UseCase의 역할이 모호한 부분이 많아 성훈님 코드에 수정이 많이 발생하고 현재 작업하시는 내용과 충돌이 날까봐 우선 하드코딩으로 처리했습니다ㅠ 문제점만 간단히 아래에 작성해둘게요! (일단 돌아가긴 하니..)시간 나실 때 수정 부탁드리겠습니다~

문제점

  1. SignUpUseCase의 과도한 책임: SignUpUseCase가 프레젠테이션 레이어 모델 -> 도메인 레이어 모델 변환을 수행함.
  2. 도메인 모델을 거치지 않고, 프레젠테이션 레이어 모델 -> 도메인 레이어 모델로 바로 변환함.

프레젠테이션 레이어 모델(한글 관심사 문자열) -> 도메인 모델(Interest Category) -> 데이터 레이어 모델(DTO, 영문 카테고리 문자열) 단계로 변환이 수행되어야 하는데, 프레젠테이션 레이어 모델에서 데이터 레이어 모델로 바로 변환이 이루어지고 있습니다.

또한 지금 코드는 SingUpUseCase에서 DTO 변환을 수행하고 있습니다. UseCase가 도메인 로직에만 집중할 수 있도록, 레이어 간 모델 변경은 도메인 레이어가 아닌 다른 레이어에서 진행하는 것이 더 좋을 것 같습니다.

제 생각으로는 아래와 같은 방식이 적절할 것 같습니다.

  • SignUpSecondViewModel관심사 한글 -> 도메인 모델(Interest Category) 변환을 담당
  • SignUpUseCase가 레포지토리에게 도메인 모델(InterestCategory가 포함된 엔티티가 필요할 것 같네요)을 전달
  • 레포지토리가 도메인 모델 -> DTO 변환을 수행한 후 서버에 데이터 전송.

CategoryMapper.mapStringToInterestCategory() 메서드와 compactMap을 이용한다면 뷰 모델에서 간단히 변경할 수 있습니다.
해당 방식으로 수정한다면 SignUpUseCase에서 convertInterestToEnglish 메서드도 삭제해도 될 것 같습니다.

추가로 compactMap 사용 시 변환되지 않은 데이터가 있을 수 있기에 변환 전, 변환 후 데이터의 개수를 확인하는 방식의 검증도 하면 좋을 것 같습니다.
compactMap 관련해서 알아보시면 좋을 것 같애요!!

아마 저랑 성훈님 둘 다 클린 아키텍처를 막 도입할 당시의 코드라 의존성 부분에서 실수한 것 같네요ㅠ 저도 코드 리뷰할 때 잡아내지 못한 점이 조금 아쉽긴 합니다..ㅠ

나름대로 상세히 설명을 작성했는데, 혹시 이해가 안가시는 부분이 있다면 말씀해주세요! 그리고 클린 아키텍처 구조를 맞춘 다른 좋은 방식이 떠오르신다면 그 방식으로 진행해도 좋을 것 같습니다!!

MinwooJe added 28 commits March 12, 2025 15:05
도메인 모델인 InterestCategory가 데이터/프레젠테이션 레이어에 의존하는 구조를 변경하기 위해
- SignUpUseCase의 관심사 변환 로직 수정
- PopupListRepository의 관심사 변환 로직 수정
프레젠테이션 레이어에 위치시켰던 MainCategory를 엔티티로 이동.
userInterest에 연관값 추가
- 이름 변경: more -> Overview
- 관심사 카테고리가 항상 필요하므로 moreInterestPath 프로퍼티 삭제, 메서드만 사용
관심사의 엔티티 <-> 프레젠테이션 매핑
@MinwooJe MinwooJe added 🎨 Feature 기능 구현 👨‍🏭 Refactor 리팩토링 labels Mar 23, 2025
@MinwooJe MinwooJe self-assigned this Mar 23, 2025
Copy link
Contributor

@snughnu snughnu left a comment

Choose a reason for hiding this comment

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

문제점 확인했습니다.
구조를 다시 짜보고 수정해보겠습니다!

/// - Parameter: 메인화면의 팝업 섹션 카테고리
static func mapToTitle(_ category: PopupSectionCategory) -> String {
switch category {
case .todayRecommend: return ""
Copy link
Contributor

Choose a reason for hiding this comment

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

p5)
질문입니다! 이거는 일부러 빈 문자열을 리턴하는건가요?

Copy link
Member Author

Choose a reason for hiding this comment

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

넵 맞습니다!
오늘의 추천 팝업의 타이틀을 사용하는 곳이 없어서 빈 문자열을 return 하도록 했습니다!

@MinwooJe MinwooJe merged commit a1e2ba0 into GDSC-Popcorn:develop Mar 25, 2025
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

🎨 Feature 기능 구현 👨‍🏭 Refactor 리팩토링

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants