Skip to content

THIP-406: 검색창 UX 개선#304

Merged
ho0010 merged 5 commits intorefactorfrom
THIP-406
Feb 24, 2026
Merged

THIP-406: 검색창 UX 개선#304
ho0010 merged 5 commits intorefactorfrom
THIP-406

Conversation

@ho0010
Copy link
Collaborator

@ho0010 ho0010 commented Feb 19, 2026

#️⃣연관된 이슈

  • Jira

개요

모임 검색, 사용자 검색, 피드 페이지에서 발생하던 UX 버그들을 수정합니다.

작업 내용

모임 검색 (GroupSearch / GroupSearchResult)

  • 필터·카테고리 변경 시 목록이 깜빡이는 레이아웃 점프 수정
    • keepPrevious 옵션 도입으로 리스트 초기화 타이밍 조정
  • 결과 갱신 중 기존 목록을 opacity fade(0.45)로 표현
  • 검색 중 "전체 0" 카운트가 잘못 노출되던 문제 수정
  • 타이핑/Enter 시마다 "검색 중..." 문구가 반복 노출되던 플래시 수정

사용자 검색 (UserSearch / UserSearchResult)

  • 키워드 입력 직후 "찾는 사용자가 없어요" 문구가 먼저 노출되던 문제 수정
    • 결과가 없고 로딩 중이거나 미확정 상태면 "검색 중..." 표시
  • isEmpty 조건에 type !== 'searching' 가드 추가

피드 페이지 (MyFeedPage / OtherFeedPage)

  • 타인·본인 피드 페이지 진입 시 발생하던 흰 화면 플래시 제거
    • 로딩 중에는 다크 배경 LoadingScreen + 헤더만 렌더링

최근 검색어 (RecentSearchTabs)

  • API 응답 전 "최근 검색어가 아직 없어요" 문구가 노출되던 문제 수정
    • isLoading prop 추가 → 로딩 중 "불러오고 있습니다" 표시

영향 범위

  • GroupSearch.tsx, GroupSearchResult.tsx, GroupSearchResult.styled.ts
  • RecentSearchTabs.tsx
  • MyFeedPage.tsx, MyFeedPage.styled.ts, OtherFeedPage.tsx
  • UserSearch.tsx, UserSearch.styled.ts, UserSearchResult.tsx

자세한 내용은 아래 문서에 상세히 기술되어 있습니다.

https://github.com/THIP-TextHip/THIP-Web/wiki/%EA%B2%80%EC%83%89-UX-%EA%B0%9C%EC%84%A0

Summary by CodeRabbit

  • 새로운 기능

    • 검색 결과 재조회 중 시각적 피드백(불투명도 변화 및 전환) 추가
    • 최근검색 및 피드 페이지에 로딩 화면/로딩 메시지 UI 추가
  • 개선 사항

    • 검색 중 빈 상태 처리 개선(디바운스/초기 로딩 시 빈 상태 미노출)
    • 중간 입력 시 이전 검색 결과를 유지하는 동작 개선
    • 재검색과 초기 로딩을 구분해 총계 표시 제어 강화

- keepPrevious 옵션 도입: 재검색 시 기존 results를 유지한 채 API 호출
  → 글자 입력·Enter·필터·카테고리 변경 시 "검색 중..." 플래시 제거
- Content에 isRefetching prop 추가: 재검색 중 opacity 0.45 fade 처리
- Effect를 C/D로 분리
  · Effect C(searchStatus, searchTerm): 검색어·상태 전환 시 호출
  · Effect D(selectedFilter, category): 필터·카테고리 변경 시만 호출
- 카운트(전체 N) 초기 로딩 중 "전체 0" 표시 방지
- isEmpty 판단 시 type !== 'searching' 조건 추가
- OtherFeedPage, MyFeedPage 로딩 중 return <></> → LoadingScreen으로 교체
- LoadingScreen에 background-color: #121212 적용해 배경색 일치
- 로딩 중에도 TitleHeader 표시해 뒤로 가기 가능하도록 유지
- RecentSearchTabs에 isLoading prop 추가
- 로딩 중 "최근 검색어를 불러오고 있습니다." 표시
- UserSearch, GroupSearch에서 fetchRecentSearches 로딩 상태 관리 추가
- 검색 중(debounce 대기 or API 응답 전)에 "검색 중..." 표시
- UserSearchResult isEmpty 판단 시 type !== 'searching' 조건 추가
- LoadingMessage 스타일 컴포넌트 추가
@ho0010 ho0010 self-assigned this Feb 19, 2026
@vercel
Copy link

