Skip to content

[REFACTOR] 피드 메인 화면에서 스크롤 및 데이터 최신화 문제 해결#122

Merged
Nico1eKim merged 1 commit intoTHIP-TextHip:developfrom
rbqks529:refactor/#102_QA_4
Aug 25, 2025
Merged

[REFACTOR] 피드 메인 화면에서 스크롤 및 데이터 최신화 문제 해결#122
Nico1eKim merged 1 commit intoTHIP-TextHip:developfrom
rbqks529:refactor/#102_QA_4

Conversation

@rbqks529
Copy link
Collaborator

@rbqks529 rbqks529 commented Aug 21, 2025

➕ 이슈 링크

  • closed #이슈넘버

🔎 작업 내용

  • 어떤 부분이 구현되었는지 설명해주세요

📸 스크린샷


😢 해결하지 못한 과제

  • [] TASK


📢 리뷰어들에게

  • 참고해야 할 사항들을 적어주세요

Summary by CodeRabbit

  • 신기능
    • 프로필 화면에서 돌아올 때, 전체 새로고침 대신 최근 작성자만 갱신하도록 화면 복귀 흐름을 개선했습니다.
    • 초기 진입 시 조건에 따라 전체 새로고침 후 상단으로 스크롤합니다.
    • 피드 삭제 후 화면 복귀 시 목록에서 해당 피드가 즉시 반영되도록 처리했습니다.
    • 당겨서 새로고침 시 최근 작성자도 함께 갱신됩니다.
  • 스타일
    • 소소한 포맷 정리 및 공백 수정.

@coderabbitai
Copy link

coderabbitai bot commented Aug 21, 2025

Walkthrough

백스택 SavedState 기반 처리 추가: 삭제된 피드 ID 관찰 및 정리, 프로필에서 복귀 여부(from_profile) 플래그로 초기 로딩 분기. 프로필 진입 시 플래그 설정. 초기 진입/복귀 시 전체 새로고침 vs 최근 작성자만 갱신 흐름이 분기됨. ViewModel의 fetchRecentWriters 공개화 및 pullToRefresh에 포함.

Changes

Cohort / File(s) Change Summary
Feed 화면 상태/내비게이션 연동
app/src/main/java/com/texthip/thip/ui/feed/screen/FeedScreen.kt
SavedStateHandle을 통해 deleted_feed_id 관찰 후 제거 처리 추가. from_profile 플래그 도입 및 처리: 초기 로드/복귀 시 전체 새로고침 vs 최근 작성자 갱신 분기. 프로필 내비게이션 전 from_profile=true 설정. 경미한 포맷 수정.
피드 ViewModel 갱신 흐름
app/src/main/java/com/texthip/thip/ui/feed/viewmodel/FeedViewModel.kt
pullToRefresh에 fetchRecentWriters 호출 포함. fetchRecentWriters 가시성을 private→public으로 변경. 기타 경미한 정리/포맷 변경.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor User as 사용자
  participant FeedUI as FeedScreen
  participant Nav as NavController/SavedState
  participant VM as FeedViewModel

  rect rgba(200,230,255,0.25)
  note over FeedUI,VM: 초기 진입 또는 복귀 처리 (from_profile 플래그)
  FeedUI->>Nav: savedState.get(from_profile)
  alt from_profile == true or updated_feed_id 존재
    FeedUI->>VM: fetchRecentWriters()
    FeedUI->>Nav: remove(from_profile)
  else 초기 진입(프로필 아님)
    FeedUI->>VM: 전체 새로고침(탭 피드 새로고침)
    FeedUI->>FeedUI: 스크롤 상단 이동
  end
  end

  rect rgba(220,255,220,0.25)
  note over FeedUI,Nav: 프로필로 이동 시 원점 표시
  User->>FeedUI: 작성자 프로필 탭
  FeedUI->>Nav: set(from_profile=true)
  FeedUI-->>Nav: navigate(UserProfile)
  end

  rect rgba(255,230,200,0.25)
  note over FeedUI,Nav: 삭제된 피드 처리
  Nav-->>FeedUI: deleted_feed_id
  FeedUI->>VM: removeDeletedFeed(deletedId)
  FeedUI->>Nav: remove(deleted_feed_id)
  end

  rect rgba(245,245,255,0.35)
  note over User,VM: 풀투리프레시
  User->>FeedUI: Pull to refresh
  FeedUI->>VM: pullToRefresh()
  VM->>VM: refresh current tab feeds
  VM->>VM: fetchRecentWriters()
  end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

