Skip to content

[Refactor] LoginViewModel에서 세션/인증 상태 분리 (SessionStore 도입)#39

Open
ddodle wants to merge 3 commits into
mainfrom
refactor/session-store
Open

[Refactor] LoginViewModel에서 세션/인증 상태 분리 (SessionStore 도입)#39
ddodle wants to merge 3 commits into
mainfrom
refactor/session-store

Conversation

@ddodle

@ddodle ddodle commented Jul 2, 2026

Copy link
Copy Markdown
Collaborator

✨ PR 유형

  • 코드 리팩토링

🛠️ 작업내용

LoginViewModel에 섞여 있던 폼 상태앱 전역 세션/인증 상태의 관심사를 분리했습니다.

  • SessionStore 신규 — 인증/세션 소유
    • isLoggedIn, userInfo, name
    • restore()(자동 로그인 복원) / login() / logout() / forceLogout()
    • 인증 UseCase 호출을 담당하고 실패는 throw로 알림 (로딩/에러 등 화면 표현 상태는 갖지 않음)
    • refresh 실패 콜백 배선을 이관
  • LoginViewModel 슬림화 — 폼 표현만 (loginId/password/isLoading/errorMessage), 로그인은 session.login()에 위임
  • ContentViewSessionStore@State로 소유, isLoggedIn으로 루트 분기, .task로 자동 로그인 복원을 루트에서 수행
  • LoginViewsession 주입받아 LoginViewModel@State로 생성, 기존 onAppear 제거

흐름

LoginView → LoginViewModel.login() → SessionStore.login() → UseCase → Repository → HTTP
              (로딩/에러 표현)          (인증 + isLoggedIn 전환)

📋 추후 진행 상황

  • 로그아웃/프로필 UI를 SessionStore에 연결
  • SessionStore 단위 테스트 (로그인/복원/강제 로그아웃 시나리오)

📌 리뷰 포인트

  • 관심사 분리 방향(세션=인증 로직·throw, 폼 VM=화면 표현)이 적절한지
  • 로그인 성공 후 fetchUser() 실패를 조용히 무시하도록 변경함 (이미 로그인은 성공, 프로필만 미로딩 → 비치명적 처리). 에러 노출은 추후 프로필 화면 관심사로 분리 예정
  • 자동 로그인 복원(restore)을 LoginView가 아닌 앱 루트(ContentView)에서 수행하도록 이동

✅ Checklist

  • 커밋 메시지 컨벤션에 맞게 작성했습니다
  • 유지-보수를 위해 주석처리를 잘 작성하였는가?

Closes #37

Summary by CodeRabbit

  • New Features
    • 앱 로그인 상태가 세션 기반으로 전환되어, 실행 시 이전 세션을 자동으로 복원합니다.
    • 로그인 화면이 세션을 기준으로 표시되도록 개선되었습니다.
  • Bug Fixes
    • 로그인/로그아웃 시 상태 전이가 더 일관되게 반영되며, 갱신 실패 시 안전하게 로그아웃됩니다.
  • Tests
    • 세션 복원, 로그인 성공/실패, 로그아웃 동작을 검증하는 테스트를 추가했습니다.

- SessionStore 신규: isLoggedIn/userInfo/name + restore/login/logout/forceLogout
  인증 UseCase 호출 담당, 실패는 throw. refresh-실패 콜백 배선 이관
- LoginViewModel: 폼 표현(loginId/password/isLoading/errorMessage)만 남기고
  로그인은 session.login()에 위임
- ContentView: SessionStore를 소유하고 isLoggedIn으로 루트 분기,
  .task로 자동 로그인 복원(restore)을 루트에서 수행
- LoginView: session 주입받아 LoginViewModel을 @State로 생성, onAppear 제거
@coderabbitai

coderabbitai Bot commented Jul 2, 2026

Copy link
Copy Markdown

Review Change Stack

Walkthrough

세션/인증 상태를 관리하는 SessionStore가 추가되고, LoginViewModel은 로그인 폼 상태만 담당하도록 변경되었다. ContentView, LoginView, 앱 진입점, 탭 계층, DI 구성이 새 세션 흐름에 맞게 갱신되었으며, 관련 테스트와 목 구현이 추가되었다.

Changes

세션 분리 리팩토링

