Skip to content

피드 UX 개선 게시글 이동 방식 변경 및 스크롤 위치 복원#312

Merged
ho0010 merged 12 commits intorefactorfrom
fix/feed-ux
Feb 27, 2026
Merged

피드 UX 개선 게시글 이동 방식 변경 및 스크롤 위치 복원#312
ho0010 merged 12 commits intorefactorfrom
fix/feed-ux

Conversation

@ho0010
Copy link
Collaborator

@ho0010 ho0010 commented Feb 25, 2026

📝작업 내용

  • 피드에서 게시물을 클릭하면 새 탭(window.open)으로 열려 UX가 좋지 않았고, 같은 탭으로 이동하도록 변경하면 뒤로 돌아올 때 스크롤 위치가 초기화되는 문제가 새로 발생함.

상세 변경 사항

  1. 새 탭 → 같은 화면 navigate 전환
  • PostBody, PostFooter의 window.open('/feed/:id', '_blank') → navigate('/feed/:id')
  • FeedDetailPage 뒤로가기 핸들러의 window.close() / window.opener 체크 제거 → navigate(-1)
  1. 피드 목록 스크롤 위치 보존
  • 피드 데이터(게시물 목록, 커서, 탭 상태)를 sessionStorage에 캐싱
  • 상세 페이지에서 돌아올 때 API 재호출 없이 캐시 데이터로 즉시 렌더링 후 저장된 scrollY로 복원
  • 캐시 TTL: 10분 / 스크롤 저장: 100ms 디바운스
  1. 캐시 로직 훅으로 분리 (useFeedCache)
  • Feed.tsx에 있던 캐시 관련 상수/인터페이스/함수를
  • useFeedCache.ts로 이전
    스크롤 복원(rAF), 스크롤 디바운스 저장을 훅 내부에 캡슐화
  • writeFeedCache() export로 Feed에서 명시적으로 호출
  1. 피드 상세 진입 시 흰 화면 깜빡임 개선
  • 로딩 중 return <></> → 로 교체해 어두운 배경 유지
    error / !feedData 상태의 3개 분기도 하나로 통합
  1. 캐시 저장 조건 버그 수정
  • totalFeedPosts.length === 0 가드로 인해 '내 피드' 탭 진입 시 캐시가 영구 미저장되던 문제 수정
    현재 활성 탭 기준으로 조건 변경: activeTab === '피드' ? totalFeedPosts.length > 0 : myFeedPosts.length > 0

개선전

2026-02-25.5.38.58.mov

개선후

2026-02-25.5.45.12.mov

Summary by CodeRabbit

  • 새로운 기능

    • 피드에 클라이언트 캐시 및 복원 기능 추가(스크롤 위치 포함).
    • 다양한 목록(피드, 팔로워/팔로잉, 그룹 검색, 메모리, 저장한 항목, 북 검색)에 무한 스크롤 통합 및 초기 상태 복원 지원.
  • UI/UX 개선

    • 포스트/댓글 클릭 시 새 탭 대신 앱 내부 네비게이션으로 이동.
    • 로딩 스켈레톤/스피너 및 로딩 표시 개선.
    • 뒤로가기 동작과 에러 표시 방식 정돈.

- 새 탭(window.open) 대신 같은 화면에서 navigate()로 이동하도록 변경
  - PostBody: 게시물 본문 클릭 시 window.open → navigate('/feed/:feedId')
  - PostFooter: 댓글 아이콘 클릭 시 window.open → navigate('/feed/:feedId')

- FeedDetailPage: 뒤로가기 핸들러 정리
  - window.close() / window.opener 체크 제거
  - navigate(-1) 단일 호출로 단순화

- Feed: 피드 목록 상태 및 스크롤 위치를 sessionStorage에 캐싱하여 복원
  - 게시물 상세에서 돌아올 때 API 재호출 없이 캐시 데이터로 즉시 렌더링
  - 10분 TTL 초과 시 자동으로 캐시 무효화
  - 스크롤 이벤트(100ms 디바운스)로 scrollY를 sessionStorage에 주기 저장
  - 마운트 시 저장된 scrollY로 스크롤 위치 복원(rAF 2회)
  - 탭 전환 시 scrollTo(0, 0) 정상 동작 유지
  - navigate(state: { initialTab }) 진입 시 캐시 무시하고 새로 로드
@vercel
Copy link

vercel bot commented Feb 25, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
thip Ready Ready Preview, Comment Feb 27, 2026 2:53am

@coderabbitai
Copy link

coderabbitai bot commented Feb 25, 2026

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Jira integration is disabled

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between c2e7b8b and ef8db6c.

📒 Files selected for processing (5)
  • src/hooks/useFeedCache.ts
  • src/hooks/useInifinieScroll.ts
  • src/pages/feed/Feed.tsx
  • src/pages/feed/FeedDetailPage.tsx
  • src/pages/memory/Memory.tsx

Walkthrough

Post 컴포넌트 클릭이 클라이언트 라우팅으로 변경되었고, 피드 페이지에 sessionStorage 기반 캐시 훅 및 초기 복원과 무한 스크롤 초기화 로직이 추가되었으며, 여러 페이지가 공통 useInifinieScroll로 전환되었습니다. FeedDetail의 일부 fetch/내비게이션 흐름도 단순화되었습니다.

Changes