Suggested labels

🍀 refactor, 🐻 규빈, ✅ OK merge

Suggested reviewers

  • Nico1eKim
  • JJUYAAA

Poem

오늘도 깡총, 피드 숲을 달려가요
프로필 다녀오면 살짝만 흔들어요
새 작가 소식 톡톡, 당겼다 놓으면 촉촉
삭제된 발자국도 산뜻하게 지워요
코드밭 바람에, 토끼 귀가 살랑 👂🌿

Tip

🔌 Remote MCP (Model Context Protocol) integration is now available!

Pro plan users can now connect to remote MCP servers from the Integrations page. Connect with popular remote MCPs such as Notion and Linear to add more context to your reviews and chats.

✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment

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
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR/Issue comments)

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

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Status, Documentation and Community

  • Visit our Status Page to check the current availability of CodeRabbit.
  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

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

⚠️ Outside diff range comments (3)
app/src/main/java/com/texthip/thip/ui/feed/viewmodel/FeedViewModel.kt (2)

202-212: Pull-to-refresh 중 recentWriters 호출이 전체 로딩(isLoading)을 건드려 인디케이터가 꼬일 수 있습니다

현재 pullToRefresh()에서 fetchRecentWriters()를 호출하는데, 해당 함수가 isLoading을 on/off합니다. 그 결과:

  • 풀투리프레시 인디케이터(isPullToRefreshing)가 내려가기 전에 recentWriters 비동기 작업이 끝나지 않고,
  • 현재 탭 목록이 비어있는 경우에는 전체 화면 로딩 UI가 뜨는 부자연스러운 상태 전이가 발생할 수 있습니다.

권장: fetchRecentWriters를 suspend로 변경하고, 기본적으로는 글로벌 로딩을 건드리지 않도록 하세요. pullToRefresh 내에서 순차적으로 await하여 모든 갱신이 끝난 후 isPullToRefreshing을 false로 내리면 일관됩니다. 실제 변경은 아래 291라인 코멘트의 diff를 참고해 주세요.


291-313: fetchRecentWriters를 suspend 함수로 전환하고 모든 호출부를 코루틴 컨텍스트로 감싸세요

이 PR에서 fetchRecentWriters() 시그니처를 suspend fun fetchRecentWriters(withGlobalLoading: Boolean = false)로 변경한 경우, 현재 프로젝트 내 4곳의 호출부 중 1곳(FeedScreen.kt)이 코루틴 밖에서 직접 호출되고 있어 런타임 에러가 발생합니다. 다음 위치를 반드시 수정하세요:

  • FeedViewModel init 블록 (FeedViewModel.kt:68)
    • 기존:

    init {
        loadAllFeeds()
        fetchRecentWriters()
        fetchMyFeedInfo()
    }

    • 수정:

    init {
        loadAllFeeds()
        viewModelScope.launch { fetchRecentWriters(withGlobalLoading = true) }
        fetchMyFeedInfo()
    }
  • pullToRefresh (FeedViewModel.kt:202-210)
    • 이미 viewModelScope.launch 내부이므로, withGlobalLoading 기본값(false)로 호출 가능합니다.

  • refreshData (FeedViewModel.kt:284-289)
    • 이미 viewModelScope.launch 내부이므로, 기본값(false)로 호출 가능합니다.

  • FeedScreen.kt (앱 UI 화면, 약 160행)
    • 기존:

    // 댓글 화면 또는 프로필에서 돌아온 경우
    feedViewModel.fetchRecentWriters()

    • 수정 예시:

    LaunchedEffect(Unit) {
        feedViewModel.fetchRecentWriters()
    }
    // 혹은
    LaunchedEffect(key1 = isProfileReturned) {
        feedViewModel.fetchRecentWriters()
    }

위 4곳 모두 코루틴 내부(또는 Compose LaunchedEffect)에서 호출해야 컴파일·런타임 에러 없이 정상 동작합니다.

