증상 (Symptom)
- onPrev/onNext 눌렀을 때 (화살표) 이전, 다음 요일로 넘어갔어야했다.
- 그런데 UI(식단 내용) 갱신 X
- 즉, 부모에서 idx는 바뀌지만 자식이 받는 첫 props가 “유효 데이터”가 아님
정리하면 API에서 데이터를 받아오면 나의 컴포넌트는 이런 순서로 동작했다.
- useMenuData()가 처음 렌더될 때는→ idx도 초깃값(null 또는 0)
- 아직 keys와 todayKey가 빈 상태([], '')
- React가 렌더를 수행 →빈 배열([]) 반환 → 자식 컴포넌트는 “메뉴 없음”만 보임.
- getByDate(dateKey)는 아직 유효한 키를 못 찾음
- 그 후 react-query가 API 응답을 받고 keys 갱신 →이때 제대로 “한 번만” todayKey에 맞춰야 정상 작동.
- useEffect가 실행되며 idx를 업데이트해야 함.
- 아래의 useEffect로 인해 화살표로 인해 keys 라는 상태가 변하면, todayKey로 계속 유지했었다.
- 따라서 화살표 로직은 작동은했지만, 아래 useEffect 때문에 계속 그대로였던 것.
useEffect(() => {
if (!keys.length) return;
const i = keys.indexOf(todayKey);
setIdx(i >= 0 ? i : 0); // ← keys나 todayKey가 바뀔 때마다 ‘무조건 리셋’
}, [keys, todayKey]);
원인 코드 (Before – 문제가 있던 버전)
아래가 “키 불일치”와 “강제 재초기화”가 겹쳐 첫 props 단계에서 빈 데이터가 내려간 핵심 패턴.
// (Before) MenuPage.tsx - 문제 패턴
const { groupedByDate, keys, todayKey, getByDate } = useMenuData();
// ❶ idx를 0으로 시작
const [idx, setIdx] = useState(0);
// ❷ keys/todayKey 바뀔 때마다 "항상" todayKey로 재설정 (되돌림 발생)
useEffect(() => {
if (!keys.length) return;
const i = keys.indexOf(todayKey); // ← todayKey가 "MM.DD (요일)" 이면 -1
setIdx(i >= 0 ? i : 0); // ← 결국 0으로 되돌림 (사용자 이동 무효화)
}, [keys, todayKey]);
// ❸ dateKey가 일시적으로 없으면 todayKey로 폴백(형식 불일치 위험)
// ex) keys[idx] = "10.10", todayKey = "10.10 (금)" → 불일치
const dateKey = keys[idx] ?? todayKey;
// ❹ 완전 일치(===) 비교라 키 포맷이 다르면 항상 실패 → []
const items = getByDate(dateKey); // getByDate("10.10 (금)") → []
// ↓ 자식으로 내려가지만 항상 같은 "빈 데이터"라 화면 변화가 없어 보임
<DailyMenuView dateKey={dateKey} items={items} onPrev={...} onNext={...} />
Root Cause?
- 키 포맷 불일치
- API 키: "MM.DD" (예: "10.10")
- UI 키: "MM.DD (요일)" (예: "10.10 (금)")
- getByDate는 === 비교 → "10.10" !== "10.10 (금)" → 항상 빈 배열([])
- {
"10.07 (월)": [...],
"10.08 (화)": [...],
"10.09 (수)": [...]
}
- idx 재초기화 루프
- useEffect([keys, todayKey])가 매 갱신마다 idx를 today로 덮어씀
- 사용자가 이동해도 즉시 0번으로 되돌아가 “안 옮겨지는 것처럼” 보임
- 첫 props 폴백이 비유효 키
- dateKey = keys[idx] ?? todayKey에서 todayKey가 실제 keys에 없는 문자열일 수 있음
- 그 상태로 getByDate(dateKey) 호출 → 항상 실패
const groupedByDate = Array.from(map.entries());
const keys = groupedByDate.map(([k]) => k);
keys = API 응답의 날짜 문자열 키 목록
- groupedByDate → { "10.07 (월)": MenuItem[], ... }
- keys → ["10.07 (월)", "10.08 (화)", ...]
- getByDate(key) → "10.08 (화)"로 조회 시 해당 메뉴 배열 반환
안정화 패턴 (키 정규화, 최초 1회 초기화)
이 구조의 의미
- “최초 1회”: API 응답이 처음 들어왔을 때만 idx를 todayKey로 설정
- 이후에는 onPrev / onNext가 직접 상태를 관리 (React의 자연스러운 단방향 흐름 유지)
- 즉, props와 state의 책임이 명확히 분리됨
- useMenuData는 “데이터 제공”
- MenuPage는 “현재 인덱스 상태 관리”
- 즉 idx(최초 idx)가 null일때만 today , 아니라면 idx의 값을 강제로 할당
❌ 이전 | useEffect가 keys/todayKey 바뀔 때마다 idx를 덮어씀 | onNext()가 즉시 무효화됨 |
✅ 수정 후 | 최초 1회만 idx를 today로 설정, 이후엔 유지 | props가 갱신돼도 화면 상태 유지 |
⚙️ 역할 | 비동기 데이터 최초 로드 타이밍 보정 | 첫 렌더에 “빈 화면” 방지, 이후엔 UX 안정 |
// (After) MenuPage.tsx - 안정 패턴
import { useEffect, useState } from 'react';
import { useMenuData } from '@/hooks/menu/useMenuData';
import { DailyMenuView } from '@/components/atoms/Meal/Mobile/DailyMenuView';
import WeeklyMenuView from '@/components/atoms/Meal/Web/WeeklyMenuView';
import LoadingIndicator from '@/components/atoms/LoadingIndicator';
export default function MenuPage() {
const { isLoading, error, groupedByDate, keys, todayKey, getByDate } = useMenuData();
// ❶ idx는 null로 시작 → "최초 1회"만 todayKey로 세팅
const [idx, setIdx] = useState<number | null>(null);
useEffect(() => {
if (!keys.length) return;
setIdx(prev => {
if (prev === null) {
const i = keys.indexOf(todayKey); // todayKey는 반드시 keys 내부 "실제 값"
return i >= 0 ? i : 0; // 최초 1회만 today로
}
// keys 길이 변화 시 범위 보정(재초기화 금지)
const max = Math.max(0, keys.length - 1);
return Math.min(Math.max(prev, 0), max);
});
}, [keys, todayKey]);
if (isLoading || idx === null) return <LoadingIndicator />;
if (error) return <div className='p-4 text-red-500'>식단 로딩 실패</div>;
// ❷ dateKey는 항상 keys[idx] (폴백으로 todayKey 넣지 않음)
const dateKey = keys[idx];
// ❸ 유효 키로 조회 → 첫 props부터 실제 데이터가 내려감
const items = getByDate(dateKey) ?? [];
// ❹ 엣지 UX: 양끝 no-op + 버튼 disabled는 자식에서 처리 가능
const onPrev = () => setIdx(i => Math.max(0, (i ?? 0) - 1));
const onNext = () => setIdx(i => Math.min(keys.length - 1, (i ?? 0) + 1));
return (
<div className='...'>
{/* 모바일 */}
<DailyMenuView
key={dateKey} // 날짜 변경 시 자식 리마운트(디버그/안정)
dateKey={dateKey}
items={items}
nowCategory={/* ... */}
onPrev={onPrev}
onNext={onNext}
/>
{/* 데스크톱 */}
<WeeklyMenuView
groupedByDate={groupedByDate}
todayKey={todayKey}
nowCategory={/* ... */}
/>
</div>
);
}
문제는 props 전달이 아니라, props가 가리키던 “키 문자열”이 데이터와 안 맞았던 것.
구분설명결과
❌ 이전 | useEffect가 keys/todayKey 바뀔 때마다 idx를 덮어씀 | onNext()가 즉시 무효화됨 |
✅ 수정 후 | 최초 1회만 idx를 today로 설정, 이후엔 유지 | props가 갱신돼도 화면 상태 유지 |
⚙️ 역할 | 비동기 데이터 최초 로드 타이밍 보정 | 첫 렌더에 “빈 화면” 방지, 이후엔 UX 안정 |