Cohort / File(s) Summary
Post 네비게이션 변경
src/components/common/Post/PostBody.tsx, src/components/common/Post/PostFooter.tsx
window.openreact-routeruseNavigate로 인앱 네비게이션 변경(새 탭 대신 내부 라우트 사용).
Feed 페이지 — 캐시 도입 및 로드/복원 변경
src/pages/feed/Feed.tsx, src/pages/feed/FeedDetailPage.tsx
useFeedCache 기반 세션 캐시 복원(스크롤Y 포함), 초기Items/nextCursor/isLast로 무한스크롤 초기화, 캐시 쓰기 로직 추가; FeedDetail은 댓글 병렬 호출 제거 및 뒤로가기 간소화.
캐시 훅 추가
src/hooks/useFeedCache.ts
FeedCache 인터페이스와 useFeedCache/writeFeedCache 구현 추가(세션 TTL, scroll 복원, debounced 저장).
공통 무한스크롤 도입
src/hooks/useInifinieScroll.ts, src/pages/feed/FollowerListPage.tsx, src/pages/groupSearch/GroupSearch.tsx, src/pages/memory/Memory.tsx, src/pages/mypage/SavePage.tsx, src/pages/searchBook/SearchBook.tsx
useInifinieScroll에 initialItems/initialCursor/initialIsLast 옵션 및 nextCursor 반환 추가; 여러 페이지에서 수동 관찰·로딩 로직을 제거하고 hook 기반 페이징(sentinelRef, fetchPage)으로 전환.
경미한 포맷/렌더 조정
src/components/common/Post/..., src/pages/...
이미지 JSX 줄바꿈, 로딩 조건/스켈레톤 교체 등 비기능적 포맷 및 렌더 조건 조정 다수(의미 변화 없음).

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant Post as PostComponent
    participant Router
    participant FeedDetail as FeedDetailPage

    User->>Post: 게시물 클릭
    Post->>Router: navigate(`/feed/${feedId}`)
    Router->>FeedDetail: 라우팅 요청
    FeedDetail-->>User: Feed 상세 뷰 렌더링
Loading
sequenceDiagram
    participant Feed as FeedPage
    participant Hook as useFeedCache
    participant Session as sessionStorage
    participant API as BackendAPI

    Feed->>Hook: 마운트 시 초기 캐시 요청
    Hook->>Session: readCache()
    alt 캐시 존재 (TTL 유효)
        Hook-->>Feed: initialCache 반환 (items, cursors, scrollY)
        Feed->>Session: writeFeedCache(...) on tab/change
        Feed->>API: loadMoreFeeds 이후 필요 시 추가 페치
        Hook->>Feed: restore scroll (requestAnimationFrame x2)
    else 캐시 없음
        Hook-->>Feed: initialCache = null
        Feed->>API: 초기 데이터 페치
        Feed->>Session: writeFeedCache(...) on tab/change
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested reviewers

  • heeeeyong
  • ljh130334

Poem

🐰 폴짝폴짝 코드밭을 뛰어가요,
탭 대신 길 따라 길을 잇고,
스크롤 자국은 살며시 주머니에 담아,
피드는 다시 이어져 반짝이네,
당근 한 입 축하 인사! 🥕

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed PR 제목은 피드 UX 개선의 핵심 변경사항인 게시글 이동 방식 변경(window.open → navigate)과 스크롤 위치 복원을 정확하게 요약하고 있으며, 변경사항의 주요 목적을 명확하게 전달합니다.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch fix/feed-ux

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 and usage tips.

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: 2

🧹 Nitpick comments (3)
src/pages/feed/Feed.tsx (3)

33-43: 만료된 캐시가 sessionStorage에서 제거되지 않습니다.

getInitialFeedCache에서 TTL이 만료된 경우 null을 반환하지만, 만료된 항목을 sessionStorage에서 삭제하지 않습니다. 큰 문제는 아니지만, 정리하면 storage 공간을 확보할 수 있습니다.