app/src/main/java/com/texthip/thip/ui/feed/screen/FeedScreen.kt (1)

124-133: observeForever 대신 Flow(collect) 사용으로 메모리 누수 방지

LiveData의 observeForever 사용은 수명주기를 무시해 콜백 누수 및 메모리 누수를 유발할 수 있습니다. SavedStateHandle.getStateFlow을 사용해 collect로 구독하도록 리팩토링해주세요.

수정 대상:

  • app/src/main/java/com/texthip/thip/ui/feed/screen/FeedScreen.kt:126~133 (deleted_feed_id)
  • app/src/main/java/com/texthip/thip/ui/feed/screen/FeedScreen.kt:205~212 (updated_feed_id)

예시 diff (deleted_feed_id 관찰부):

 LaunchedEffect(navController) {
-    val handle = navController.currentBackStackEntry?.savedStateHandle ?: return@LaunchedEffect
-    handle.getLiveData<Long>("deleted_feed_id").observeForever { deletedId ->
-        if (deletedId != null) {
-            feedViewModel.removeDeletedFeed(deletedId)
-            handle.remove<Long>("deleted_feed_id")
-        }
-    }
+    val handle = navController.currentBackStackEntry?.savedStateHandle ?: return@LaunchedEffect
+    handle.getStateFlow<Long?>("deleted_feed_id", null).collect { deletedId ->
+        if (deletedId != null) {
+            feedViewModel.removeDeletedFeed(deletedId)
+            handle["deleted_feed_id"] = null
+        }
+    }
 }

예시 diff (updated_feed_id 관찰부):

 LaunchedEffect(navController) {
-    val handle = navController.currentBackStackEntry?.savedStateHandle ?: return@LaunchedEffect
-    handle.getLiveData<Long>("updated_feed_id").observeForever { updatedId ->
-        if (updatedId != null) {
-            feedViewModel.updateFeed(updatedId)
-            handle.remove<Long>("updated_feed_id")
-        }
-    }
+    val handle = navController.currentBackStackEntry?.savedStateHandle ?: return@LaunchedEffect
+    handle.getStateFlow<Long?>("updated_feed_id", null).collect { updatedId ->
+        if (updatedId != null) {
+            feedViewModel.updateFeed(updatedId)
+            handle["updated_feed_id"] = null
+        }
+    }
 }

위와 같이 Flow 기반 구독으로 전환하면, 콜백 자동 해제와 수명 주기 안전성을 확보할 수 있습니다.

🧹 Nitpick comments (5)
app/src/main/java/com/texthip/thip/ui/feed/viewmodel/FeedViewModel.kt (3)

333-387: 낙관적 업데이트 실패 시 전체 리스트 롤백은 경쟁 상황에서 다른 변경을 덮어쓸 수 있습니다

현재 onFailure에서 allFeeds/myFeeds 전체를 이전 스냅샷으로 복구합니다. 사용자가 빠르게 여러 피드에 상호작용할 경우, 한 요청의 실패가 다른 요청의 성공/진행 중 변경까지 덮어써 데이터 손실/깜빡임이 생길 수 있습니다.

실패 시 해당 feedId에 한해 원소만 되돌리도록 부분 롤백을 권장합니다.

부분 롤백 diff(실패 핸들러만 교체):

-            ).onFailure {
-                _uiState.update {
-                    it.copy(
-                        allFeeds = currentAllFeeds,
-                        myFeeds = currentMyFeeds
-                    )
-                }
-            }
+            ).onFailure {
+                _uiState.update { state ->
+                    state.copy(
+                        allFeeds = state.allFeeds.map { feed ->
+                            if (feed.feedId.toLong() == feedId) allFeedToUpdate ?: feed else feed
+                        },
+                        myFeeds = state.myFeeds.map { feed ->
+                            if (feed.feedId.toLong() == feedId) myFeedToUpdate ?: feed else feed
+                        }
+                    )
+                }
+            }

추가로, allFeeds/myFeeds를 각각 map으로 두 번 업데이트하는 로직이 changeFeedLike/Save, updateFeedStateFromResult에 중복되어 있습니다. feedId 기준으로 단일 아이템을 업데이트하는 헬퍼를 만들어 중복 제거를 고려해 주세요.