Layer / File(s) Summary
SessionStore 신규 구현
Rephoto_iOS/Features/User/Presentation/Session/SessionStore.swift
SessionStore가 토큰 복원, 로그인, 로그아웃, 강제 로그아웃, 사용자 정보 갱신을 담당하고 refresh 실패 시 강제 로그아웃을 수행한다.
LoginViewModel의 세션 위임 전환
Rephoto_iOS/Features/User/Presentation/ViewModels/LoginViewModel.swift
LoginViewModelSessionStore를 주입받고, 폼 상태와 session.login(id:password:) 호출 중심으로 동작하도록 바뀌었다.
ContentView/LoginView 세션 연동
Rephoto_iOS/App/ContentView.swift, Rephoto_iOS/Features/User/Presentation/Views/LoginView.swift, Rephoto_iOS/App/Rephoto_iOSApp.swift, Rephoto_iOS/Core/DIContainer/AppContainer.swift, Rephoto_iOS/Features/RephotoTabView.swift
루트 뷰 분기와 로그인 뷰 생성이 SessionStore 기준으로 바뀌고, 앱 전역 세션 등록 및 탭 환경 주입이 추가되었다.
SessionStore/LoginViewModel 테스트 및 목 객체
Rephoto_iOSTests/SessionStoreTests.swift, Rephoto_iOSTests/LoginViewModelTests.swift, Rephoto_iOSTests/MockUserUseCaseProvider.swift
세션 복원, 로그인, 로그아웃, refresh 실패, 폼 검증을 위한 목과 테스트가 추가되었다.

Estimated code review effort: 4 (Complex) | ~45 minutes

Possibly related PRs

Poem

나는 토끼, 세션을 깔끔히 정리해요
로그인 폼은 가볍게, 전역 상태는 단단히요
탭 숲에는 environment를 살짝 심고
복원과 로그아웃도 폴짝폴짝 이어가요
테스트 당근도 냠냠, 참 든든하네요 🥕

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed 제목이 SessionStore 도입을 통한 로그인/세션 상태 분리라는 핵심 변경을 정확히 요약합니다.
Description check ✅ Passed 작업내용, 추후 진행 상황, 리뷰 포인트, 체크리스트가 템플릿의 핵심 섹션을 모두 포함합니다.
Linked Issues check ✅ Passed SessionStore 도입, LoginViewModel 슬림화, ContentView/TabView/DI 연결이 #37의 요구사항을 충족합니다.
Out of Scope Changes check ✅ Passed 테스트와 앱/DI/탭 연결 변경도 세션 분리 목적에 직접 연결되어 보여지는 이탈 변경은 없습니다.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch refactor/session-store

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@ddodle ddodle self-assigned this Jul 2, 2026
- MockUserUseCaseProvider: UseCase 결과 stub + 호출 기록 spy
- SessionStoreTests: restore/login/logout/리프레시 실패 콜백 8케이스
- LoginViewModelTests: 입력 검증 및 isLoading/errorMessage 상태 전이 4케이스

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
Rephoto_iOS/Features/User/Presentation/ViewModels/LoginViewModel.swift (1)

13-14: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

가이드라인상 "ViewModel은 UseCase에만 의존" 규칙과의 정합성 확인.

LoginViewModel이 이제 UseCase가 아닌 SessionStore(Presentation 레이어 객체)에 의존합니다. PR 목표상 폼 상태/세션 상태 분리를 위한 의도된 설계이지만, 코딩 가이드라인은 "ViewModel depends only on UseCase"를 명시하고 있어 문자 그대로는 위반입니다. SessionStore를 Domain 레이어의 공유 유스케이스/서비스로 재정의하거나, 가이드라인에 이 패턴(세션 스토어 위임)에 대한 예외를 명시하는 것을 검토해주세요.

