Skip to content

refactor: View/ViewModel 책임 분리 + PullToRefresh 성능 개선 - #106

Merged
gomminjae merged 2 commits into
developfrom
refactor/mvvm-view-viewmodel-separation
Jun 17, 2026
Merged

gomminjae merged 2 commits into
developfrom
refactor/mvvm-view-viewmodel-separation

Conversation

@gomminjae

Copy link
Copy Markdown
Collaborator

Summary

  • View에 남아 있던 비즈니스 로직·상태 판정을 각 Feature ViewModel로 이전 (BrandDetail, Subscribe, Explore, MyPage, Search, Bookmark)
  • PullToRefreshView를 GeometryReader + 매 프레임 DispatchQueue.main.async 패턴에서 onScrollGeometryChange 기반으로 전환 — 버벅임 제거 및 짧은 콘텐츠 동작 보장

Details

View → ViewModel 이전

  • BrandDetail: View의 모델 직접 mutate 제거 (pause/resume VM 인텐트), 매직넘버 321 → requiresSpecialSubscribeFlow
  • Subscribe: subscribeState·filteredSubscriptions·새로고침 throttle → VM
  • Explore: 6개 트리거의 isGuest 분기 + fetch → VM 인텐트 메서드, isLoaded → VM isInitialLoaded
  • MyPage: View의 store 직접 접근(userInfo @State) 제거 → VM displayNickname / displaySubscribeEmail
  • Search: 에러 채널 중복(errorMessage / popularErrorMessage String) → AppError 영역 상태로 정리
  • Bookmark: sortedCategories·빈 상태 판정 → VM computed
  • Subscribe/Explore VM에 appState 주입 (isGuest 판정용, default .shared)

PullToRefresh 성능

  • contentOffset 기반 안정적 오버스크롤 측정
  • scrollBounceBehavior(.always)로 짧은 콘텐츠도 동작
  • 인디케이터 페이드 전환

Test plan

  • BrandDetail: 구독/해지 플로우, 특수 brandID(321) 분기 동작 확인
  • Subscribe: 카테고리 필터링, 당겨서 새로고침 throttle 동작
  • Explore: 게스트/로그인 상태별 진입·새로고침·검색 분기
  • MyPage: 닉네임·이메일 표시 (로그인/게스트)
  • Search: 인기검색어/검색결과 에러 상태 표시
  • Bookmark: 빈 상태, 카테고리 정렬
  • PullToRefresh: 긴/짧은 리스트 모두 동작, 60fps 유지

🤖 Generated with Claude Code

GeometryReader + 매 프레임 DispatchQueue.main.async @State 난타 제거(버벅임 원인).
contentOffset 기반 안정적 오버스크롤 측정 + scrollBounceBehavior(.always)로
짧은 콘텐츠도 동작 + 인디케이터 페이드 전환.
- BrandDetail: View의 모델 직접 mutate 제거(pause/resume VM 인텐트), 매직넘버 321 → VM requiresSpecialSubscribeFlow
- Subscribe: subscribeState·filteredSubscriptions·새로고침 throttle → VM
- Explore: 6개 트리거의 isGuest 분기+fetch → VM 인텐트 메서드, isLoaded → VM isInitialLoaded
- MyPage: View의 store 직접 접근(userInfo @State) 제거 → VM displayNickname/displaySubscribeEmail
- Search: 에러 채널 중복(errorMessage/popularErrorMessage String) → AppError 영역 상태로 정리
- Bookmark: sortedCategories·빈상태 판정 → VM computed
- Subscribe/Explore VM에 appState 주입(isGuest 판정용, default .shared)
@gomminjae
gomminjae merged commit a6b2612 into develop Jun 17, 2026
1 check passed
@gomminjae
gomminjae deleted the refactor/mvvm-view-viewmodel-separation branch June 17, 2026 12:32

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Code Review

This pull request refactors several views and view models across multiple modules (DesignSystem, Bookmark, Detail, Explore, MyPage, Search, and Subscribe) to improve separation of concerns by moving UI state and business logic into the view models. Key feedback points out that using onScrollGeometryChange in PullToRefreshView requires iOS 18.0+, which may cause compilation issues on older deployment targets. Additionally, it is recommended to mark handleOverscroll with @MainActor for concurrency safety, and to remove duplicated cooldown logic and the redundant canRefresh method from SubscribeViewModel.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment on lines +46 to +50
.onScrollGeometryChange(for: CGFloat.self) { geometry in
geometry.contentOffset.y + geometry.contentInsets.top
} action: { _, offset in
handleOverscroll(-offset)
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

high

onScrollGeometryChange(for:action:) is only available in iOS 18.0 and later. If the project's deployment target is lower than iOS 18.0, this will cause a compilation error.\n\nIf the app supports iOS 17 or earlier, you should wrap this modifier in an #available check or provide a fallback implementation (such as the previous GeometryReader approach) for older iOS versions.

.accessibilityHint("당겨서 새로고침")
}

private func handleOverscroll(_ distance: CGFloat) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

Mark handleOverscroll with @MainActor to ensure that state mutations (such as isRefreshing, hasTriggered, and lastRefreshTime) are safely performed on the main thread. This prevents potential data races and adheres to Swift 6 strict concurrency guidelines.

    @MainActor\n    private func handleOverscroll(_ distance: CGFloat) {

Comment on lines +40 to +41
private var lastRefreshTime = Date.distantPast
private let refreshCooldown: TimeInterval = 2.0

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

The cooldown logic is now duplicated between PullToRefreshView (which has its own cooldownInterval and lastRefreshTime tracking) and SubscribeViewModel. This redundancy can cause UI bugs where the pull-to-refresh animation triggers but the actual data refresh is silently ignored because the ViewModel's cooldown (2.0s) is longer than the View's cooldown (1.5s).\n\nWe should remove the cooldown state and logic from SubscribeViewModel and instead pass cooldownInterval: 2.0 directly to PullToRefreshView in SubscribeView.

Comment on lines +77 to 83
public func canRefresh(referenceDate: Date = Date()) -> Bool {
guard referenceDate.timeIntervalSince(lastRefreshTime) >= refreshCooldown else {
return false
}
lastRefreshTime = referenceDate
return true
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

Remove the redundant canRefresh method as the cooldown throttling should be handled entirely by PullToRefreshView.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant