카테고리 없음

사소하지만 잡기 어려운 키 불일치로 props 1단계 미적용 버그

Frisbeen 2025. 10. 10. 16:48

 증상 (Symptom)

  • onPrev/onNext 눌렀을 때 (화살표) 이전, 다음 요일로 넘어갔어야했다.
  • 그런데 UI(식단 내용) 갱신 X
  • 즉, 부모에서 idx는 바뀌지만 자식이 받는 첫 props가 “유효 데이터”가 아님

정리하면 API에서 데이터를 받아오면 나의 컴포넌트는 이런 순서로 동작했다.

 

  1. useMenuData()가 처음 렌더될 때는idx도 초깃값(null 또는 0)
  2. 아직 keystodayKey빈 상태([], '')
  3. React가 렌더를 수행 →빈 배열([]) 반환 → 자식 컴포넌트는 “메뉴 없음”만 보임.
  4. getByDate(dateKey)는 아직 유효한 키를 못 찾음 
  5. 그 후 react-query가 API 응답을 받고 keys 갱신 →이때 제대로 “한 번만” todayKey에 맞춰야 정상 작동.
  6. useEffect가 실행되며 idx를 업데이트해야 함.
  7. 아래의 useEffect로 인해 화살표로 인해 keys 라는 상태가 변하면, todayKey로 계속 유지했었다.
  8. 따라서 화살표 로직은 작동은했지만, 아래 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?

 

  1. 키 포맷 불일치
    • API 키: "MM.DD" (예: "10.10")
    • UI 키: "MM.DD (요일)" (예: "10.10 (금)")
    • getByDate=== 비교 → "10.10" !== "10.10 (금)" → 항상 빈 배열([])
    • {
        "10.07 (월)": [...],
        "10.08 (화)": [...],
        "10.09 (수)": [...]
      }
    •  
  2. idx 재초기화 루프
    • useEffect([keys, todayKey])가 매 갱신마다 idxtoday로 덮어씀
    • 사용자가 이동해도 즉시 0번으로 되돌아가 “안 옮겨지는 것처럼” 보임
  3. 첫 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 안정