Skip to content

Comments

[refactor] 카테고리 context를 zustand기반으로 마이그레이션#729

Merged
seongwon030 merged 4 commits intodevelop-fefrom
refactor/#720-category-button-rerender-solved-MOA-215
Sep 7, 2025
Merged

[refactor] 카테고리 context를 zustand기반으로 마이그레이션#729
seongwon030 merged 4 commits intodevelop-fefrom
refactor/#720-category-button-rerender-solved-MOA-215

Conversation

@seongwon030
Copy link
Member

@seongwon030 seongwon030 commented Sep 6, 2025

#️⃣연관된 이슈

ex) #720

📝작업 내용

이번 PR에서 작업한 내용을 간략히 설명해주세요(이미지/동영상 첨부 가능)

목표

  1. [refactor] 검색창 입력시 리렌더링되는 문제를 해결한다 #714 에서 도입한 zustand를 사용하여 카테고리 버튼 클릭 시 리렌더링 최소화
  2. context api 기반 코드를 zustand로 마이그레이션

Profiler로 성능 측정하기

기존 context api

스크린샷 2025-09-06 21 25 03

zustand 로 변경후

스크린샷 2025-09-06 21 43 55

결과

5ms에서 2.6ms로 약 50% 성능 개선을 하였습니다.

zustand storage 활용하기

카테고리 context 에서 세션스토리지에 카테고리 이름을 저장하는 로직이 있었습니다.
그래서 storage라는 객체에 get, set, remove 를 정의하였습니다.

        storage: {
          getItem: (name) => {
            const value = sessionStorage.getItem(name);
            return value ? JSON.parse(value) : null;
          },
          setItem: (name, value) => {
            sessionStorage.setItem(name, JSON.stringify(value));
          },
          removeItem: (name) => {
            sessionStorage.removeItem(name);
          },
        },

zustand-createjsonstorage를 보면 createJSONStorage()는 JSON 직렬화/역직렬화,
브라우저 저장소 저장 및 복원 기능을 자동으로수행합니다.

storage: createJSONStorage(() => sessionStorage)

그 결과 12줄의 코드가 1줄로 간소화되었습니다.

중점적으로 리뷰받고 싶은 부분(선택)

리뷰어가 특별히 봐주었으면 하는 부분이 있다면 작성해주세요

ex) 메서드 XXX의 이름을 더 잘 짓고 싶은데 혹시 좋은 명칭이 있을까요?

논의하고 싶은 부분(선택)

논의하고 싶은 부분이 있다면 작성해주세요.

🫡 참고사항

Summary by CodeRabbit

  • 신규 기능

    • 없음
  • 버그 수정

    • 없음
  • 리팩터링

    • 카테고리 선택 상태 관리를 컨텍스트에서 스토어 기반으로 전환하여 일관성과 유지보수성을 개선.
    • 메인 페이지, 카테고리 버튼 목록, 검색 상자에서 카테고리 상태 접근/갱신 경로를 스토어로 통일.
  • 사용자 영향

    • UI와 라우팅 구조는 동일하며 사용 흐름 변화 없음.
    • 선택한 카테고리는 세션 동안 계속 유지됨.
    • 검색 중에는 전체 카테고리로 검색되는 기존 동작이 그대로 유지됨.

- useCategoryStorage hook 및 storage 이벤트 리스너 제거
- persist 미들웨어로 sessionStorage 자동 동기화
- CategoryButtonList, SearchBox, MainPage에서 쓰이던 것 변경
@seongwon030 seongwon030 self-assigned this Sep 6, 2025
@seongwon030 seongwon030 added 🔨 Refactor 코드 리팩토링 💻 FE Frontend labels Sep 6, 2025
@vercel
Copy link

vercel bot commented Sep 6, 2025

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

Project Deployment Preview Comments Updated (UTC)
moadong Ready Ready Preview Comment Sep 6, 2025 1:00pm

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Sep 6, 2025

Warning

.coderabbit.yaml has a parsing error

The CodeRabbit configuration file in this repository has a parsing error and default settings were used instead. Please fix the error(s) in the configuration file. You can initialize chat with CodeRabbit to get help with the configuration file.

💥 Parsing errors (1)
Validation error: Invalid regex pattern for base branch. Received: "**" at "reviews.auto_review.base_branches[0]"
⚙️ Configuration instructions
  • 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

Walkthrough

React Context 기반 카테고리 상태를 제거하고, Zustand 스토어(useCategoryStore/useSelectedCategory)로 전환했다. CategoryProvideruseCategory를 삭제하고, 관련 소비자 컴포넌트의 훅/임포트를 교체했으며, App.tsx에서 Provider 래퍼를 제거했다. 상태는 sessionStorage에 지속된다.

Changes

Cohort / File(s) Summary
Context 제거
frontend/src/context/CategoryContext.tsx
카테고리 컨텍스트, CategoryProvider, useCategory 전면 삭제(세션 스토리지 동기화 및 storage 이벤트 리스너 포함).
스토어 도입
frontend/src/store/useCategoryStore.ts
Zustand 스토어 추가: selectedCategory, setSelectedCategory 제공. persist로 sessionStorage(category-storage)에 지속. useSelectedCategory 편의 훅 제공.
앱 래퍼 업데이트
frontend/src/App.tsx
CategoryProvider 래핑 제거. QueryClientProviderBrowserRouterGlobalStyles/Routes 구성 유지.
소비자 컴포넌트 마이그레이션
frontend/src/pages/MainPage/MainPage.tsx
useCategoryuseSelectedCategory로 교체. 이 컴포넌트에서는 selectedCategory만 사용하도록 변경.
소비자 컴포넌트 마이그레이션
frontend/src/pages/MainPage/components/CategoryButtonList/CategoryButtonList.tsx
useCategoryuseSelectedCategory. setSelectedCategory를 스토어에서 사용. 클릭 처리 동일.
소비자 컴포넌트 마이그레이션
frontend/src/pages/MainPage/components/SearchBox/SearchBox.tsx
useCategoryuseSelectedCategory. 검색 시 setSelectedCategory 사용 로직 유지.

Sequence Diagram(s)

sequenceDiagram
    participant UI as MainPage / SearchBox / CategoryButtonList
    participant Store as useCategoryStore (Zustand)
    participant Persist as sessionStorage ("category-storage")

    rect rgba(230,245,255,0.5)
    note over UI,Store: 카테고리 상태 접근/변경 (New)
    UI->>Store: select()로 selectedCategory 구독
    UI->>Store: setSelectedCategory(id)
    Store-->>UI: selectedCategory 업데이트(구독자 통지)
    end

    rect rgba(240,255,240,0.5)
    note over Store,Persist: 상태 지속 (New)
    Store-->>Persist: selectedCategory 저장(JSON)
    Persist-->>Store: 초기화 시 하이드레이션
    end
Loading

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

Assessment against linked issues

Objective Addressed Explanation
카테고리 버튼 상태를 Context API에서 Zustand로 변경 [MOA-215/#720]

Assessment against linked issues: Out-of-scope changes

(없음)

Possibly related issues

(없음)

Possibly related PRs

Suggested reviewers

  • lepitaaar
  • oesnuj
  • suhyun113
✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch refactor/#720-category-button-rerender-solved-MOA-215

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
Contributor

@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: 0

🧹 Nitpick comments (8)
frontend/src/store/useCategoryStore.ts (2)

1-6: 매직 스트링 상수화 + shallow로 중복 구독 제거 + persist 버전 지정 제안

  • 'all', 'category-storage'를 상수화하면 의도와 재사용성이 명확해집니다.
  • useSelectedCategory가 동일 스토어에 2번 구독합니다. shallow를 써서 한 번의 셀렉션으로 합치면 불필요한 구독을 줄일 수 있습니다.
  • persist에 version을 지정해 추후 마이그레이션을 수월하게 하세요.
 import { create } from 'zustand';
+import { shallow } from 'zustand/shallow';
 import {
   persist,
   subscribeWithSelector,
   createJSONStorage,
 } from 'zustand/middleware';
 
+export const CATEGORY_STORAGE_KEY = 'category-storage';
+export const DEFAULT_CATEGORY = 'all';
+
 export const useCategoryStore = create<CategoryStore>()(
   subscribeWithSelector(
     persist(
       (set) => ({
-        selectedCategory: 'all',
+        selectedCategory: DEFAULT_CATEGORY,
         setSelectedCategory: (category) => set({ selectedCategory: category }),
       }),
       {
-        name: 'category-storage',
+        name: CATEGORY_STORAGE_KEY,
+        version: 1,
         storage: createJSONStorage(() => sessionStorage),
       },
     ),
   ),
 );
 
-export const useSelectedCategory = () => {
-  const selectedCategory = useCategoryStore((state) => state.selectedCategory);
-  const setSelectedCategory = useCategoryStore(
-    (state) => state.setSelectedCategory,
-  );
-  return { selectedCategory, setSelectedCategory };
-};
+export const useSelectedCategory = () =>
+  useCategoryStore(
+    (s) => ({
+      selectedCategory: s.selectedCategory,
+      setSelectedCategory: s.setSelectedCategory,
+    }),
+    shallow,
+  );

Also applies to: 17-24, 28-34


21-23: sessionStorage 의존성: SSR/테스트 환경 호환성 확인 필요

현재는 SPA라 문제 없겠지만 SSR(예: Next.js) 도입 시 스토리지 접근 시점/하이드레이션 순서에 주의가 필요합니다. 필요 시 클라이언트에서만 storage를 주입하거나, 하이드레이션 콜백(onFinishHydration)으로 초기 렌더를 제어하는 패턴을 고려하세요.

SSR 계획이 있으시면 알려주세요. SSR-safe 초기화 스니펫을 준비해드릴게요.

frontend/src/pages/MainPage/components/CategoryButtonList/CategoryButtonList.tsx (2)

31-31: setter만 사용하는 컴포넌트는 무(無)구독 패턴으로 전환 가능

이 컴포넌트는 상태를 읽지 않고 setter만 사용하므로 getState로 액션을 꺼내오면 구독 자체를 없앨 수 있습니다.

-import { useSelectedCategory } from '@/store/useCategoryStore';
+import { useCategoryStore } from '@/store/useCategoryStore';
...
-  const { setSelectedCategory } = useSelectedCategory();
+  const setSelectedCategory = useCategoryStore.getState().setSelectedCategory;

Also applies to: 11-12


20-28: 불변성 보장: 카테고리 목록을 readonly로 선언

런타임 변경 가능성을 제거해 실수 수정을 방지합니다.

-const clubCategories: Category[] = [
+const clubCategories: readonly Category[] = [
frontend/src/pages/MainPage/components/SearchBox/SearchBox.tsx (2)

1-5: 이벤트명 상수화로 일관성 유지

다른 파일에서 EVENT_NAME을 쓰고 있으므로 여기서도 동일 상수로 맞추는 것을 권장합니다. 없다면 constants에 추가 부탁드립니다.

 import { useSearchInput } from '@/store/useSearchStore';
 import { useSelectedCategory } from '@/store/useCategoryStore';
 import useMixpanelTrack from '@/hooks/useMixpanelTrack';
 import SearchField from '@/components/common/SearchField/SearchField';
 import { useLocation, useNavigate } from 'react-router-dom';
+import { EVENT_NAME } from '@/constants/eventName';
...
-    trackEvent('Search Executed', {
+    trackEvent(EVENT_NAME.SEARCH_EXECUTED, {
       inputValue: inputValue,
       page: currentPage,
     });

Also applies to: 28-31


37-37: 불필요한 람다 제거

함수 참조를 그대로 넘겨도 됩니다.

-      onChange={(v) => setInputValue(v)}
+      onChange={setInputValue}
frontend/src/pages/MainPage/MainPage.tsx (2)

26-29: 매직 스트링 상수화로 의도 명확화

'OPEN' / 'all'을 상수로 두면 의미가 드러나고 오타를 줄일 수 있습니다.

+const ALL = 'all' as const;
+const OPEN = 'OPEN' as const;
...
-  const recruitmentStatus = isFilterActive ? 'OPEN' : 'all';
-  const division = 'all';
-  const searchCategory = isSearching ? 'all' : selectedCategory;
+  const recruitmentStatus = isFilterActive ? OPEN : ALL;
+  const division = ALL;
+  const searchCategory = isSearching ? ALL : selectedCategory;

60-70: 중첩 삼항 제거로 가독성 개선

가이드라인(중첩/복잡한 삼항 지양)에 맞춰 렌더링 분기를 함수로 분리하길 권장합니다.

   const clubList = useMemo(() => {
     if (!hasData) return null;
     return clubs.map((club: Club) => <ClubCard key={club.id} club={club} />);
   }, [clubs, hasData]);
+
+  const renderContent = () => {
+    if (isLoading) return <Spinner />;
+    if (isEmpty) {
+      return (
+        <Styled.EmptyResult>
+          앗, 조건에 맞는 동아리가 없어요.
+          <br />
+          다른 키워드나 조건으로 다시 시도해보세요!
+        </Styled.EmptyResult>
+      );
+    }
+    return <Styled.CardList>{clubList}</Styled.CardList>;
+  };
@@
-          {isLoading ? (
-            <Spinner />
-          ) : isEmpty ? (
-            <Styled.EmptyResult>
-              앗, 조건에 맞는 동아리가 없어요.
-              <br />
-              다른 키워드나 조건으로 다시 시도해보세요!
-            </Styled.EmptyResult>
-          ) : (
-            <Styled.CardList>{clubList}</Styled.CardList>
-          )}
+          {renderContent()}

Also applies to: 38-41

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • Jira 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 0ed2fa7 and bf26022.

📒 Files selected for processing (6)
  • frontend/src/App.tsx (2 hunks)
  • frontend/src/context/CategoryContext.tsx (0 hunks)
  • frontend/src/pages/MainPage/MainPage.tsx (2 hunks)
  • frontend/src/pages/MainPage/components/CategoryButtonList/CategoryButtonList.tsx (2 hunks)
  • frontend/src/pages/MainPage/components/SearchBox/SearchBox.tsx (1 hunks)
  • frontend/src/store/useCategoryStore.ts (1 hunks)
💤 Files with no reviewable changes (1)
  • frontend/src/context/CategoryContext.tsx
🧰 Additional context used
📓 Path-based instructions (2)
frontend/**/*.{ts,tsx}

📄 CodeRabbit inference engine (frontend/.cursorrules)

frontend/**/*.{ts,tsx}: Replace magic numbers with named constants for clarity.
Replace complex or nested ternary operators with if/else statements or IIFEs for readability.
Assign complex boolean conditions to named variables.
Use consistent return types for similar functions and hooks.
Avoid hidden side effects; functions should only perform actions implied by their signature (Single Responsibility Principle).
Use unique, descriptive names for custom wrappers and functions to avoid ambiguity.
Define constants near related logic or ensure names link them clearly.

Files:

  • frontend/src/store/useCategoryStore.ts
  • frontend/src/App.tsx
  • frontend/src/pages/MainPage/components/SearchBox/SearchBox.tsx
  • frontend/src/pages/MainPage/components/CategoryButtonList/CategoryButtonList.tsx
  • frontend/src/pages/MainPage/MainPage.tsx
frontend/**/*.tsx

📄 CodeRabbit inference engine (frontend/.cursorrules)

frontend/**/*.tsx: Abstract complex logic/interactions into dedicated components or higher-order components (HOCs).
Separate significantly different conditional UI/logic into distinct components.
Colocate simple, localized logic or use inline definitions to reduce context switching.
Choose field-level or form-level cohesion based on form requirements.
Break down broad state management into smaller, focused hooks or contexts.
Use component composition instead of props drilling.

Files:

  • frontend/src/App.tsx
  • frontend/src/pages/MainPage/components/SearchBox/SearchBox.tsx
  • frontend/src/pages/MainPage/components/CategoryButtonList/CategoryButtonList.tsx
  • frontend/src/pages/MainPage/MainPage.tsx
🧬 Code graph analysis (4)
frontend/src/App.tsx (1)
frontend/src/context/AdminClubContext.tsx (1)
  • AdminClubProvider (15-28)
frontend/src/pages/MainPage/components/SearchBox/SearchBox.tsx (2)
frontend/src/store/useSearchStore.ts (1)
  • useSearchInput (38-45)
frontend/src/store/useCategoryStore.ts (1)
  • useSelectedCategory (28-34)
frontend/src/pages/MainPage/components/CategoryButtonList/CategoryButtonList.tsx (1)
frontend/src/store/useCategoryStore.ts (1)
  • useSelectedCategory (28-34)
frontend/src/pages/MainPage/MainPage.tsx (1)
frontend/src/store/useCategoryStore.ts (1)
  • useSelectedCategory (28-34)
🔇 Additional comments (5)
frontend/src/store/useCategoryStore.ts (1)

13-26: Zustand + persist 구성 괜찮습니다

subscribeWithSelector까지 적용해 셀렉터 단위로 리렌더를 최소화한 점이 좋습니다. 초기 상태와 액션도 명확합니다.

frontend/src/App.tsx (2)

28-86: 라우트 트리 정리 LGTM

Provider 트리 간결해졌고, Admin 영역은 전용 Provider로 스코프가 분리되어 명확합니다.


28-86: CategoryProvider 관련 레거시 참조 제거 검증 완료
CategoryProvider, useCategory, @/context/CategoryContext 모든 참조가 삭제된 것을 확인했습니다.

frontend/src/pages/MainPage/components/CategoryButtonList/CategoryButtonList.tsx (1)

35-44: 이벤트 트래킹/검색 리셋 순서 적절

클릭 트래킹 → 검색 상태 리셋 → 카테고리 변경 흐름이 명확합니다. 👍

frontend/src/pages/MainPage/MainPage.tsx (1)

21-35: 조회 파라미터 결정 로직 깔끔합니다

검색 상태에 따라 카테고리를 'all'로 덮는 분기와 쿼리 훅 연결이 명확합니다.

Copy link
Contributor

@lepitaaar lepitaaar left a comment

Choose a reason for hiding this comment

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

수고하셧습니다. zustand 도입한거 좋은거같습니다

@seongwon030 seongwon030 merged commit c72d914 into develop-fe Sep 7, 2025
5 checks passed
@seongwon030 seongwon030 deleted the refactor/#720-category-button-rerender-solved-MOA-215 branch September 7, 2025 07:44
@coderabbitai coderabbitai bot mentioned this pull request Sep 11, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

💻 FE Frontend 🔨 Refactor 코드 리팩토링

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[refactor] MOA-215 카테고리 버튼 클릭 시 리렌더링 문제를 해결한다

2 participants