Daily/Weekly 루틴을 나누고, 캘린더 일자별 화면에 깔았다
1179줄짜리 화면을 둘로 쪼개면서 공유 utility를 추출했고, 그 결과로 캘린더 일자별 화면에 routine 배경 표시도 자연스럽게 들어갔습니다.
Daily/Weekly 루틴을 나누고, 캘린더 일자별 화면에 깔았다
시작: “Daily Routine과 Weekly Routine을 나눌까?”
기존 list-daily-routine.tsx는 한 화면 안에서 Daily/Weekly를 토글로 전환하는 구조였습니다. 데이터 모델은 이미 양쪽을 다 지원했고, UI도 동작하긴 했어요.
그런데 토글 안에 Weekly가 묻혀 있다는 느낌이 들었습니다. 동등한 비중으로 두려면 별도 화면으로 나누는 게 낫겠다 싶어서, 분리하기로 했습니다.
1179줄
원래 파일이 1179줄이었습니다. Daily 단일 timeline view, Weekly 7-day grid view, 단일 요일 뷰, 드래그/리사이즈 핸들러, 블록 충돌 해결 알고리즘, 시간 변환 헬퍼… 다 한 파일에. 그래서 그냥 토글만 빼고 끝나는 작업이 아니었습니다.
선택지를 셋 놓고 봤습니다:
- A) 클린 분리: 공유 utility를
utils/routine/util.ts로 추출하고, 두 화면을 독립적으로 - B) 빠른 분리: 파일 복사해서 각자 자기 period만 남기기 (코드 중복)
- C) 라우트만 분리: 같은 컴포넌트를 두 라우트로
A로 갔습니다.
추출
공유 utility로 뽑은 것들:
- 상수:
HOUR_HEIGHT,TIME_LABEL_WIDTH,BLOCK_GAP,HOURS,DAYS등 - 타입:
Block - 함수:
formatHour,formatTime,snapToGrid,pxToTimeDate,getDayKey,buildBlockFromRoutine,assignOverlapColumns,buildBlocks,buildWeeklyBlocks
buildBlocks는 routine 배열을 받아 시간 충돌까지 자동 해결하는 핵심 함수라서, 이걸 utility로 빼는 게 가장 큰 이득이었습니다.
그러고 나서:
list-daily-routine.tsx: Weekly 관련 코드 전부 제거. 1179줄 → 약 370줄.list-weekly-routine.tsx: 신규 생성. 요일 picker + ALL view + 단일 요일 view + Daily 배경 블록까지.- 메뉴:
menu-calendar에 “주간 루틴” 항목 추가.
”이게 자연스럽게 다른 데에서도 쓰일 것 같다”
분리하고 나니, calendar 탭에서 날짜를 탭하면 들어가는 일자별 화면(DayTimeTable)에서도 같은 routine을 배경으로 보여주면 좋겠다는 생각이 들었습니다.
기존 Weekly Routine view는 Daily routine을 faded 배경으로 깔아주거든요 — 컨텍스트 차원에서. 그런데 DayTimeTable은 정작 그게 빠져 있었습니다. “오늘 화요일에 어떤 routine이 발동되더라” 라는 정보를 보려면 따로 routine 화면으로 가야 했어요.
마침 buildBlocks가 utility로 빠져 있으니, DayTimeTable에도 호출만 하면 됩니다.
DailyTimelineView에 prop 추가
routineBackgroundBlocks?: RoutineBackgroundBlock[];
이걸 받으면 timeline 안에 task 블록보다 한 단계 아래 z-order로 faded 블록을 깔도록.
browse-calendar-date.tsx에서:
- 해당 날짜의 daily routine을 fetch (모든 날짜에 발동)
- 해당 날짜의 weekly routine을 fetch 후, 그 날짜의 요일과 매칭되는 것만 필터
- 두 개를 합쳐서
buildBlocks로 변환 routineBackgroundBlocks로 전달
여기서 한 가지 고민: 캘린더 일자별 화면에 들어갈 때마다 routine을 fetch하면 비용이 클 텐데?
캐시 vs 매번 fetch
옵션이 셋 있었습니다:
- (1) 매번 fetch (단순)
- (2) 단순 캐시 + 명시적 invalidate (atom)
- (3) 제대로 된 인프라 — SQLite + SSE
처음엔 매번 fetch로 코드를 넣어뒀는데, 이게 진짜 비싼지 자문하면서 (3)으로 가기로 했습니다. 하이브리드 형태로:
- SQLite는 안 함 (routine은 task_record처럼 hot한 데이터가 아님)
- atom 기반 캐시
- SSE 이벤트로 invalidation
SSE 이벤트 발행
서버에서 publish_create_reservation_event를 새로 만들고, 모든 routine 변경 endpoint(register, 9개 PUT mutation, delete)에서 publish 호출. 처음에는 패턴이 비슷하다 보니 read endpoint(gather, get)에까지 잘못 추가되어서 잠시 정리해야 했습니다.
def publish_create_reservation_event(achiever_id: str, action: str = "update", exclude_connection_id: Optional[str] = None):
publish_event(achiever_id, "create_reservation_updated", {"action": action}, exclude_connection_id)
exclude_connection_id는 None이라 본인 디바이스에도 echo back. 이건 의도한 동작입니다 — 본인이 변경하더라도 SSE로 받아서 캐시를 invalidate하면, mutation callsite마다 직접 invalidate를 호출할 필요가 없거든요.
클라이언트 쪽
// atoms.ts
export const dailyRoutinesCacheAtom = atom<CreateReservationModel[] | null>(null);
export const weeklyRoutinesCacheAtom = atom<CreateReservationModel[] | null>(null);
null = 미로드. 로드되면 배열로 채워짐.
// (tabs)/_layout.tsx
const handleReservationUpdate = () => {
setDailyRoutinesCache(null);
setWeeklyRoutinesCache(null);
};
sseClient.on("create_reservation_updated", handleReservationUpdate);
SSE 이벤트가 오면 두 atom을 null로 리셋. browse-calendar-date는 다음 진입 시 null을 보고 fetch 다시 함.
텍스트 가독성 디테일
routine 배경 블록을 처음 만들었을 때 opacity: 0.35를 부모 View에 줬더니, 안에 있는 텍스트까지 같이 흐려져서 안 보였습니다. 흐릿한 배경 + 또렷한 텍스트가 의도였는데.
분리:
- 외부 View: 위치/크기만, opacity 없음
- 내부 View (absolute fill): 배경색 + opacity → 흐리게 깔림
- 텍스트: full opacity로 또렷하게
같은 패턴을 Weekly Routine view에도 적용. ALL 뷰의 daily 배경, 단일 요일 뷰의 daily 배경 — 둘 다.
그러고 나서도 텍스트가 “조금 진하다”는 피드백 → NEUTRAL700 → NEUTRAL600 → NEUTRAL500 으로 두 단계 톤다운. 배경은 컨텍스트일 뿐이라 충분히 약하게.
Weekly Routine 진입 시 ALL 뷰
Weekly Routine 화면이 처음 열릴 때, 오늘 요일이 자동 선택되어 있었습니다. 사실 요일별 뷰보다 ALL 뷰가 한눈에 정보가 많아서, 기본을 ALL로 변경.
// before
const [selectedDay, setSelectedDay] = useState(today's day);
// after
const [selectedDay, setSelectedDay] = useState<CreateReservationDay | null>(null);
null이 ALL 뷰입니다.
정리
- 1179줄짜리 한 화면 → 두 화면 + 공유 utility로 분리
buildBlocks가 utility가 되니 DayTimeTable에서도 재사용- DayTimeTable에 routine 배경 표시 = “오늘 어떤 routine이 깔리는가”를 일자별 화면에서 바로 볼 수 있음
- SSE 기반 캐시 invalidation 인프라 추가 (overkill일 수도 있지만 게으르게 보면 가장 깔끔)
- 텍스트 가독성 + 기본 뷰 디테일 조정
리팩토링이 의도하지 않은 보너스를 줄 때가 있습니다. Daily/Weekly를 분리하려고 utility를 추출했더니, 그 utility가 다른 화면(DayTimeTable)에서 쓸 수 있는 형태였고 — 그러니까 자연스럽게 새 기능이 따라왔습니다.
원래는 분리만 하려고 했는데 어쩌다 보니 캘린더 일자별 화면 UX까지 개선됐네요. 이런 게 모놀리식 화면을 쪼개는 진짜 보상이라고 생각합니다. 코드 줄 수가 줄어드는 게 아니라, 다음에 쓸 수 있는 부품이 생기는 것.