vercel bot commented Feb 19, 2026

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

Project Deployment Actions Updated (UTC)
thip Ready Ready Preview, Comment Feb 20, 2026 0:54am

@coderabbitai
Copy link

coderabbitai bot commented Feb 19, 2026

No actionable comments were generated in the recent review. 🎉


Walkthrough

검색·피드 관련 컴포넌트에 로딩·재검색 상태 플래그(isLoading, isRecentLoading, isRefetching, keepPrevious)를 추가하고, 로딩 UI(LoadingScreen, LoadingMessage)와 재검색 시 시각적 피드백(불투명도 전환) 및 결과 보존 로직을 도입했습니다.

Changes

Cohort / File(s) Summary
GroupSearchResult 스타일링 및 사용
src/components/search/GroupSearchResult.styled.ts, src/components/search/GroupSearchResult.tsx
Content에 제네릭 prop isRefetching?: boolean 추가, 재검색 시 opacity 0.45 전환 및 transition 적용. 컴포넌트에 isRefetching 전달 로직 추가 및 빈 상태/총합 표시 조건 조정.
RecentSearchTabs 로딩 상태 전파
src/components/search/RecentSearchTabs.tsx
isLoading?: boolean prop 추가, 로딩 시 전용 메시지 렌더링(빈 상태보다 우선).
그룹 검색 로직 및 상태 관리
src/pages/groupSearch/GroupSearch.tsx
isRecentLoading 상태 추가, searchFirstPage(..., keepPrevious?: boolean) 도입으로 재검색 중 기존 결과 보존 가능. 입력/필터/카테고리 변경에 따른 보존/갱신 로직과 useEffect 정비. isLoading/isRecentLoading를 하위 컴포넌트로 전달.
사용자 검색 로딩 UX
src/pages/feed/UserSearch.styled.ts, src/pages/feed/UserSearch.tsx, src/pages/feed/UserSearchResult.tsx
LoadingMessage 스타일 추가. isRecentLoading 도입으로 최근 검색 로딩 추적 및 RecentSearchTabs에 전달. 빈 상태 판정 로직 단순화(isEmpty).
피드 페이지 로딩 화면 추가
src/pages/feed/MyFeedPage.styled.ts, src/pages/feed/MyFeedPage.tsx, src/pages/feed/OtherFeedPage.tsx
LoadingScreen 스타일 컴포넌트 추가 및 로딩 분기에서 TitleHeader가 포함된 LoadingScreen 렌더링으로 변경(빈 fragment 대신).
패키지/매니페스트 변경
package.json
소량 라인 변경(+3/-1) — 메타 변경 포함.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant GroupSearch
    participant API
    participant GroupSearchResult
    participant RecentSearchTabs

    User->>GroupSearch: 입력/필터 변경 (중간/최종)
    GroupSearch->>GroupSearch: set isLoading / decide keepPrevious
    GroupSearch->>API: searchFirstPage(term, ..., keepPrevious)
    API-->>GroupSearch: results (rooms, total, nextCursor)
    GroupSearch->>GroupSearchResult: update props (rooms, isLoading, isRefetching)
    GroupSearch->>RecentSearchTabs: update props (recentSearches, isLoading)
    GroupSearchResult-->>User: render (opacity changes if isRefetching)
    RecentSearchTabs-->>User: render (loading message if isLoading)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested labels

✨ Feature

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 개선이라는 주요 변경사항을 명확하게 요약하고 있으며, 전체 changeset의 핵심 목적과 일치합니다.

✏️ 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 THIP-406

Warning

Review ran into problems

🔥 Problems

Errors were encountered while retrieving linked issues.

Errors (1)
  • THIP-406: Request failed with status code 403

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

Caution

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

⚠️ Outside diff range comments (1)
src/pages/groupSearch/GroupSearch.tsx (1)

48-66: ⚠️ Potential issue | 🟡 Minor

초기 마운트 시 최근 검색어 API 이중 호출