390-437: Save 토글 실패 시 전체 리스트 롤백 → 부분 롤백으로 변경 권장

위 changeFeedLike와 동일한 문제입니다. 실패 시 해당 항목만 원복하도록 바꾸세요.

실패 핸들러 부분 diff:

-            ).onFailure {
-                _uiState.update { it.copy(allFeeds = currentAllFeeds, myFeeds = currentMyFeeds) }
-            }
+            ).onFailure {
+                _uiState.update { state ->
+                    state.copy(
+                        allFeeds = state.allFeeds.map { feed ->
+                            if (feed.feedId.toLong() == feedId) allFeedToUpdate ?: feed else feed
+                        },
+                        myFeeds = state.myFeeds.map { feed ->
+                            if (feed.feedId.toLong() == feedId) myFeedToUpdate ?: feed else feed
+                        }
+                    )
+                }
+            }

453-468: 상태 동기화 로직은 타당합니다. 다만 중복 패턴을 헬퍼로 추출하면 가독성이 좋아집니다

updatedAllFeeds/updatedMyFeeds 생성 로직이 changeFeedLike/Save와 유사합니다. feedId로 특정 아이템만 copy하는 헬퍼(ex. private fun List.updateIfId(...))를 도입해 중복을 제거하면 유지보수가 쉬워집니다.

app/src/main/java/com/texthip/thip/ui/feed/screen/FeedScreen.kt (2)

474-476: 프로필 진입 전 from_profile 플래그 설정: 접근 방향 맞습니다

현재 화면(Feed)이 곧 이전 스택 엔트리가 되므로 currentBackStackEntry.savedStateHandle에 set하는 방식이 적절합니다. 동일 패턴을 사용하는 saved-state 이벤트 전반을 getStateFlow로 일원화하면 관측/해제 관리가 단순해집니다.


205-232: (참고) updated_feed_id LiveData 관찰도 Flow로 바꾸면 누수/중복 호출을 방지할 수 있습니다

변경 라인 외 참고용 제안입니다. deleted_feed_id와 동일하게 getStateFlow로 치환하세요. 예시:

LaunchedEffect(navController) {
    val handle = navController.currentBackStackEntry?.savedStateHandle ?: return@LaunchedEffect
    handle.getStateFlow<Long?>("updated_feed_id", null).collect { feedId ->
        if (feedId != null) {
            val isLiked = handle.get<Boolean>("updated_feed_isLiked") ?: false
            val likeCount = handle.get<Int>("updated_feed_likeCount") ?: 0
            val isSaved = handle.get<Boolean>("updated_feed_isSaved") ?: false
            val commentCount = handle.get<Int>("updated_feed_commentCount") ?: 0

            val result = FeedStateUpdateResult(
                feedId = feedId,
                isLiked = isLiked,
                likeCount = likeCount,
                isSaved = isSaved,
                commentCount = commentCount
            )
            feedViewModel.updateFeedStateFromResult(result)

            handle["updated_feed_id"] = null
            handle["updated_feed_isLiked"] = null
            handle["updated_feed_likeCount"] = null
            handle["updated_feed_isSaved"] = null
            handle["updated_feed_commentCount"] = null
        }
    }
}
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between d8f9ce3 and cd7e1f3.

📒 Files selected for processing (2)
  • app/src/main/java/com/texthip/thip/ui/feed/screen/FeedScreen.kt (4 hunks)
  • app/src/main/java/com/texthip/thip/ui/feed/viewmodel/FeedViewModel.kt (11 hunks)
🔇 Additional comments (1)
app/src/main/java/com/texthip/thip/ui/feed/screen/FeedScreen.kt (1)

549-549: 변경 없음 — 포맷팅 변경으로 보입니다

기능적 변화가 없어 스킵합니다.

Comment on lines 149 to 166
val hasUpdatedFeedData =
navController.currentBackStackEntry?.savedStateHandle?.get<Long>("updated_feed_id") != null
val fromProfile =
navController.currentBackStackEntry?.savedStateHandle?.get<Boolean>("from_profile") ?: false