As per coding guidelines, **/*.swift: "Maintain Clean Architecture + MVVM pattern with clear separation: Presentation → Domain ← Data layers, where ViewModel depends only on UseCase, and Data implements Domain Protocol".

Also applies to: 24-26, 38-38

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Rephoto_iOS/Features/User/Presentation/ViewModels/LoginViewModel.swift`
around lines 13 - 14, LoginViewModel currently depends on SessionStore, which
conflicts with the “ViewModel depends only on UseCase” guideline. Update
LoginViewModel to depend on a Domain-layer abstraction instead of the
Presentation-layer SessionStore, or move SessionStore behind a UseCase/service
that the ViewModel can inject; if this dependency is intentional, document the
exception in the MVVM/Clean Architecture guideline and align the initializer and
session-handling members in LoginViewModel accordingly.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@Rephoto_iOS/Features/User/Presentation/ViewModels/LoginViewModel.swift`:
- Around line 13-14: LoginViewModel currently depends on SessionStore, which
conflicts with the “ViewModel depends only on UseCase” guideline. Update
LoginViewModel to depend on a Domain-layer abstraction instead of the
Presentation-layer SessionStore, or move SessionStore behind a UseCase/service
that the ViewModel can inject; if this dependency is intentional, document the
exception in the MVVM/Clean Architecture guideline and align the initializer and
session-handling members in LoginViewModel accordingly.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 49159f28-fc7a-4924-82ff-690947c0644e

📥 Commits

Reviewing files that changed from the base of the PR and between 8647a9c and 07fd387.

📒 Files selected for processing (7)
  • Rephoto_iOS/App/ContentView.swift
  • Rephoto_iOS/Features/User/Presentation/Session/SessionStore.swift
  • Rephoto_iOS/Features/User/Presentation/ViewModels/LoginViewModel.swift
  • Rephoto_iOS/Features/User/Presentation/Views/LoginView.swift
  • Rephoto_iOSTests/LoginViewModelTests.swift
  • Rephoto_iOSTests/MockUserUseCaseProvider.swift
  • Rephoto_iOSTests/SessionStoreTests.swift

- AppContainer: sessionStore singleton 등록 (@mainactor 타입이라 assumeIsolated로 해석)
- ContentView: 직접 생성 대신 @injected 주입으로 전환
- Rephoto_iOSApp: userProvider 전달 배선 제거
- RephotoTabView: 세션 주입 + .environment로 하위 탭에 공급
@Grit-23 Grit-23 deleted a comment from coderabbitai Bot Jul 3, 2026
@ddodle

ddodle commented Jul 3, 2026

Copy link
Copy Markdown
Collaborator Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 3, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
Rephoto_iOS/App/ContentView.swift (1)

14-23: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

초기 렌더링 시 로그인 화면이 잠깐 노출될 수 있음.

isLoggedIn의 기본값이 false이므로, 저장된 토큰이 있어 자동 로그인이 될 사용자라도 restore()가 완료되기 전까지는 LoginView가 먼저 그려집니다. 세션 복원 중임을 나타내는 로딩 상태(예: 스플래시/로딩 뷰)를 두면 깜빡임을 피할 수 있습니다.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Rephoto_iOS/App/ContentView.swift` around lines 14 - 23, Initial render can
briefly show LoginView because ContentView switches on session.isLoggedIn before
session.restore() finishes. Update ContentView to account for a
loading/restoring state in the Session flow, and have body render a
splash/loading view while restore() is in progress instead of immediately
choosing between RephotoTabView and LoginView. Use the existing
session.restore() task and the session/isLoggedIn logic to add a dedicated
restoring flag or equivalent state so the authenticated UI only appears after
restoration completes.
🧹 Nitpick comments (2)
Rephoto_iOS/Core/DIContainer/AppContainer.swift (1)

83-93: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

assumeIsolated 대신 Factory를 @MainActor로 선언하는 것을 권장.

SessionStore@MainActor 타입이라는 전제를 런타임에 MainActor.assumeIsolated로 강제하고 있습니다. Factory 라이브러리는 이런 경우를 위해 팩토리 프로퍼티 자체를 @MainActor로 선언하는 방식을 공식적으로 지원하며, Swift 공식 문서도 "assume"보다 액터 어노테이션을 우선하도록 권장합니다. 현재 코드 경로(View의 body)에서는 크래시 가능성이 낮지만, 추후 백그라운드 컨텍스트나 예정된 SessionStoreTests.swift에서 이 팩토리를 해석(resolve)할 경우 assumeIsolated는 컴파일 타임이 아닌 런타임 크래시로 이어집니다.

♻️ 제안하는 리팩토링
-    var sessionStore: Factory<SessionStore> {
-        self {
-            MainActor.assumeIsolated {
-                SessionStore(provider: self.userUseCaseProvider.resolve())
-            }
-        }.singleton
-    }
+    `@MainActor`
+    var sessionStore: Factory<SessionStore> {
+        self {
+            SessionStore(provider: self.userUseCaseProvider.resolve())
+        }.singleton
+    }

Web search 결과, Factory 저장소 문서는 Factory also works with Observation, @MainActor and actor isolation in Swift concurrency, and you just annotate the Factory as needed.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Rephoto_iOS/Core/DIContainer/AppContainer.swift` around lines 83 - 93, The
sessionStore Factory currently relies on MainActor.assumeIsolated to enforce
main-actor access at runtime, which can crash when resolved off the main actor.
Update the sessionStore property in AppContainer to be annotated with `@MainActor`
instead of wrapping SessionStore creation in assumeIsolated, and keep the
Factory singleton setup intact. This lets Factory and the SessionStore type
express actor isolation at compile time and avoids runtime isolation
assumptions.
Rephoto_iOS/Features/RephotoTabView.swift (1)

13-13: 📐 Maintainability & Code Quality | 🔵 Trivial | 🏗️ Heavy lift

SessionStoreFeatures/User에 위치해 교차 Feature 의존을 유발할 수 있음.

RephotoTabViewFeatures/User/Presentation/Session/SessionStore를 직접 주입받습니다. 코딩 가이드라인상 Feature 모듈 간 직접 의존은 금지되고 Core 계층을 통해야 합니다. RephotoTabView가 이미 Features/Home, Features/Search의 UseCaseProvider를 주입받는 기존 패턴을 따르고 있어 이 변경 자체가 새로운 문제는 아니지만, 이제 앱 전역 세션 상태로 격상된 SessionStore는 장기적으로 Core 계층으로 옮기는 편이 가이드라인 의도에 더 부합합니다.

As per coding guidelines, "Feature modules must not depend on other Feature modules; all inter-feature communication must go through Core layer."

Also applies to: 28-29

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Rephoto_iOS/Features/RephotoTabView.swift` at line 13, RephotoTabView is
directly injecting SessionStore from Features/User, creating a cross-Feature
dependency that violates the module boundary rule. Move SessionStore into the
Core layer (or expose it through a Core abstraction) and update RephotoTabView
to depend on that Core-owned interface instead of Features/User. Keep the
existing injection pattern used in RephotoTabView for other providers, but
replace the session dependency with a Core-based symbol so inter-feature
communication stays out of Feature modules.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@Rephoto_iOS/App/ContentView.swift`:
- Around line 14-23: Initial render can briefly show LoginView because
ContentView switches on session.isLoggedIn before session.restore() finishes.
Update ContentView to account for a loading/restoring state in the Session flow,
and have body render a splash/loading view while restore() is in progress
instead of immediately choosing between RephotoTabView and LoginView. Use the
existing session.restore() task and the session/isLoggedIn logic to add a
dedicated restoring flag or equivalent state so the authenticated UI only
appears after restoration completes.

---

Nitpick comments:
In `@Rephoto_iOS/Core/DIContainer/AppContainer.swift`:
- Around line 83-93: The sessionStore Factory currently relies on
MainActor.assumeIsolated to enforce main-actor access at runtime, which can
crash when resolved off the main actor. Update the sessionStore property in
AppContainer to be annotated with `@MainActor` instead of wrapping SessionStore
creation in assumeIsolated, and keep the Factory singleton setup intact. This
lets Factory and the SessionStore type express actor isolation at compile time
and avoids runtime isolation assumptions.

In `@Rephoto_iOS/Features/RephotoTabView.swift`:
- Line 13: RephotoTabView is directly injecting SessionStore from Features/User,
creating a cross-Feature dependency that violates the module boundary rule. Move
SessionStore into the Core layer (or expose it through a Core abstraction) and
update RephotoTabView to depend on that Core-owned interface instead of
Features/User. Keep the existing injection pattern used in RephotoTabView for
other providers, but replace the session dependency with a Core-based symbol so
inter-feature communication stays out of Feature modules.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 1772ef4f-9edb-47b5-8932-22cb3b4b21fd

📥 Commits

Reviewing files that changed from the base of the PR and between 07fd387 and c1db7ce.

📒 Files selected for processing (4)
  • Rephoto_iOS/App/ContentView.swift
  • Rephoto_iOS/App/Rephoto_iOSApp.swift
  • Rephoto_iOS/Core/DIContainer/AppContainer.swift
  • Rephoto_iOS/Features/RephotoTabView.swift

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.

♻️ Refactor: LoginViewModel에서 세션/인증 상태 분리 (SessionStore 도입)

1 participant