useEffect([], []) (Line 48)와 useEffect([searchStatus]) (Line 62)가 모두 마운트 시 실행됩니다. searchStatus의 초깃값이 'idle'이므로 두 이펙트가 동시에 getRecentSearch('ROOM')을 호출하게 되어 불필요한 중복 요청이 발생합니다.

Line 48의 IIFE 이펙트를 제거하고 fetchRecentSearches에 위임하는 것을 권장합니다.

♻️ 제안 변경
-  useEffect(() => {
-    (async () => {
-      setIsRecentLoading(true);
-      try {
-        const response = await getRecentSearch('ROOM');
-        setRecentSearches(response.isSuccess ? response.data.recentSearchList : []);
-      } catch {
-        setRecentSearches([]);
-      } finally {
-        setIsRecentLoading(false);
-      }
-    })();
-  }, []);
-
   useEffect(() => {
     if (searchStatus === 'idle') {
       fetchRecentSearches();
     }
   }, [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 48 - 66, Remove the IIFE
useEffect that directly calls getRecentSearch and instead delegate initial load
to the existing fetchRecentSearches helper so you don't double-call the API;
delete the useEffect containing the async IIFE that calls
getRecentSearch('ROOM') and rely on the useEffect that checks searchStatus ===
'idle' to invoke fetchRecentSearches (ensure fetchRecentSearches still calls
getRecentSearch, updates setRecentSearches and setIsRecentLoading and handles
errors).
🧹 Nitpick comments (7)
src/pages/feed/MyFeedPage.styled.ts (1)

10-16: LoadingScreen의 기본 레이아웃 스타일 중복 및 파일 위치 문제

LoadingScreenContainer와 동일한 min-width, max-width, margin 선언을 그대로 반복하고 있습니다. 또한 이 컴포넌트는 OtherFeedPage.tsx에서도 MyFeedPage.styled.ts로부터 임포트되어 사용되므로, MyFeedPage 전용 스타일 파일에 두는 것은 아키텍처 상 부적절합니다.

Emotion의 컴포넌트 확장 패턴을 사용하면 중복을 없앨 수 있으며, 공유 컴포넌트는 공용 파일로 이동하는 것이 적합합니다.

♻️ 제안: Container 확장 및 공유 위치로 이동

LoadingScreenMyFeedPage.styled.ts에서 독립된 공유 파일(예: src/pages/feed/FeedPage.styled.ts)로 이동하고, Container를 확장하여 중복을 제거합니다:

-export const LoadingScreen = styled.div`
-  min-width: 320px;
-  max-width: 767px;
-  margin: 0 auto;
-  min-height: 100dvh;
-  background-color: ${colors.black.main};
-`;
+export const LoadingScreen = styled(Container)`
+  min-height: 100dvh;
+  background-color: ${colors.black.main};
+`;

이후 MyFeedPage.tsxOtherFeedPage.tsx 모두 공유 파일에서 임포트합니다:

// 예: src/pages/feed/FeedPage.styled.ts 에 Container, LoadingScreen 통합
import { Container, LoadingScreen } from './FeedPage.styled';
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/pages/feed/MyFeedPage.styled.ts` around lines 10 - 16, LoadingScreen
duplicates Container layout and lives in a page-specific file while also being
imported by OtherFeedPage; fix by moving LoadingScreen into a shared feed styles
file (e.g., create FeedPage.styled.ts) and implement it by extending the
existing Container using Emotion's extension (styled(Container)`min-height:
100dvh; background-color: ${colors.black.main};`) so only the differing rules
remain; update MyFeedPage.tsx and OtherFeedPage.tsx to import LoadingScreen (and
Container if needed) from the new shared file and remove the duplicate
declarations from MyFeedPage.styled.ts.
src/pages/feed/OtherFeedPage.tsx (1)

11-11: MyFeedPage.styled로부터의 교차 임포트 및 두 페이지 컴포넌트 간 코드 중복

OtherFeedPage.tsxMyFeedPage.styled.ts에서 직접 LoadingScreen을 가져오는 것은 페이지 간 순환적 의존 위험을 내포하며, 파일 이름만으로는 공유 컴포넌트임을 알기 어렵습니다.

또한 MyFeedPage.tsxOtherFeedPage.tsx는 다음 요소가 완전히 동일합니다:

  • 상태 변수(feedData, profileData, loading, error)
  • useEffect 내 데이터 페치 로직 (Promise.all, 에러 처리, finally)
  • handleBackClick
  • 로딩/에러 분기 렌더링

OtherFeed에 전달하는 props(isMyFeed, isMyself, showFollowButton)만 다릅니다. 공유 데이터 페치 로직은 커스텀 훅으로 추출하고, LoadingScreen은 공용 스타일 파일로 이동하는 것을 권장합니다.

♻️ 제안: 커스텀 훅으로 공통 로직 추출
// src/hooks/useUserFeedData.ts
import { useState, useEffect } from 'react';
import { getOtherFeed, type OtherFeedItem } from '@/api/feeds/getOtherFeed';
import { getOtherProfile } from '@/api/users/getOtherProfile';
import type { OtherProfileData } from '@/types/profile';

export function useUserFeedData(userId: string | undefined) {
  const [feedData, setFeedData] = useState<OtherFeedItem[]>([]);
  const [profileData, setProfileData] = useState<OtherProfileData | null>(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState<string | null>(null);

  useEffect(() => {
    const loadOtherData = async () => {
      if (!userId) {
        setError('사용자 ID가 없습니다.');
        setLoading(false);
        return;
      }
      try {
        setLoading(true);
        const [feedResponse, profileResponse] = await Promise.all([
          getOtherFeed(Number(userId)),
          getOtherProfile(Number(userId)),
        ]);
        setFeedData(feedResponse.data.feedList);
        setProfileData(profileResponse.data);
        setError(null);
      } catch (err) {
        console.error('다른 사용자 데이터 로드 실패:', err);
        setError('사용자 정보를 불러오는데 실패했습니다.');
      } finally {
        setLoading(false);
      }
    };
    loadOtherData();
  }, [userId]);

  return { feedData, profileData, loading, error };
}

이후 각 페이지 컴포넌트에서:

const { feedData, profileData, loading, error } = useUserFeedData(userId);

그리고 LoadingScreensrc/pages/feed/FeedPage.styled.ts(또는 동등한 공유 파일)에서 임포트합니다.

Also applies to: 55-64

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

In `@src/pages/feed/OtherFeedPage.tsx` at line 11, OtherFeedPage imports
LoadingScreen from MyFeedPage.styled causing cross-file coupling and duplicates
data-fetching logic (feedData, profileData, loading, error, useEffect,
handleBackClick) between MyFeedPage and OtherFeedPage; extract the shared fetch
logic into a custom hook (e.g., useUserFeedData(userId) that returns {feedData,
profileData, loading, error}) and update both pages to call that hook, move
LoadingScreen into a shared styled module (e.g., FeedPage.styled) and change
imports in OtherFeedPage and MyFeedPage to import LoadingScreen from that shared
file, and remove duplicated useEffect/Promise.all/error/finally code and any
duplicate state variables from OtherFeedPage so it only handles page-specific
props (isMyFeed, isMyself, showFollowButton) and local UI handlers like
handleBackClick.
src/pages/feed/UserSearchResult.tsx (2)

25-40: onLoadMore prop 변경 시마다 IntersectionObserver가 재등록되는 문제

onLoadMore이 의존성 배열에 포함되어 있어, 부모 컴포넌트가 useCallback 없이 함수를 내려보내면 매 렌더링마다 새로운 참조가 생성되고 observer가 disconnect → 재등록됩니다. 이 방식은 의존성 배열에 상태값을 넣어 최신값을 확보할 수는 있지만, 매번 Observer API를 등록/해제하는 것은 이상적이지 않습니다.

콜백을 useRef에 저장하면 observer 인스턴스를 안정적으로 유지하면서도 최신 onLoadMore를 항상 참조할 수 있습니다.

♻️ 제안: onLoadMore를 ref로 관리하여 observer 재등록 방지
+  const onLoadMoreRef = useRef(onLoadMore);
+  useEffect(() => {
+    onLoadMoreRef.current = onLoadMore;
+  }, [onLoadMore]);
+
   useEffect(() => {
     const observer = new IntersectionObserver(
       entries => {
         if (entries[0].isIntersecting && hasMore && !loading && onLoadMore) {
-          onLoadMore();
+          onLoadMoreRef.current?.();
         }
       },
       { threshold: 0.1 },
     );

     if (observerRef.current) {
       observer.observe(observerRef.current);
     }

     return () => observer.disconnect();
-  }, [hasMore, loading, onLoadMore]);
+  }, [hasMore, loading]);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/pages/feed/UserSearchResult.tsx` around lines 25 - 40, The current
useEffect re-creates the IntersectionObserver whenever the onLoadMore prop
identity changes, causing unnecessary disconnects; fix by storing onLoadMore in
a ref (e.g., onLoadMoreRef = useRef(onLoadMore)), update onLoadMoreRef.current
inside a separate effect whenever prop changes, and change the observer callback
to call onLoadMoreRef.current() instead of onLoadMore; keep the
IntersectionObserver creation effect dependent only on stable values (hasMore,
loading) so the observer instance (observerRef) is not re-registered on every
onLoadMore prop change and still invokes the latest callback.

45-45: 빈 fragment <></> 대신 && 조건부 렌더링 사용 권장

♻️ 제안: 불필요한 빈 fragment 제거
-        {type === 'searching' ? <></> : <ResultHeader>전체 {searchedUserList.length}</ResultHeader>}
+        {type !== 'searching' && <ResultHeader>전체 {searchedUserList.length}</ResultHeader>}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/pages/feed/UserSearchResult.tsx` at line 45, In UserSearchResult, replace
the ternary that returns an empty fragment when type === 'searching' with
short-circuit rendering: instead of "{type === 'searching' ? <></> :
<ResultHeader>전체 {searchedUserList.length}</ResultHeader>}", use a conditional
that only renders ResultHeader when type is not 'searching' (e.g., "type !==
'searching' && <ResultHeader>전체 {searchedUserList.length}</ResultHeader>") so
you remove the unnecessary empty fragment while keeping the same behavior;
update the JSX where ResultHeader and searchedUserList are referenced.
src/pages/groupSearch/GroupSearch.tsx (1)

351-352: 로딩 조건 단순화 제안

두 조건 모두 rooms.length === 0을 공유하므로 인수분해하면 가독성이 향상됩니다.

♻️ 제안 변경
-            {(isLoading && rooms.length === 0) ||
-            (searchStatus === 'searching' && rooms.length === 0) ? (
+            {rooms.length === 0 && (isLoading || searchStatus === 'searching') ? (
               <LoadingMessage>검색 중...</LoadingMessage>
🤖 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 351 - 352, The render
condition duplicates rooms.length === 0; refactor the JSX conditional in
GroupSearch (where isLoading, searchStatus and rooms are used) by extracting the
shared check and combining the rest with OR — e.g., replace "(isLoading &&
rooms.length === 0) || (searchStatus === 'searching' && rooms.length === 0)"
with "rooms.length === 0 && (isLoading || searchStatus === 'searching')" so the
intention is clearer and the expression is shorter.
src/components/search/GroupSearchResult.tsx (1)

93-94: 가독성 개선 제안: isLoading && !isRefetching 표현식 단순화

isRefetching = isLoading && mapped.length > 0이므로, isLoading && !isRefetchingisLoading && mapped.length === 0과 동치입니다. 중간 변수 isRefetching을 거치지 않고 직접 표현하면 의도가 더 명확하게 전달됩니다.

♻️ 제안 변경
-          {/* 재검색 중엔 이전 카운트를 유지하고, 초기 검색 완료 시 카운트를 표시 */}
-          <GroupNum>{isLoading && !isRefetching ? '' : `전체 ${mapped.length}`}</GroupNum>
+          {/* 초기 로딩 중(이전 결과 없음)에는 카운트 숨기고, 재검색·완료 시 표시 */}
+          <GroupNum>{isLoading && mapped.length === 0 ? '' : `전체 ${mapped.length}`}</GroupNum>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/search/GroupSearchResult.tsx` around lines 93 - 94, The
conditional rendering for GroupNum is using the redundant boolean expression
isLoading && !isRefetching; replace it with the simpler equivalent isLoading &&
mapped.length === 0 to improve clarity: locate the JSX line that sets GroupNum
(using isLoading, isRefetching, mapped.length) and change the condition to
directly check mapped.length === 0 when isLoading, leaving the displayed string
`전체 ${mapped.length}` unchanged for all other cases.
src/pages/feed/UserSearch.styled.ts (1)

29-36: font-size 디자인 토큰 사용 권장

다른 styled 파일들(예: GroupSearchResult.styled.ts)은 ${typography.fontSize.sm} 같은 타이포그래피 토큰을 사용하는데, 여기는 16px가 하드코딩되어 있어 디자인 시스템과 일관성이 없습니다.

♻️ 제안 변경
-import { colors } from '@/styles/global/global';
+import { colors, typography } from '@/styles/global/global';

 export const LoadingMessage = styled.div`
   display: flex;
   justify-content: center;
   align-items: center;
   padding: 40px 20px;
   color: ${colors.white};
-  font-size: 16px;
+  font-size: ${typography.fontSize.md};
 `;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/pages/feed/UserSearch.styled.ts` around lines 29 - 36, The LoadingMessage
styled component uses a hardcoded font-size (16px); replace it with the
typography token (e.g., typography.fontSize.sm) and ensure the typography tokens
are imported at top of the file; update the font-size line in LoadingMessage to
use that token so it matches other components like GroupSearchResult.styled.ts.
🤖 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/UserSearch.tsx`:
- Around line 30-38: fetchRecentSearches currently sets loading true and awaits
getRecentSearch('USER') but doesn't check response.isSuccess and lacks a catch,
which can cause TypeErrors or unhandled rejections; update fetchRecentSearches
to wrap the await in a try/catch, in the try assert response.isSuccess (or that
response.data is defined) before calling
setRecentSearches(response.data.recentSearchList), call setRecentSearches only
on success, handle/log errors in the catch (so they don't bubble as unhandled
rejections), and keep setIsRecentLoading(false) in the finally block; reference
the fetchRecentSearches function, getRecentSearch call, setRecentSearches, and
setIsRecentLoading when making the changes.

In `@src/pages/feed/UserSearchResult.tsx`:
- Line 21: The empty-state branch in UserSearchResult uses a loading ternary
that can never be true given how UserSearch (parent) conditionally renders: when
type === 'searched' and searchedUserList.length === 0, loading is guaranteed
false; remove the unreachable loading branch inside the EmptyWrapper (the
ternary that returns '사용자 찾는 중...' vs '찾는 사용자가 없어요.') and always render the "찾는
사용자가 없어요." message for the isEmpty case; keep the isEmpty computation
(searchedUserList.length === 0 && type !== 'searching') and only change the
EmptyWrapper rendering logic in UserSearchResult.

---

Outside diff comments:
In `@src/pages/groupSearch/GroupSearch.tsx`:
- Around line 48-66: Remove the IIFE useEffect that directly calls
getRecentSearch and instead delegate initial load to the existing
fetchRecentSearches helper so you don't double-call the API; delete the
useEffect containing the async IIFE that calls getRecentSearch('ROOM') and rely
on the useEffect that checks searchStatus === 'idle' to invoke
fetchRecentSearches (ensure fetchRecentSearches still calls getRecentSearch,
updates setRecentSearches and setIsRecentLoading and handles errors).

---

Nitpick comments:
In `@src/components/search/GroupSearchResult.tsx`:
- Around line 93-94: The conditional rendering for GroupNum is using the
redundant boolean expression isLoading && !isRefetching; replace it with the
simpler equivalent isLoading && mapped.length === 0 to improve clarity: locate
the JSX line that sets GroupNum (using isLoading, isRefetching, mapped.length)
and change the condition to directly check mapped.length === 0 when isLoading,
leaving the displayed string `전체 ${mapped.length}` unchanged for all other
cases.

In `@src/pages/feed/MyFeedPage.styled.ts`:
- Around line 10-16: LoadingScreen duplicates Container layout and lives in a
page-specific file while also being imported by OtherFeedPage; fix by moving
LoadingScreen into a shared feed styles file (e.g., create FeedPage.styled.ts)
and implement it by extending the existing Container using Emotion's extension
(styled(Container)`min-height: 100dvh; background-color: ${colors.black.main};`)
so only the differing rules remain; update MyFeedPage.tsx and OtherFeedPage.tsx
to import LoadingScreen (and Container if needed) from the new shared file and
remove the duplicate declarations from MyFeedPage.styled.ts.

In `@src/pages/feed/OtherFeedPage.tsx`:
- Line 11: OtherFeedPage imports LoadingScreen from MyFeedPage.styled causing
cross-file coupling and duplicates data-fetching logic (feedData, profileData,
loading, error, useEffect, handleBackClick) between MyFeedPage and
OtherFeedPage; extract the shared fetch logic into a custom hook (e.g.,
useUserFeedData(userId) that returns {feedData, profileData, loading, error})
and update both pages to call that hook, move LoadingScreen into a shared styled
module (e.g., FeedPage.styled) and change imports in OtherFeedPage and
MyFeedPage to import LoadingScreen from that shared file, and remove duplicated
useEffect/Promise.all/error/finally code and any duplicate state variables from
OtherFeedPage so it only handles page-specific props (isMyFeed, isMyself,
showFollowButton) and local UI handlers like handleBackClick.

In `@src/pages/feed/UserSearch.styled.ts`:
- Around line 29-36: The LoadingMessage styled component uses a hardcoded
font-size (16px); replace it with the typography token (e.g.,
typography.fontSize.sm) and ensure the typography tokens are imported at top of
the file; update the font-size line in LoadingMessage to use that token so it
matches other components like GroupSearchResult.styled.ts.

In `@src/pages/feed/UserSearchResult.tsx`:
- Around line 25-40: The current useEffect re-creates the IntersectionObserver
whenever the onLoadMore prop identity changes, causing unnecessary disconnects;
fix by storing onLoadMore in a ref (e.g., onLoadMoreRef = useRef(onLoadMore)),
update onLoadMoreRef.current inside a separate effect whenever prop changes, and
change the observer callback to call onLoadMoreRef.current() instead of
onLoadMore; keep the IntersectionObserver creation effect dependent only on
stable values (hasMore, loading) so the observer instance (observerRef) is not
re-registered on every onLoadMore prop change and still invokes the latest
callback.
- Line 45: In UserSearchResult, replace the ternary that returns an empty
fragment when type === 'searching' with short-circuit rendering: instead of
"{type === 'searching' ? <></> : <ResultHeader>전체
{searchedUserList.length}</ResultHeader>}", use a conditional that only renders
ResultHeader when type is not 'searching' (e.g., "type !== 'searching' &&
<ResultHeader>전체 {searchedUserList.length}</ResultHeader>") so you remove the
unnecessary empty fragment while keeping the same behavior; update the JSX where
ResultHeader and searchedUserList are referenced.

In `@src/pages/groupSearch/GroupSearch.tsx`:
- Around line 351-352: The render condition duplicates rooms.length === 0;
refactor the JSX conditional in GroupSearch (where isLoading, searchStatus and
rooms are used) by extracting the shared check and combining the rest with OR —
e.g., replace "(isLoading && rooms.length === 0) || (searchStatus ===
'searching' && rooms.length === 0)" with "rooms.length === 0 && (isLoading ||
searchStatus === 'searching')" so the intention is clearer and the expression is
shorter.

- fetchRecentSearches에 isSuccess 확인 추가: API 실패 응답 시
  response.data 접근으로 발생하는 TypeError 방지
- fetchRecentSearches에 catch 블록 추가: 네트워크 오류 등
  예외가 unhandled rejection으로 전파되지 않도록 처리
- UserSearchResult의 isEmpty 조건 내 loading 분기 제거:
  부모에서 loading=true일 때 LoadingMessage가 렌더링되므로
  해당 경로는 도달 불가능한 데드 코드
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.

수고하셨습니다! 검색 플로우 전반에 걸쳐서 자잘한 버거들 한 번에 정리해주셔서 좋았습니다!! 👍🏻 💯

Comment on lines +29 to +30
opacity: ${({ isRefetching }) => (isRefetching ? 0.45 : 1)};
transition: opacity 0.15s ease;
Copy link
Member

Choose a reason for hiding this comment

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

오호 opacity로 전환되는 패턴 너무 좋은 것 같아요! 👍🏻

@heeeeyong
Copy link
Collaborator

boolean 조합을 Union 상태로 정리해서 상태 전이 모호성을 없앤 점이 좋았습니다!
또, effect를 역할별로 분리해 감시 범위를 나눠서 불필요한 중간 렌더를 줄인 것이 UX 측면에서 더 안정적이어진 것 같습니다!
고생하셨습니다~

@ho0010 ho0010 merged commit 9e017e2 into refactor Feb 24, 2026
3 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