[REFACTOR] 타이머 상태 관리 동기화 구조 정리 - #280
Conversation
- 타이머 mutation별로 흩어져 있던 쿼리 무효화 호출을 invalidateTimerProgress(일시정지/재개/연장)와 invalidateTimerFinish(완료/중지) 두 헬퍼로 통일했습니다 - 기존 invalidateTimerState를 invalidateTimerProgress로 이름을 맞춰 다른 화면(home/today)에도 동일하게 적용했습니다
- sessionStorage를 컴포넌트 로컬 상태로 직접 읽던 방식을 zustand 스토어로 옮겼습니다 - TimerPanel과 FocusSession처럼 이 훅을 각자 호출하는 화면들이 초과시간 상태를 서로 동기화해서 볼 수 있게 했습니다
- TimerPanel과 useFocusSession에 거의 동일하게 중복돼 있던 progress/overtime/분 단위 변환 계산을 useTimerProgress 훅으로 추출했습니다
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Timo Performance ReportBundle Size — timo-web
Lighthouse — timo-web
Image Optimization — timo-web
측정 커밋: |
|
@coderabbitai review |
- invalidateTimerProgress(일시정지/재개/연장)에서 빠졌던 invalidateStatistics 호출을 복원했습니다 - use-focus-session.ts가 같은 무효화 로직을 따로 손으로 하고 있어서 invalidateTimerProgress를 쓰도록 함께 정리했습니다
|
Important
This repository does not receive automatic reviews because it has fewer than 10 stars. ⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: ASSERTIVE Plan: Pro Plus Run ID: 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. Comment |
kimminna
left a comment
There was a problem hiding this comment.
타이머 상태는 역시 넘 어렵네요...
다만 구조 측면에서 더 리뷰를 달아보자면, 함수 내부에서 리액트 훅을 직접 호출하는지의 여부에 따라 hooks/ 폴더 내부에 구조화하는 기준을 조금 더 세우면 좋을 것 같아요!
천천히 리팩해봅쉬다~~
- 앞 커밋에서 새 파일 추가와 호출부 수정이 스테이징 누락으로 빠졌던 부분을 마저 커밋했습니다
- JSON.parse(raw)를 곧바로 as OvertimeBase로 단언하던 걸 unknown으로 받아 isOvertimeBase 타입가드로 좁히도록 바꿨습니다 - sessionStorage에서 읽은 외부 입력은 검증 전에 타입을 확정하면 안 된다는 리뷰 피드백을 반영했습니다
- completeTimer/stopTimer의 무효화 목록을 손으로 나열하던 걸 공유 헬퍼 invalidateTimerFinish(todoId)로 교체했습니다 - invalidateTodoDetail(todoId)는 date 파라미터가 없지만 React Query v5의 invalidateQueries는 기본이 prefix 매칭이라 date-scoped 캐시까지 함께 무효화됩니다 - 더는 쓰이지 않는 invalidateActiveTimer/invalidateHomeView 구조분해도 정리했습니다
- invalidateTimerProgress/invalidateTimerFinish에 여러 줄 JSDoc과 invalidateTimerFinish의 todoId에 @PARAM을 추가했습니다 - 동작 변화는 없고 문서만 보강했습니다
ISSUE 🔗
close #279
What is this PR? 🔍
타이머 상태 관리에서 발생하던 동기화 부담을 줄이기 위해, 쿼리 무효화 로직을 통일하고 overtime 상태를 zustand로 옮기고,
TimerPanel/useFocusSession에 중복돼 있던 진행률 계산을 공통 훅으로 추출했습니다.배경
TimerPanel,useFocusSession, home/today 훅 등 여러 화면에 각각 손으로 흩어져 있었습니다.activeTimer전체를 zustand로 미러링하는 방안도 검토했습니다. 하지만 이 경우 React Query 캐시와 zustand 스토어라는 진실 소스가 2개가 되어 이 둘을 맞추는 sync bridge 코드가 항상 필요해지고, 버그가 나면 캐시와 스토어가 서로 어긋나는 새로운 동기화 문제가 생깁니다. 게다가 이 방식은 실제로 겪은 문제(여러 화면에 걸친 무효화 목록 관리, 화면 간 파생 상태 중복)를 직접 해결해주지 않아서 채택하지 않았습니다. 대신 서버 상태가 아닌 것(overtime 기준값)만 zustand로 승격하고, 나머지는 React Query 구조 안에서 정리하는 쪽을 택했습니다.쿼리 무효화 로직
invalidateTimerProgress/invalidateTimerFinish두 헬퍼로 통일했습니다.TimerPanel의 mutation마다 무효화할 쿼리키 조합을 다르게 손으로 나열하고 있었고, 기존에 있던 통합 헬퍼(invalidateTimerState)는TimerPanel에서 쓰이지 않은 채 home/today 화면에서만 쓰이고 있어 이름과 실제 용도가 어긋나 있었습니다.invalidateTimerProgress(activeTimer+home+timeBoxes+statistics)로, 완료/중지처럼 종료되는 액션은invalidateTimerFinish(위 4개+today+focusTodo+선택적 todoDetail)로 나눴습니다.invalidateTimerState호출부도 동일한 조합만 하고 있어서, 동작 변화 없이invalidateTimerProgress로 이름만 맞췄습니다.useFocusSession도 startTimer/changeStatus/extendTimer에서 같은 조합을 직접 나열하고 있던 걸invalidateTimerProgress호출로 통일했습니다.invalidateTimerProgress에서 뺐었는데, 리뷰 과정에서 이 가정이 틀릴 수 있다는 피드백을 받아 statistics 무효화를 다시 포함했습니다.Overtime 상태 관리
overtimeBaseSeconds)을 sessionStorage 기반 컴포넌트 로컬 상태에서 zustand 스토어로 옮겼습니다.TimerPanel과useFocusSession이 각자useTimerOvertime을 호출하면 서로 다른 React 상태 인스턴스를 가지게 되어, 한쪽에서markOvertimeStart를 호출해도 다른 쪽은 자기timerId가 바뀌어useEffect가 재실행되기 전까지 반영되지 않는 화면 간 비동기화가 있었습니다.stores/timer/useTimerOvertimeStore.ts에{ timerId, baseSeconds }단일 상태를 두고, sessionStorage 읽기/쓰기는utils/timer/overtime-storage.ts로 분리했습니다(useAuthStore가token-manager.ts를 쓰는 기존 패턴과 동일).use-timer-overtime.ts는 이 스토어를 감싸는 얇은 훅으로 남겨 외부 API(useTimerOvertime(timer) => { overtimeBaseSeconds, markOvertimeStart })는 그대로 유지해 호출부 변경을 최소화했습니다.activeTimer자체(서버 상태)는 여전히 React Query가 유일한 소스이고, zustand는 서버에 없는 순수 클라이언트 상태(overtime 기준값)만 담당합니다.타이머 진행률 계산
TimerPanel과useFocusSession에 거의 동일하게 중복돼 있던 progress/overtime/분 단위 변환 계산을useTimerProgress훅으로 추출했습니다.todo.durationSeconds)만 다를 뿐 나머지 계산 로직이 완전히 동일하게 복붙돼 있어서, 한쪽만 고치고 다른 쪽을 놓칠 위험이 있었습니다.hooks/timer/use-timer-progress.ts가{ timer, overtimeBaseSeconds, fallbackPlannedSeconds? }를 받아{ plannedSeconds, remainingSeconds, progress, isOvertime, overtimeProgress, plannedMinutes, basePlannedMinutes, actualMinutes }를 반환합니다.TimerPanel은fallbackPlannedSeconds를 생략(0)하고,useFocusSession은todo?.durationSeconds ?? 0을 넘겨 두 화면의 유일한 차이를 파라미터로 흡수했습니다.To Reviewers
overtime 스토어를
{ timerId, baseSeconds }단일 값으로 뒀는데, 이건 활성 타이머가 한 번에 하나만 존재한다는 서버 정책(동시 시작 시 409)을 전제로 한 설계입니다. 이 전제가 맞는지 한번 봐주세요.pause/resume 시 서버 응답을 기다렸다가 무효화하는 방식은 이번 PR에서 그대로 뒀습니다(낙관적 업데이트는 의도적으로 범위에서 제외했고, 후속 작업으로 남겨뒀습니다).
UI 변경은 없고 내부 상태 관리 구조만 정리한 PR이라, 실제 로그인 후 타이머 pause/resume/extend/complete/stop 클릭 테스트는 프로덕션 API 인증이 필요해 제가 직접 하지 못했습니다 — 리뷰 시 한 번 확인 부탁드립니다.
Screenshot 📷
Test Checklist ✔
pnpm check-types통과pnpm lint통과/home,/today,/focus라우트가 500 없이 컴파일/응답되는지 확인 (curl)