♻️ 만료 시 캐시 제거 제안
 function getInitialFeedCache(): FeedCache | null {
   try {
     const raw = sessionStorage.getItem(FEED_CACHE_KEY);
     if (!raw) return null;
     const cache = JSON.parse(raw) as FeedCache;
-    if (Date.now() - cache.timestamp < FEED_CACHE_TTL) return cache;
+    if (Date.now() - cache.timestamp < FEED_CACHE_TTL) return cache;
+    sessionStorage.removeItem(FEED_CACHE_KEY);
   } catch {
     // ignore
   }
   return null;
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/pages/feed/Feed.tsx` around lines 33 - 43, getInitialFeedCache currently
returns null for expired caches but leaves the stale entry in sessionStorage;
update the function (getInitialFeedCache) to remove the expired item by calling
sessionStorage.removeItem(FEED_CACHE_KEY) when the TTL check fails (i.e., when
Date.now() - cache.timestamp >= FEED_CACHE_TTL) so the stale data is cleaned up
before returning null; keep the existing try/catch behavior for parse errors.

148-158: loadMoreFeeds의 의존성 배열에 loadTotalFeedsloadMyFeeds가 누락되어 있습니다.

두 함수는 현재 빈 의존성 배열로 안정적이지만, 향후 변경 시 stale closure 문제가 발생할 수 있고 react-hooks/exhaustive-deps 경고가 발생할 수 있습니다.

♻️ 의존성 배열 보완
-  }, [activeTab, totalIsLast, totalLoading, totalNextCursor, myIsLast, myLoading, myNextCursor]);
+  }, [activeTab, totalIsLast, totalLoading, totalNextCursor, myIsLast, myLoading, myNextCursor, loadTotalFeeds, loadMyFeeds]);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/pages/feed/Feed.tsx` around lines 148 - 158, The useCallback for
loadMoreFeeds is missing loadTotalFeeds and loadMyFeeds in its dependency array
which can cause stale closures and lint warnings; update the dependency array of
the useCallback that defines loadMoreFeeds to include loadTotalFeeds and
loadMyFeeds, or if those functions are unstable, memoize them (e.g., wrap
loadTotalFeeds/loadMyFeeds with useCallback where they are declared) so
loadMoreFeeds remains correct and lint-clean.

239-239: sessionStorage.setItem에서 QuotaExceededError가 발생할 수 있습니다.

피드 목록이 많거나 contentUrls에 긴 URL이 포함된 경우 직렬화된 데이터가 sessionStorage 용량(~5MB)을 초과할 수 있습니다. setItem 호출을 try-catch로 감싸거나, 캐시할 포스트 수에 상한을 두는 것을 고려해 주세요. Line 212의 scroll persistence 쪽도 동일합니다.

🛡️ setItem에 try-catch 추가 제안
-    sessionStorage.setItem(FEED_CACHE_KEY, JSON.stringify(cache));
+    try {
+      sessionStorage.setItem(FEED_CACHE_KEY, JSON.stringify(cache));
+    } catch {
+      // QuotaExceededError — 캐시 저장 실패 무시
+    }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/pages/feed/Feed.tsx` at line 239, Wrap the
sessionStorage.setItem(FEED_CACHE_KEY, JSON.stringify(cache)) call in a
try-catch and handle QuotaExceededError by either pruning the cache (limit
number of posts or strip/shorten contentUrls) before retrying or by falling back
to no-op (skip caching) and logging the failure; apply the same try-catch +
graceful fallback to the scroll persistence sessionStorage.setItem usage as well
so storage quota failures do not crash the page.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/components/common/Post/PostFooter.tsx`:
- Line 36: Import the missing hook from react-router-dom and add it to the
top-level imports so useNavigate() is defined; specifically, update the imports
in PostFooter.tsx to include useNavigate (used where const navigate =
useNavigate()) alongside the other React/third-party imports so the
ReferenceError is resolved.

In `@src/pages/feed/Feed.tsx`:
- Around line 226-250: The current guard in the useEffect uses
totalFeedPosts.length === 0 which prevents caching when only myFeedPosts are
loaded; update the condition to check the currently activeTab (activeTab) and
only bail out when the relevant list for that tab is empty (e.g., if activeTab
=== '전체' require totalFeedPosts.length > 0, if activeTab === '내 피드' require
myFeedPosts.length > 0), keeping other guards (initialLoading, tabLoading)
intact so sessionStorage.setItem(FEED_CACHE_KEY, ...) runs when the active tab's
data is present.

---

Nitpick comments:
In `@src/pages/feed/Feed.tsx`:
- Around line 33-43: getInitialFeedCache currently returns null for expired
caches but leaves the stale entry in sessionStorage; update the function
(getInitialFeedCache) to remove the expired item by calling
sessionStorage.removeItem(FEED_CACHE_KEY) when the TTL check fails (i.e., when
Date.now() - cache.timestamp >= FEED_CACHE_TTL) so the stale data is cleaned up
before returning null; keep the existing try/catch behavior for parse errors.
- Around line 148-158: The useCallback for loadMoreFeeds is missing
loadTotalFeeds and loadMyFeeds in its dependency array which can cause stale
closures and lint warnings; update the dependency array of the useCallback that
defines loadMoreFeeds to include loadTotalFeeds and loadMyFeeds, or if those
functions are unstable, memoize them (e.g., wrap loadTotalFeeds/loadMyFeeds with
useCallback where they are declared) so loadMoreFeeds remains correct and
lint-clean.
- Line 239: Wrap the sessionStorage.setItem(FEED_CACHE_KEY,
JSON.stringify(cache)) call in a try-catch and handle QuotaExceededError by
either pruning the cache (limit number of posts or strip/shorten contentUrls)
before retrying or by falling back to no-op (skip caching) and logging the
failure; apply the same try-catch + graceful fallback to the scroll persistence
sessionStorage.setItem usage as well so storage quota failures do not crash the
page.

ℹ️ Review info

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Jira integration is disabled

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between fa6d152 and 16847e7.

📒 Files selected for processing (4)
  • src/components/common/Post/PostBody.tsx
  • src/components/common/Post/PostFooter.tsx
  • src/pages/feed/Feed.tsx
  • src/pages/feed/FeedDetailPage.tsx

@ho0010 ho0010 changed the title 피드 UX 개선 fix: feed UX 개선 Feb 25, 2026
@ho0010 ho0010 changed the title fix: feed UX 개선 fix: feed 상세 게시글 UX 저하 Feb 25, 2026
@ho0010 ho0010 changed the title fix: feed 상세 게시글 UX 저하 fix: feed 상세글 진입시 UX 저하 Feb 25, 2026
로딩 중 return <></>로 아무것도 렌더하지 않아
부모 Layout의 흰 배경이 노출되면서 깜빡임이 발생하던 문제 수정

- loading 상태: <Wrapper> + LoadingSpinner를 렌더해 어두운 배경 유지
- error / !feedData 상태: 세 개로 분리된 return <></> 를 <Wrapper /> 하나로 통합
@ho0010 ho0010 changed the title fix: feed 상세글 진입시 UX 저하 피드 UX 개선 게시글 이동 방식 변경 및 스크롤 위치 복원 Feb 25, 2026
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 (6)
src/pages/feed/FeedDetailPage.tsx (3)

174-180: ⚠️ Potential issue | 🟠 Major

에러 분기가 중복되어 Wrapper 유지 로직이 무력화됩니다.

Line 174에서 이미 error를 반환해 Line 178의 error || !feedData 분기 중 error 경로는 도달하지 않습니다. 에러/빈 데이터 분기는 하나로 합쳐 주세요.

🔧 제안 수정
-  if (error) {
-    return <></>;
-  }
-
   if (error || !feedData) {
     return <Wrapper />;
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/pages/feed/FeedDetailPage.tsx` around lines 174 - 180, Remove the
duplicated error branch in FeedDetailPage.tsx: eliminate the early "if (error)
return <></>;" and keep a single check "if (error || !feedData) return <Wrapper
/>;" so that both error and missing feedData are handled in one place; reference
the variables error and feedData and the Wrapper return to locate and update the
logic.

148-171: ⚠️ Potential issue | 🔴 Critical

로딩 분기의 return 문 이후 도달 불가능한 JSX가 남아있어 문법 오류가 발생합니다.

Line 153에서 return 문이 끝났는데 Line 154부터 </Wrapper> 태그까지 도달 불가능한 JSX 코드가 남아있습니다. 머지 잔재로 보이며, 이 블록은 제거해야 합니다.

🔧 제안 수정
   if (loading) {
     return (
       <Wrapper>
         <LoadingSpinner size="large" fullHeight={true} />
       </Wrapper>
     );
-        <TitleHeader
-          leftIcon={<img src={leftArrow} alt="뒤로가기" />}
-          onLeftClick={handleBackClick}
-        />
-        <SkeletonWrapper>
-          <FeedPostSkeleton />
-          {Array.from({ length: 5 }).map((_, index) => (
-            <CommentSkeletonItem key={index}>
-              <Skeleton.Circle width={36} />
-              <div style={{ flex: 1 }}>
-                <Skeleton.Text width={80} height={14} />
-                <Skeleton.Text lines={2} height={14} gap={6} />
-              </div>
-            </CommentSkeletonItem>
-          ))}
-        </SkeletonWrapper>
-      </Wrapper>
-    );
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/pages/feed/FeedDetailPage.tsx` around lines 148 - 171, The loading branch
currently returns early (if (loading) return ...) but leftover unreachable JSX
(the TitleHeader and subsequent SkeletonWrapper/FeedPostSkeleton block including
TitleHeader, handleBackClick, SkeletonWrapper, CommentSkeletonItem) remains
after that return causing a syntax error; remove the extraneous JSX that follows
the early return so the if (loading) { return ( <Wrapper> <LoadingSpinner .../>
</Wrapper> ); } block is the entire loading branch and ensure TitleHeader and
SkeletonWrapper are only present outside this removed unreachable fragment where
intended.

48-53: ⚠️ Potential issue | 🔴 Critical

중복된 const 선언과 import 누락으로 컴파일 오류가 발생합니다.

라인 48과 라인 50에서 동일한 스코프에 feedResponse를 재선언하고 있으며, getComments는 import되지 않아 컴파일이 실패합니다. 또한 getFeedDetail이 중복으로 호출되고 commentsResponse는 사용되지 않습니다.

라인 48의 const feedResponse = await getFeedDetail(...) 선언을 제거하고, getComments@/api/comments/getComments에서 import해야 합니다.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/pages/feed/FeedDetailPage.tsx` around lines 48 - 53, Remove the redundant
single-await declaration "const feedResponse = await getFeedDetail(...)" and
keep a single Promise.all that fetches getFeedDetail and getComments; ensure
getFeedDetail is only called once (inside the Promise.all) and either use or
remove the returned commentsResponse accordingly. Add the missing import for
getComments from "@/api/comments/getComments" and remove any unused variables
(e.g., commentsResponse) if you decide not to consume comments in
FeedDetailPage.
src/pages/feed/Feed.tsx (3)

1-17: ⚠️ Potential issue | 🔴 Critical

import 정합성 오류로 빌드가 깨질 수 있습니다.

Line 14와 16에서 Container가 중복 선언되고, Line 259와 296에서 사용하는 LoadingSpinner import가 누락되었습니다.

🔧 제안 수정
 import { FeedPostSkeleton, OtherFeedSkeleton } from '@/shared/ui/Skeleton';
+import LoadingSpinner from '@/components/common/LoadingSpinner';
 import writefab from '../../assets/common/writefab.svg';
 import { useNavigate, useLocation } from 'react-router-dom';
 import { getTotalFeeds } from '@/api/feeds/getTotalFeed';
 import { getMyFeeds } from '@/api/feeds/getMyFeed';
 import { useSocialLoginToken } from '@/hooks/useSocialLoginToken';
 import { useFeedCache, writeFeedCache } from '@/hooks/useFeedCache';
-import { Container } from './Feed.styled';
 import { useInifinieScroll } from '@/hooks/useInifinieScroll';
 import { Container, SkeletonWrapper } from './Feed.styled';
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/pages/feed/Feed.tsx` around lines 1 - 17, The file has an import conflict
and a missing symbol: remove the duplicate Container import (both imports from
'./Feed.styled' — keep a single Container import) so there is only one Container
symbol, and add an import for the LoadingSpinner component (used by this module
with FeedPostSkeleton and OtherFeedSkeleton) so references to LoadingSpinner at
runtime resolve; update the import block to import Container once and include
LoadingSpinner alongside FeedPostSkeleton/OtherFeedSkeleton (and verify the
LoadingSpinner export name matches the symbol used).

227-248: ⚠️ Potential issue | 🔴 Critical

useEffect 시작 블록이 누락되어 Hook 구조가 깨져 있습니다.

Line 227-248의 코드는 useEffect 본문으로 작성되었으나, 대응하는 useEffect(() => { 시작이 없어 문법 오류입니다. Line 248의 }, [activeTab, waitForToken, loadTotalFeeds, loadMyFeeds]);는 닫혀야 할 useEffect 블록이 없습니다.

🔧 제안 수정(useEffect 래핑 추가)
   const isLoading = initialLoading || tabLoading;
+    useEffect(() => {
     const loadFeedsWithToken = async () => {
       await waitForToken();

       setTabLoading(true);

       try {
         const minLoadingTime = new Promise(resolve => setTimeout(resolve, 500));

         if (activeTab === '피드') {
           await Promise.all([loadTotalFeeds(), minLoadingTime]);
         } else if (activeTab === '내 피드') {
           await Promise.all([loadMyFeeds(), minLoadingTime]);
         }
       } finally {
         setTabLoading(false);
         setInitialLoading(false);
       }
     };

     loadFeedsWithToken();
   }, [activeTab, waitForToken, loadTotalFeeds, loadMyFeeds]);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/pages/feed/Feed.tsx` around lines 227 - 248, Wrap the block that defines
isLoading, loadFeedsWithToken and calls loadFeedsWithToken inside a useEffect so
the Hook structure is valid: add a useEffect(() => { ... }, [activeTab,
waitForToken, loadTotalFeeds, loadMyFeeds]) wrapper around the existing code
(keeping isLoading, setTabLoading, setInitialLoading, loadFeedsWithToken, and
the call to loadFeedsWithToken unchanged) so the effect runs when
activeTab/waitForToken/loadTotalFeeds/loadMyFeeds change and the
cleanup/try/finally behavior remains intact.

258-298: ⚠️ Potential issue | 🔴 Critical

렌더 분기 머지 충돌로 JSX 구조가 깨져 있습니다.

라인 258에서 시작한 삼항 연산자(isLoading ?)가 라인 263에서 닫혀있지 않으며, 라인 264에서 또 다른 삼항 연산자(initialLoading || tabLoading ?)가 시작됩니다. 이로 인해 중괄호 불일치로 JSX 파싱이 실패합니다.

구 렌더 로직(라인 258-263, totalFeedPosts/myFeedPosts 기반)과 신 렌더 로직(라인 264-298, totalFeed/myFeed 기반)이 중첩되어 있습니다. 신 로직이 무한 스크롤, 스켈레톤 로딩, 센티널 처리를 포함하는 더 완전한 구현이므로, 구 로직을 제거하여 신 로직만 유지해야 합니다.

제안 수정
      <TabBar tabs={tabs} activeTab={activeTab} onTabClick={setActiveTab} />
-     {isLoading ? (
-       <LoadingSpinner size="large" fullHeight={true} />
-     ) : activeTab === '피드' ? (
-       <TotalFeed showHeader={true} posts={totalFeedPosts} isMyFeed={false} isLast={totalIsLast} />
-     ) : (
-       <MyFeed showHeader={false} posts={myFeedPosts} isMyFeed={true} isLast={myIsLast} />
      {initialLoading || tabLoading ? (
        activeTab === '내 피드' ? (
          <OtherFeedSkeleton showFollowButton={false} paddingTop={136} />
        ) : (
          <SkeletonWrapper>
            {Array.from({ length: 3 }).map((_, index) => (
              <FeedPostSkeleton key={index} />
            ))}
          </SkeletonWrapper>
        )
      ) : (
        <>
          {activeTab === '피드' ? (
            <>
              <TotalFeed
                showHeader={true}
                posts={totalFeed.items}
                isMyFeed={false}
                isLast={totalFeed.isLast}
              />
            </>
          ) : (
            <>
              <MyFeed
                showHeader={false}
                posts={myFeed.items}
                isMyFeed={true}
                isLast={myFeed.isLast}
              />
            </>
          )}
          {!currentFeed.isLast && <div ref={currentFeed.sentinelRef} style={{ height: 40 }} />}
          {currentFeed.isLoadingMore && <LoadingSpinner size="small" fullHeight={false} />}
        </>
       )}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/pages/feed/Feed.tsx` around lines 258 - 298, Remove the old, duplicated
JSX branch that uses totalFeedPosts/myFeedPosts and leaves the isLoading ternary
unclosed; keep only the newer render flow that handles initialLoading,
tabLoading, infinite scroll and skeletons (the branch using totalFeed/myFeed,
currentFeed.sentinelRef, and currentFeed.isLoadingMore). Specifically, update
the JSX around isLoading, initialLoading and tabLoading so the top-level
isLoading ? ... : ... ternary wraps the single new branch (which renders
TotalFeed and MyFeed via totalFeed.items/myFeed.items and uses currentFeed for
sentinel/loading), and delete the outdated TotalFeed(showHeader,
posts={totalFeedPosts})/MyFeed(posts={myFeedPosts}) fragments to restore proper
brace/parenthesis balance.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/hooks/useFeedCache.ts`:
- Around line 33-40: The writeFeedCache function currently calls
sessionStorage.setItem which can throw and break the feed render flow; wrap the
sessionStorage.setItem call (and the JSON.stringify step) in a try/catch inside
writeFeedCache to absorb any exceptions and avoid throwing to the UI, and in the
catch block log a non-fatal warning (e.g., console.warn or similar logger)
including the error and a short context message so failures are visible but do
not interrupt rendering.

---

Outside diff comments:
In `@src/pages/feed/Feed.tsx`:
- Around line 1-17: The file has an import conflict and a missing symbol: remove
the duplicate Container import (both imports from './Feed.styled' — keep a
single Container import) so there is only one Container symbol, and add an
import for the LoadingSpinner component (used by this module with
FeedPostSkeleton and OtherFeedSkeleton) so references to LoadingSpinner at
runtime resolve; update the import block to import Container once and include
LoadingSpinner alongside FeedPostSkeleton/OtherFeedSkeleton (and verify the
LoadingSpinner export name matches the symbol used).
- Around line 227-248: Wrap the block that defines isLoading, loadFeedsWithToken
and calls loadFeedsWithToken inside a useEffect so the Hook structure is valid:
add a useEffect(() => { ... }, [activeTab, waitForToken, loadTotalFeeds,
loadMyFeeds]) wrapper around the existing code (keeping isLoading,
setTabLoading, setInitialLoading, loadFeedsWithToken, and the call to
loadFeedsWithToken unchanged) so the effect runs when
activeTab/waitForToken/loadTotalFeeds/loadMyFeeds change and the
cleanup/try/finally behavior remains intact.
- Around line 258-298: Remove the old, duplicated JSX branch that uses
totalFeedPosts/myFeedPosts and leaves the isLoading ternary unclosed; keep only
the newer render flow that handles initialLoading, tabLoading, infinite scroll
and skeletons (the branch using totalFeed/myFeed, currentFeed.sentinelRef, and
currentFeed.isLoadingMore). Specifically, update the JSX around isLoading,
initialLoading and tabLoading so the top-level isLoading ? ... : ... ternary
wraps the single new branch (which renders TotalFeed and MyFeed via
totalFeed.items/myFeed.items and uses currentFeed for sentinel/loading), and
delete the outdated TotalFeed(showHeader,
posts={totalFeedPosts})/MyFeed(posts={myFeedPosts}) fragments to restore proper
brace/parenthesis balance.

In `@src/pages/feed/FeedDetailPage.tsx`:
- Around line 174-180: Remove the duplicated error branch in FeedDetailPage.tsx:
eliminate the early "if (error) return <></>;" and keep a single check "if
(error || !feedData) return <Wrapper />;" so that both error and missing
feedData are handled in one place; reference the variables error and feedData
and the Wrapper return to locate and update the logic.
- Around line 148-171: The loading branch currently returns early (if (loading)
return ...) but leftover unreachable JSX (the TitleHeader and subsequent
SkeletonWrapper/FeedPostSkeleton block including TitleHeader, handleBackClick,
SkeletonWrapper, CommentSkeletonItem) remains after that return causing a syntax
error; remove the extraneous JSX that follows the early return so the if
(loading) { return ( <Wrapper> <LoadingSpinner .../> </Wrapper> ); } block is
the entire loading branch and ensure TitleHeader and SkeletonWrapper are only
present outside this removed unreachable fragment where intended.
- Around line 48-53: Remove the redundant single-await declaration "const
feedResponse = await getFeedDetail(...)" and keep a single Promise.all that
fetches getFeedDetail and getComments; ensure getFeedDetail is only called once
(inside the Promise.all) and either use or remove the returned commentsResponse
accordingly. Add the missing import for getComments from
"@/api/comments/getComments" and remove any unused variables (e.g.,
commentsResponse) if you decide not to consume comments in FeedDetailPage.

ℹ️ Review info

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Jira integration is disabled

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 16847e7 and c13d29b.

📒 Files selected for processing (4)
  • src/components/common/Post/PostFooter.tsx
  • src/hooks/useFeedCache.ts
  • src/pages/feed/Feed.tsx
  • src/pages/feed/FeedDetailPage.tsx
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/components/common/Post/PostFooter.tsx

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: 3

Caution

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

⚠️ Outside diff range comments (2)
src/pages/feed/FeedDetailPage.tsx (2)

36-62: ⚠️ Potential issue | 🟠 Major

비동기 응답 경합으로 상세 데이터가 뒤바뀔 수 있습니다.

Line 37-58은 취소/최신성 보장이 없어, 이전 요청이 늦게 도착하면 현재 feedId의 상태를 덮어쓸 수 있습니다. 언마운트/파라미터 변경 시 무시 플래그(또는 AbortController 연계)로 최신 요청만 반영해 주세요.

🔧 제안 수정
  useEffect(() => {
+    let isActive = true;
     const loadFeedDetail = async () => {
       if (!feedId) {
-        setError('피드 ID가 없습니다.');
-        setLoading(false);
+        if (!isActive) return;
+        setError('피드 ID가 없습니다.');
+        setLoading(false);
         return;
       }

       try {
-        setLoading(true);
+        if (isActive) setLoading(true);

         const minLoadingTime = new Promise(resolve => setTimeout(resolve, 500));
         const feedResponse = await getFeedDetail(Number(feedId));
         await minLoadingTime;

+        if (!isActive) return;
         setFeedData(feedResponse.data);
         setError(null);
       } catch (err) {
+        if (!isActive) return;
         console.error('피드 상세 정보 로드 실패:', err);
         setError('피드 정보를 불러오는데 실패했습니다.');
       } finally {
-        setLoading(false);
+        if (isActive) setLoading(false);
       }
     };

     loadFeedDetail();
+    return () => {
+      isActive = false;
+    };
   }, [feedId]);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/pages/feed/FeedDetailPage.tsx` around lines 36 - 62, The loadFeedDetail
effect can apply stale responses to state when feedId changes; modify the
useEffect and its inner loadFeedDetail to cancel or ignore previous requests by
creating an AbortController (or a local "isCurrent" flag) scoped to the effect,
pass its signal into getFeedDetail (or check the flag before calling
setFeedData/setError), and in the cleanup return set the controller.abort() (or
flip the flag) so only the latest request updates state (update references in
useEffect, loadFeedDetail, getFeedDetail call site, and the
setFeedData/setError/setLoading usages).

38-49: ⚠️ Potential issue | 🟡 Minor

feedId 숫자 검증이 없어 /feeds/NaN 호출이 가능합니다.

Line 48 전에 Number.isFinite 검증을 추가해 잘못된 경로 입력을 조기에 차단해 주세요.

🔧 제안 수정
       if (!feedId) {
         setError('피드 ID가 없습니다.');
         setLoading(false);
         return;
       }
+      const parsedFeedId = Number(feedId);
+      if (!Number.isFinite(parsedFeedId)) {
+        setError('유효하지 않은 피드 ID입니다.');
+        setLoading(false);
+        return;
+      }

       try {
         setLoading(true);

         const minLoadingTime = new Promise(resolve => setTimeout(resolve, 500));
-        const feedResponse = await getFeedDetail(Number(feedId));
+        const feedResponse = await getFeedDetail(parsedFeedId);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/pages/feed/FeedDetailPage.tsx` around lines 38 - 49, The current code
only checks for a falsy feedId string and can call getFeedDetail(Number(feedId))
with NaN; before calling getFeedDetail, parse the id to a number and add a
Number.isFinite check (e.g. const idNum = Number(feedId); if
(!Number.isFinite(idNum)) { setError('유효하지 않은 피드 ID입니다.'); setLoading(false);
return; }) to early-return on invalid paths; update subsequent calls to use
idNum when calling getFeedDetail and keep existing setError/setLoading behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/pages/feed/Feed.tsx`:
- Around line 25-33: useFeedCache() eagerly reads and runs its restore effect
causing scroll restoration even when initialTabFromState is present; change
useFeedCache to accept an options param like { enabled?: boolean } and only call
readCache()/run the restore/save effects when enabled is true, and then update
the Feed component to call useFeedCache({ enabled: !initialTabFromState }) (use
the symbols useFeedCache, initialTabFromState, isRestoringFromCache, readCache,
initialCache) so that when initialTabFromState is set the hook does not read or
restore scroll state.
- Around line 65-150: The code duplicates data flows: keep a single source (the
manual state variables totalFeedPosts / myFeedPosts and their loaders
loadTotalFeeds / loadMyFeeds) and remove the separate useInfiniteScroll logic
that also calls the APIs; update renders to read from totalFeedPosts or
myFeedPosts (not the hook's items) and wire loadMoreFeeds as the only pagination
trigger (and remove the separate hook and its items/nextCursor usage).
Specifically, delete or disable the useInfiniteScroll block and any variables it
exposes, ensure loadTotalFeeds and loadMyFeeds set nextCursor/isLast
consistently, and update the render (where it referenced hook.items) to use
totalFeedPosts or myFeedPosts so cache restoration and single API path are
respected.

In `@src/pages/feed/FeedDetailPage.tsx`:
- Around line 166-167: In FeedDetailPage replace the early return that renders
only <Wrapper /> when (error || !feedData) so the back header remains visible;
update the error/empty branch to render the existing header/back component (the
same header used in the normal UI) together with the Wrapper and a minimal error
or empty-state message. Locate the conditional in FeedDetailPage and ensure you
include the header/back component (not just Wrapper) before returning for the
error or empty-data case.

---

Outside diff comments:
In `@src/pages/feed/FeedDetailPage.tsx`:
- Around line 36-62: The loadFeedDetail effect can apply stale responses to
state when feedId changes; modify the useEffect and its inner loadFeedDetail to
cancel or ignore previous requests by creating an AbortController (or a local
"isCurrent" flag) scoped to the effect, pass its signal into getFeedDetail (or
check the flag before calling setFeedData/setError), and in the cleanup return
set the controller.abort() (or flip the flag) so only the latest request updates
state (update references in useEffect, loadFeedDetail, getFeedDetail call site,
and the setFeedData/setError/setLoading usages).
- Around line 38-49: The current code only checks for a falsy feedId string and
can call getFeedDetail(Number(feedId)) with NaN; before calling getFeedDetail,
parse the id to a number and add a Number.isFinite check (e.g. const idNum =
Number(feedId); if (!Number.isFinite(idNum)) { setError('유효하지 않은 피드 ID입니다.');
setLoading(false); return; }) to early-return on invalid paths; update
subsequent calls to use idNum when calling getFeedDetail and keep existing
setError/setLoading behavior.

ℹ️ Review info

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Jira integration is disabled

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between c13d29b and f65968e.

📒 Files selected for processing (2)
  • src/pages/feed/Feed.tsx
  • src/pages/feed/FeedDetailPage.tsx

- FollowerListPage: 구버전 loadUserList useCallback 제거, useInifinieScroll 복원
- GroupSearch: 고아 setTimeout·loadMore useCallback 제거,
  searchFirstPage 대신 useInifinieScroll 기반 searchResult 도입
- Memory: fetchPage 내 loadMemoryPosts useCallback 혼입 코드 제거,
  미사용 error/loading 상태 및 LoadingSpinner import 제거
- SavePage: 구버전 loadSavedBooks/loadSavedFeeds/loadAllData
  수동 페이지네이션 코드 제거 (useInifinieScroll 중복)
- SearchBook: 구버전 loadMore useCallback 잔재 제거, LoadingBox import 추가
- useFeedCache: writeFeedCache의 sessionStorage.setItem을 try/catch로
  감싸 쿼터 초과·프라이빗 브라우징 환경에서 예외가 피드 렌더로 전파되지
  않도록 보호
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)
src/pages/feed/FollowerListPage.tsx (1)

69-99: ⚠️ Potential issue | 🟠 Major

목록 조회 오류가 사용자에게 노출되지 않습니다.

userList.error를 렌더링하지 않아 실패가 “빈 리스트”처럼 보입니다. 오류 상태 UI를 별도로 분기해 주세요.

🔧 제안 수정
-      {userList.isLoading && userList.items.length === 0 ? (
+      {userList.error ? (
+        <UserProfileList>
+          <div style={{ textAlign: 'center', padding: '24px 0' }}>
+            목록을 불러오지 못했습니다. 잠시 후 다시 시도해 주세요.
+          </div>
+        </UserProfileList>
+      ) : userList.isLoading && userList.items.length === 0 ? (
         <UserProfileList>
           {Array.from({ length: 5 }).map((_, i) => (
             <UserProfileItemSkeleton key={i} type={type as UserProfileType} />
           ))}
         </UserProfileList>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/pages/feed/FollowerListPage.tsx` around lines 69 - 99, The UI hides
backend errors by never rendering userList.error, so when fetching fails the
page looks like an empty list; update FollowerListPage to branch on
userList.error before the skeleton/items render (check userList.error from the
same data source used for userList.isLoading/isLast/isLoadingMore) and render an
explicit error state inside the UserProfileList (or replace it) with a readable
message and retry affordance; ensure the error branch references the same
symbols (userList.error, userList.sentinelRef, UserProfileList,
UserProfileItemSkeleton, LoadingSpinner) and preserves existing loading and
pagination UI for other branches.
src/pages/searchBook/SearchBook.tsx (1)

258-296: ⚠️ Potential issue | 🟠 Major

피드 조회 실패가 “데이터 없음”으로 오인됩니다.

feeds.error를 렌더링 분기에 반영하지 않아, API 실패 시에도 빈 상태 문구가 노출됩니다. 실패 상태와 빈 결과 상태를 분리해 주세요.

🔧 제안 수정
-        {feeds.isLoading && feeds.items.length === 0 ? (
+        {feeds.error ? (
+          <EmptyState>
+            <EmptyTitle>피드를 불러오지 못했어요.</EmptyTitle>
+            <EmptySubText>{feeds.error}</EmptySubText>
+          </EmptyState>
+        ) : feeds.isLoading && feeds.items.length === 0 ? (
           <FeedPostContainer>
             {Array.from({ length: 3 }).map((_, i) => (
               <FeedPostSkeleton key={i} />
             ))}
           </FeedPostContainer>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/pages/searchBook/SearchBook.tsx` around lines 258 - 296, The conditional
render for feed results treats API errors as "no data"; update the JSX branch
that currently checks feeds.isLoading and feeds.items to first check feeds.error
(e.g., if (feeds.error) ...) and render an error state/component/message instead
of the EmptyState when feeds.error is truthy; ensure you still handle
feeds.isLoading, feeds.items.length === 0, feeds.isLast, feeds.sentinelRef, and
feeds.isLoadingMore as before and reference the same symbols (feeds.error,
feeds.items, feeds.isLoading, feeds.isLast, feeds.sentinelRef,
feeds.isLoadingMore, FeedPostContainer, EmptyState) so error vs empty result are
clearly separated.
src/pages/groupSearch/GroupSearch.tsx (1)

138-163: ⚠️ Potential issue | 🔴 Critical

검색어가 빠르게 바뀔 때 최신 쿼리 재조회가 누락됩니다.

reloadKey 변경 시 useInifinieScroll 훅의 useEffect(lines 90-96)가 실행되어 기존 데이터를 클리어한 후 loadFirstPage()를 호출합니다. 하지만 이전 요청이 진행 중이면 loadFirstPage()의 가드문(line 54: if (!enabled || isFetchingRef.current) return;)에서 조기 종료되어 새 검색 조건의 요청이 시작되지 않습니다. 그 결과 데이터는 비워지지만 새 결과는 로드되지 않은 상태가 유지되다가, 이전 요청이 완료될 때 오래된 결과가 화면에 표시됩니다.

GroupSearch 컴포넌트에서 reloadKeydebouncedSearchTerm, selectedFilter, category, searchStatus의 변화에 따라 자주 갱신되므로, 사용자가 빠르게 검색어를 입력할 때 이 문제가 발생할 가능성이 높습니다.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/pages/groupSearch/GroupSearch.tsx` around lines 138 - 163, The hook
useInifinieScroll can leave an inflight fetch blocking loadFirstPage due to
isFetchingRef, causing stale/empty UI when reloadKey changes; modify
useInifinieScroll so that when reloadKey (or the effect that clears data) runs
it aborts any in-progress request and resets isFetchingRef so a new
loadFirstPage can start: add an AbortController per fetch request (created
before calling the provided fetchPage), pass its signal into fetchPage or ensure
fetchPage supports cancellation, call controller.abort() in the cleanup path
triggered by reloadKey changes, and set isFetchingRef.current = false after
abort so loadFirstPage's guard (if (!enabled || isFetchingRef.current) return)
won’t short-circuit the new request; reference useInifinieScroll, loadFirstPage,
isFetchingRef, fetchPage, and reloadKey when implementing.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/pages/feed/FollowerListPage.tsx`:
- Around line 26-33: The current useInifinieScroll call always sets enabled:
true which triggers fetches for invalid route values; change the enabled flag to
only activate when the route param is a valid type (e.g., check type against an
allowed set like 'followerlist'/'followinglist') and userId is present as
needed; update the useInifinieScroll invocation (symbols: useInifinieScroll,
enabled, reloadKey, fetchPage, type, userId) so enabled becomes a boolean
expression that validates type (and userId where required) before running
fetchPage.

---

Outside diff comments:
In `@src/pages/feed/FollowerListPage.tsx`:
- Around line 69-99: The UI hides backend errors by never rendering
userList.error, so when fetching fails the page looks like an empty list; update
FollowerListPage to branch on userList.error before the skeleton/items render
(check userList.error from the same data source used for
userList.isLoading/isLast/isLoadingMore) and render an explicit error state
inside the UserProfileList (or replace it) with a readable message and retry
affordance; ensure the error branch references the same symbols (userList.error,
userList.sentinelRef, UserProfileList, UserProfileItemSkeleton, LoadingSpinner)
and preserves existing loading and pagination UI for other branches.

In `@src/pages/groupSearch/GroupSearch.tsx`:
- Around line 138-163: The hook useInifinieScroll can leave an inflight fetch
blocking loadFirstPage due to isFetchingRef, causing stale/empty UI when
reloadKey changes; modify useInifinieScroll so that when reloadKey (or the
effect that clears data) runs it aborts any in-progress request and resets
isFetchingRef so a new loadFirstPage can start: add an AbortController per fetch
request (created before calling the provided fetchPage), pass its signal into
fetchPage or ensure fetchPage supports cancellation, call controller.abort() in
the cleanup path triggered by reloadKey changes, and set isFetchingRef.current =
false after abort so loadFirstPage's guard (if (!enabled ||
isFetchingRef.current) return) won’t short-circuit the new request; reference
useInifinieScroll, loadFirstPage, isFetchingRef, fetchPage, and reloadKey when
implementing.

In `@src/pages/searchBook/SearchBook.tsx`:
- Around line 258-296: The conditional render for feed results treats API errors
as "no data"; update the JSX branch that currently checks feeds.isLoading and
feeds.items to first check feeds.error (e.g., if (feeds.error) ...) and render
an error state/component/message instead of the EmptyState when feeds.error is
truthy; ensure you still handle feeds.isLoading, feeds.items.length === 0,
feeds.isLast, feeds.sentinelRef, and feeds.isLoadingMore as before and reference
the same symbols (feeds.error, feeds.items, feeds.isLoading, feeds.isLast,
feeds.sentinelRef, feeds.isLoadingMore, FeedPostContainer, EmptyState) so error
vs empty result are clearly separated.

ℹ️ Review info

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Jira integration is disabled

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between f65968e and c2e7b8b.

📒 Files selected for processing (6)
  • src/hooks/useFeedCache.ts
  • src/pages/feed/FollowerListPage.tsx
  • src/pages/groupSearch/GroupSearch.tsx
  • src/pages/memory/Memory.tsx
  • src/pages/mypage/SavePage.tsx
  • src/pages/searchBook/SearchBook.tsx
✅ Files skipped from review due to trivial changes (1)
  • src/pages/mypage/SavePage.tsx

Copy link
Member

@ljh130334 ljh130334 left a comment

Choose a reason for hiding this comment

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

수고하셨습니다! 스크롤 복원ㄴ 문제를 sessionStorage 캐싱으로 해결한 접근 너무 좋은 것 같네요 ㅎㅎ TTL이나 디바운스 같은 엣지 케이스 챙긴 부분도 꼼꼼하신 것 같아요! 👍🏻 👍🏻 💯

Copy link
Collaborator

@heeeeyong heeeeyong left a comment

Choose a reason for hiding this comment

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

무한스크롤 데이터를 세션스토리지에 캐싱하는 것뿐만 아니라, scrollY까지 함께 저장해 페이지 복원 UX를 완성한 점이 좋았습니다. requestAnimationFrame을 통해 복원 타이밍을 늦춰준다는 새로운 내용도 배워갑니다👍

@ho0010 ho0010 merged commit 7bbe94e into refactor Feb 27, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants