578줄짜리 친구 페이지를 탭 오케스트레이터로 쪼갠 이야기
친구 페이지 리디자인을 하면서 FriendListPage.tsx 한 파일에 몰려 있던 로직을 탭 단위로 나눴다. 578줄이 47줄로 줄고, 나머지는 탭·카드·모달 컴포넌트로 흩어졌다. 그때 썼던 패턴만 정리해 둔다.
리팩터링 전후
원래 한 파일에 친구 목록, 검색, 요청 송수신, 페르소나 상세 모달, 삭제 확인 다이얼로그가 다 들어가 있었다. 상태도 많고, 검색만 고치려 해도 목록 코드 근처를 건드리게 되고, 유저 카드 UI는 목록/검색/요청마다 비슷하게 중복돼 있었다.
지금은 페이지는 탭 상태 + 라우팅만 담당하고, 각 탭이 자기 데이터 fetching·상태·UI를 가진다.
FriendListPage.tsx— 47줄, 탭 오케스트레이터FriendTabs.tsx— 세그먼트 컨트롤 + 배지FriendListTab,FriendRequestsTab,FriendDiscoverTab— 탭별 비즈니스 로직FriendUserCard— 공통 유저 카드PersonaDetailModal— 페르소나 상세
총 줄 수는 파일이 나뉘면서 오히려 늘었지만, 파일당 평균과 한 파일 최대 줄 수는 크게 줄어서 읽기·수정이 훨씬 수월해졌다.
탭 오케스트레이터 + URL 파라미터
페이지는 탭 상태와 공통 데이터(배지용 요청 개수)만 관리하고, 실제 동작은 각 탭에 맡긴다.
탭 상태는 useState 대신 useSearchParams로 URL에 넣었다. ?tab=requests 같은 식이라 딥링크(알림에서 요청 탭으로 바로 이동), 뒤로가기, 새로고침 후 복원이 자연스럽다. 기본 탭(friends)은 파라미터 없이 /friends로 두고, 나머지만 쿼리로 넣었다.
URL 파라미터는 외부 입력이니까 화이트리스트로 검증한다. TAB_PARAM_MAP에 없는 값이 오면 기본 탭으로 폴백하는 식.
FriendUserCard — 통일 카드 + actions 주입
목록/요청/찾기에서 쓰는 유저 카드를 하나로 묶고, actions prop으로 탭마다 다른 버튼을 넣었다. 대화, 수락/거절, 요청 보내기 등 맥락에 따라 다른 액션만 넘기면 된다.
personaName은 React.ReactNode로 둬서, "대기 중" 같은 걸 <span className="text-amber-500">대기 중</span>처럼 스타일된 JSX로 넘길 수 있게 했다.
찾기 탭에서 관계 상태 O(1)로 쓰기
검색·추천 결과에서 "이 사람이 친구인지, 요청 보냈는지, 받았는지"에 따라 버튼이 달라져야 한다. 친구 목록·보낸/받은 요청을 TanStack Query로 이미 갖고 있으니까, 그걸로 Set/Map을 만들어 두고 getUserRelationStatus(userId)로 O(1) 조회한다. 받은 요청은 수락 시 request.id가 필요해서 userId → requestId Map을 썼다.
빈 상태에서 CTA
데이터가 없을 때 "데이터 없음"만 쓰지 않고, "친구 찾기" 버튼으로 discover 탭으로 넘어가게 했다. onNavigateDiscover는 페이지에서 handleTabChange('discover')를 넘겨서, 빈 상태에서도 다음 행동이 바로 보이게 했다.
리팩터링 순서
실제로 한 순서는 이랬다.
- FriendUserCard 먼저 뽑기 — 가장 단순하고 영향 범위 작음
- FriendTabs — 순수 UI
- 탭 컴포넌트 세 개 분리 — 비즈니스 로직 이동
- PersonaDetailModal 분리 — 상태 연결이 복잡한 부분은 나중에
- 페이지를 오케스트레이터로 축소 — 조각 다 준비된 뒤 조합
- 백엔드 API — 검색 결과에 페르소나 정보 포함하는 전용 엔드포인트 추가 (N+1 없이 IN 절로 한 번에 조회)
의존성이 적은 것부터 빼고, 페이지 구조는 조각이 갖춰진 다음에 바꾸는 게 중간에 깨진 상태를 짧게 가져간다.
언제 쪼개면 좋을지
파일이 300줄 넘고, 기능 변경이 들어오고, 같은 UI 패턴이 여러 곳에 반복되고, 새 기능 넣을 때마다 기존 코드에 손이 많이 가면 리팩터링을 고려해 볼 만하다. 이번은 "탭 기반 리디자인"이라는 기능 변경과 같이 진행해서, 구조 개선만 따로 하지 않고 흐름에 실었다.
탭 오케스트레이터, URL 기반 탭, 통일 카드 + actions, 관계 상태 Set/Map, 빈 상태 CTA 같은 패턴은 설정 페이지나 다른 다중 탭 화면에도 그대로 쓸 수 있다.