본문 바로가기
감정일기(가칭)

빈 상태 UI(Placeholder)와 Combine 상태 설계 .assign에서 .sink로 전환해야 하는 정확한 순간

by 밤새는 탐험가89 2025. 12. 16.
728x90
SMALL

1️⃣ 문제 상황 정리 

앱을 처음 실행했을 때:

  • 감정일기 ❌
  • 주간 감정일기 ❌
  • “이번 주 랜덤 일기 1개”를 보여주려 했지만
  • 보여줄 데이터 자체가 없음

이때 요구사항은 이거다:

  1. 데이터가 있으면 → 랜덤 1개 표시
  2. 데이터가 없으면 → 플레이스홀더 뷰 표시

👉 이건 더 이상 “데이터만 관리”하는 문제가 아니다.

 

2️⃣ 핵심 판단 기준 (아주 중요)

❓ 이 화면의 상태는
“데이터 하나”로 표현 가능한가?

  • [Diary] 하나면 충분한가? ❌
  • “없다” + “그래서 placeholder 보여준다” → ❌

이 순간부터 상태는 2개 이상이다:

  • randomDiary
  • isEmpty

👉 이 시점이 바로 .assign을 내려놓는 지점

 

3️⃣ 왜 .assign이 부족해지는가?

기존 .assign 패턴

.assign(to: \.randomDiary, on: self)

 

문제는 이거다:

  • 데이터 ❌ → nil
  • 근데 UI는:
    • placeholder 보여야 함
    • 애니메이션 돌릴 수도 있음
    • CTA 버튼 노출할 수도 있음

nil 하나로는
UI 상태를 설명하기엔 정보가 부족하다

 

4️⃣ 그래서 .sink로 “상태 묶음”을 관리한다

✅ 추천 패턴: 상태를 함께 계산

store.diariesPublisher
    .map { diaries -> (EmotionDiaryModel?, Bool) in
        let weekly = diaries.filter { $0.isInThisWeek }
        if weekly.isEmpty {
            return (nil, true)
        } else {
            return (weekly.randomElement(), false)
        }
    }
    .sink { [weak self] diary, isEmpty in
        self?.randomDiary = diary
        self?.isEmpty = isEmpty
    }
    .store(in: &cancellables)

 

여기서 중요한 점

  • 상태 판단은 map에서 끝냄
  • sink는 오직 “대입”만
  • UI 판단 로직 ❌ (ViewController에서)

 

5️⃣ 이 패턴의 진짜 장점

✔️ 1. “빈 상태”가 명확해진다

isEmpty == true
 

→ “아직 작성된 감정일기가 없음”

이건:

  • 에러 ❌
  • 예외 ❌
  • 정상 상태 ✅

 

✔️ 2. 플레이스홀더 UI가 자연스럽다

ViewController에서는:

viewModel.$isEmpty
    .sink { [weak self] isEmpty in
        self?.placeholderView.isHidden = !isEmpty
        self?.contentView.isHidden = isEmpty
    }

 

👉 조건 분기 없는 UI 반응

 

✔️ 3. 확장에 강하다

나중에 이런 요구가 생겨도:

  • “첫 일기 작성 유도 CTA”
  • “샘플 감정일기 보여주기”
  • “최근 7일 기준으로 변경”

👉 map만 수정하면 된다.

 

6️⃣ 그럼 언제 다시 .assign으로 돌아갈까?

상태가 하나로 수렴할 때

예를 들어:

  • 날짜 선택 → 일기 목록
  • 검색 결과 → 리스트
.assign(to: \.diaries, on: self)

 

하나의 상태 = assign

 

7️⃣ 상태 설계 관점에서 한 단계 더 나아가면

상태를 enum으로 표현하는 방법 (고급)

enum HomeWeeklyState {
    case empty
    case content(EmotionDiaryModel)
}
store.diariesPublisher
    .map { diaries -> HomeWeeklyState in
        let weekly = diaries.filter { $0.isInThisWeek }
        return weekly.randomElement().map(HomeWeeklyState.content)
            ?? .empty
    }
    .assign(to: &$weeklyState)

 

마지막으로, 이 한 문장만 기억해

.assign vs .sink의 기준은
데이터 유무가 아니라, 상태의 개수다.

 

728x90
LIST