if (!hasUpdatedFeedData) {
if (!hasUpdatedFeedData && !fromProfile) {
// 일반적인 경우: 전체 새로고침 + 스크롤 상단 이동
feedViewModel.refreshData()
allFeedListState.scrollToItem(0)
} else {
// 댓글 화면 또는 프로필에서 돌아온 경우: recentWriters만 업데이트
feedViewModel.fetchRecentWriters()
}

// 프로필 플래그 제거
if (fromProfile) {
navController.currentBackStackEntry?.savedStateHandle?.remove<Boolean>("from_profile")
}
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

최초 진입 시 refreshData가 이중 호출됩니다 (중복 네트워크 요청 가능)

같은 LaunchedEffect(Unit) 블록 내에서:

  • (라인 144-148) 빈 목록이면 refreshData()
  • 이어서 (라인 154-158) updated_feed_id 없음 && from_profile 아님 → 다시 refreshData()

초기 진입 케이스에서 두 조건이 동시에 참이므로 두 번 호출됩니다. 한 번만 호출되도록 정리하세요.

간단히 첫 번째 블록을 제거하는 diff:

-    LaunchedEffect(Unit) {
-        // 최초 진입시에만 데이터 로딩
-        if (feedUiState.allFeeds.isEmpty() && feedUiState.myFeeds.isEmpty()) {
-            feedViewModel.refreshData()
-        }
-
+    LaunchedEffect(Unit) {
         val hasUpdatedFeedData =
             navController.currentBackStackEntry?.savedStateHandle?.get<Long>("updated_feed_id") != null
         val fromProfile =
             navController.currentBackStackEntry?.savedStateHandle?.get<Boolean>("from_profile") ?: false

         if (!hasUpdatedFeedData && !fromProfile) {
             // 일반적인 경우: 전체 새로고침 + 스크롤 상단 이동
             feedViewModel.refreshData()
             allFeedListState.scrollToItem(0)
         } else {
             // 댓글 화면 또는 프로필에서 돌아온 경우: recentWriters만 업데이트
-            feedViewModel.fetchRecentWriters()
+            // suspend로 전환된 API를 호출한다고 가정
+            feedViewModel.fetchRecentWriters()
         }
 
         // 프로필 플래그 제거
         if (fromProfile) {
             navController.currentBackStackEntry?.savedStateHandle?.remove<Boolean>("from_profile")
         }
     }

위 변경은 291라인에서 제안한 suspend 전환과 함께 적용 시 가장 자연스럽습니다.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
val hasUpdatedFeedData =
navController.currentBackStackEntry?.savedStateHandle?.get<Long>("updated_feed_id") != null
val fromProfile =
navController.currentBackStackEntry?.savedStateHandle?.get<Boolean>("from_profile") ?: false
if (!hasUpdatedFeedData) {
if (!hasUpdatedFeedData && !fromProfile) {
// 일반적인 경우: 전체 새로고침 + 스크롤 상단 이동
feedViewModel.refreshData()
allFeedListState.scrollToItem(0)
} else {
// 댓글 화면 또는 프로필에서 돌아온 경우: recentWriters만 업데이트
feedViewModel.fetchRecentWriters()
}
// 프로필 플래그 제거
if (fromProfile) {
navController.currentBackStackEntry?.savedStateHandle?.remove<Boolean>("from_profile")
}
LaunchedEffect(Unit) {
val hasUpdatedFeedData =
navController.currentBackStackEntry?.savedStateHandle?.get<Long>("updated_feed_id") != null
val fromProfile =
navController.currentBackStackEntry?.savedStateHandle?.get<Boolean>("from_profile") ?: false
if (!hasUpdatedFeedData && !fromProfile) {
// 일반적인 경우: 전체 새로고침 + 스크롤 상단 이동
feedViewModel.refreshData()
allFeedListState.scrollToItem(0)
} else {
// 댓글 화면 또는 프로필에서 돌아온 경우: recentWriters만 업데이트
// suspend로 전환된 API를 호출한다고 가정
feedViewModel.fetchRecentWriters()
}
// 프로필 플래그 제거
if (fromProfile) {
navController.currentBackStackEntry?.savedStateHandle?.remove<Boolean>("from_profile")
}
}

@Nico1eKim Nico1eKim merged commit dd78c7a into THIP-TextHip:develop Aug 25, 2025
1 check passed
@coderabbitai coderabbitai bot mentioned this pull request Aug 27, 2025
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.

2 participants