문제 상황
Cannot read properties of null (reading 'type')
TypeError: Cannot read properties of null (reading 'type')
at onEdit (CalendarWriteForm.js:xx)
- Memo 영역에서 “날짜 없는 일정”을 선택하고 → “수정” 버튼을 누르면
CalendarWriteForm 팝업이 열리고 , 기존 제목/내용/장소 등은 잘 채워진다. - 그런데 수정 후 “수정” 버튼을 누르면 저장이 안 되고 에러가 난다.
# 콘솔 에러
Cannot read properties of null (reading 'type')
TypeError: Cannot read properties of null (reading 'type')
at onEdit (CalendarWriteForm.js:xx)
=> 즉, selectedItem.type을 읽으려는데 그 순간 selectedItem이 null이라는 뜻.
수정 팝업도 그 데이터로 잘 떴는데, 저장하려고 하면 갑자기 selectedItem이 사라진다?
현재 SelectedItem 사용중인 곳들 확인
- Memo
- 날짜 없는 일정 / 시간 없는 일정 목록을 보여줌
- “수정” 버튼 클릭 시
→ ScheduleContext의 selectedItem, isEdit, popupOpen 등을 세팅
- CalendarWriteForm
- 작성/수정 폼
- ScheduleContext의 selectedItem, isEdit을 보고
- 값 채워 넣기
- 수정/저장 API 호출
- CalendarList
- 선택한 날짜의 “오늘의 일정” 목록
- 백엔드에서 가져온 일정들을
→ splitAndSortEvents로 정렬/분리하고
→ 제일 빠른 일정을 자동으로 selectedItem으로 설정하는 로직이 있음
- ScheduleContext
- 전역 상태: selectedDate, selectedItem, isEdit, sortedDailyEvents 등 저장
- 모든 컴포넌트가 여기 값을 보고/바꾸면서 움직임
문제의 핵심 → selectedItem을 여러 컴포넌트가 건드린다는 점
문제를 해결해보자
처음 에러는 이 부분에서 발생
if (selectedItem.type === "summary") {
await updateAiSchedule(selectedItem.id, formValue);
} else if (selectedItem.type === "local") {
await updateUserSchedule(selectedItem.id, formValue);
}
그래서 가장 먼저 한 일은:
- selectedItem이 null인지 체크하는 방어 코드 추가
- type이 없을 수도 있으니 기본값("summary") 지정
const onEdit = async (e) => {
e.preventDefault();
setError("");
if (!formValue.title || !formValue.eventTime) {
setError("제목/시간을 입력하세요.");
return;
}
// 1) selectedItem이 null이면 바로 종료
if (!selectedItem) {
console.error("수정할 일정이 없습니다.", selectedItem);
setError("수정할 일정 정보를 찾을 수 없습니다. 다시 시도해 주세요.");
return;
}
console.log(selectedItem, "타입넣기전");
// 2) type이 없으면 기본값 summary
const itemType = selectedItem.type ?? "summary";
console.log(selectedItem, "타입넣은후");
try {
if (itemType === "summary") {
await updateAiSchedule(selectedItem.id, formValue);
alert("AI 일정 수정 완료");
} else if (itemType === "local") {
await updateUserSchedule(selectedItem.id, formValue);
alert("일정 수정 완료");
} else {
await updateAiSchedule(selectedItem.id, formValue);
alert("일정 수정 완료");
}
dispatch({ type: "SET_ISEDIT", payload: false });
dispatch({ type: "SET_POPUPOPEN", payload: false });
} catch (err) {
console.error(err);
setError("수정 실패");
} finally {
trigger();
}
};
구현하면 “selectedItem이 null일 때” 에러가 터지지는 않고, 에러 메시지를 직접 보여줄 수 있게 된다.
→ 버튼을 눌러보니 "수정할 일정이 없습니다. null" 로그 뜸
electedItem이 언제 null이 되는가?
어느 순간에 selectedItem이 null로 바뀌는지 찾아서 해결해야함 → 로그 찍어보기
1. Memo – 수정 클릭 시
const editMemo = (memo) => {
console.log("[Memo/editMemo] SET_SELECTED_ITEM 디스패치 전, memo:", memo);
dispatch({
type: "SET_SELECTED_ITEM",
payload: memo,
});
console.log("[Memo/editMemo] SET_SELECTED_ITEM 디스패치 완료");
dispatch({ type: "SET_ISEDIT", payload: true });
dispatch({ type: "SET_ISWRITE", payload: false });
dispatch({ type: "SET_POPUPOPEN", payload: true });
};
2. ScheduleReducer – SET_SELECTED_ITEM 처리할 때
case "SET_SELECTED_ITEM": {
console.log(
"[ScheduleReducer] SET_SELECTED_ITEM 호출 ",
"\n 기존:", state.selectedItem,
"\n 새로:", action.payload
);
return {
...state,
selectedItem: action.payload,
};
}
3. CalendarWriteForm – 렌더될 때마다 확인
useEffect(() => {
console.log("[CalendarWriteForm] 렌더",
"\n isEdit:", isEdit,
"\n selectedItem:", selectedItem,
"\n selectedDate:", selectedDate
);
}, [isEdit, selectedItem, selectedDate]);
# 로그결과 👉 여기까지 완벽히 정상
[Memo/editMemo] SET_SELECTED_ITEM 디스패치 완료
[ScheduleReducer] SET_SELECTED_ITEM 호출
기존: []
새로: {id: 6, title: '...', ...}
[ScheduleReducer] SET_ISEDIT: true
[ScheduleReducer] SET_POPUPOPEN: true
[CalendarWriteForm] 렌더
isEdit: true
selectedItem: {id: 6, ...}
...
어딘가에서 SET_SELECTED_ITEM을 payload: null로 또 호출 확인
→ 그래서 Memo에서 선택한 일정이 한 번은 잘 들어갔다가, 곧바로 null로 덮어씌워지고 있었다.
[ScheduleReducer] SET_SELECTED_ITEM 호출
기존: {id: 6, ...}
새로: null //여기서 선택 일정이 날아감
[CalendarWriteForm] 렌더
isEdit: true
selectedItem: null
진짜 원인
CalendarList의 “자동 선택” 로직
→ SET_SELECTED_ITEM을 검색해보니, CalendarList에 이런 useEffect가 있었다
useEffect(() => {
// 아직 데이터가 없으면 패스
if (!dailyEvents && !summaryDailyEvents) return;
const { sortedDailyEvents, unsortedDailyEvents } = splitAndSortEvents(
dailyEvents.map((event) => ({ ...event, type: "local" })),
summaryDailyEvents.map((event) => ({ ...event, type: "summary" })),
"DailyEvents"
);
// 정렬/ 날짜, 시간 미정 리스트 저장
dispatch({ type: "SET_SORTED_DAILY_EVENTS", payload: sortedDailyEvents });
dispatch({
type: "SET_UNSORTED_DAILY_EVENTS",
payload: unsortedDailyEvents,
});
// 제일 빠른 일정 자동 선택
if (sortedDailyEvents.length > 0) {
dispatch({
type: "SET_SELECTED_ITEM",
payload: sortedDailyEvents[0],
});
} else {
if (!isEdit) {
dispatch({ type: "SET_SELECTED_ITEM", payload: null }); // ★ 문제 부분
}
}
}, [dailyEvents, summaryDailyEvents, dispatch]);
※ 여기서 중요한 포인트:
- sortedDailyEvents.length === 0 이고
- 여기서 사용하는 isEdit은 useRefresh()의 isEdit (ScheduleContext의 isEdit과는 다른 값)
- Memo에서 수정 모드(ScheduleContext.isEdit = true)를 켜더라도 CalendarList 입장에서는 isEdit이 false일 수 있다.
그 결과:
- Memo에서 수정 → selectedItem = { id: 6, ... }로 잘 들어감
- 하지만 그날에 정렬된 일정 배열이 비어 있는 타이밍에
CalendarList의 useEffect가 실행되면서 dispatch({ type: "SET_SELECTED_ITEM", payload: null }); - 코드를 한번더 보냄 → 아까 로그에 null
“자동으로 오늘의 첫 일정 선택해주는 로직”이, Memo에서 선택한 일정을 덮어버리고 있었다
해결 방법
수정 모드 / 이미 선택된 상태에서는 자동으로 건드리지 않기
- “자동 선택”은
- 수정 중이 아닐 때
- 아직 어떤 일정도 선택되지 않았을 때
→ 이 두 조건을 만족할 때에만 동작해야 한다.
- 진짜 수정 모드 여부는 useRefresh()가 아니라
**ScheduleContext의 isEdit**을 써야 한다
// 수정전
const {
selectedItem,
selectedDate,
summaryDailyEvents,
dailyEvents,
sortedDailyEvents,
} = useScheduleState();
const { trigger, refreshCount, isEdit } = useRefresh();
//수정후
const {
selectedItem,
selectedDate,
summaryDailyEvents,
dailyEvents,
sortedDailyEvents,
isEdit, // 수정 여부 가져오기 이동
} = useScheduleState();
const { trigger, refreshCount } = useRefresh();
자동 선택 useEffect에 조건 추가
useEffect(() => {
// 아직 데이터가 없으면 패스
if (!dailyEvents && !summaryDailyEvents) return;
const {
sortedDailyEvents: sorted, // 지역 변수 이름 겹치지 않도록 변경
unsortedDailyEvents,
} = splitAndSortEvents(
(dailyEvents || []).map((event) => ({ ...event, type: "local" })),
(summaryDailyEvents || []).map((event) => ({ ...event, type: "summary" })),
"DailyEvents"
);
// 정렬/ 날짜, 시간 미정 리스트 저장
dispatch({ type: "SET_SORTED_DAILY_EVENTS", payload: sorted });
dispatch({
type: "SET_UNSORTED_DAILY_EVENTS",
payload: unsortedDailyEvents,
});
// 1) 수정 모드일 땐 selectedItem 건들지 않기
if (isEdit) {
console.log("[CalendarList] isEdit=true, 자동 선택/초기화 스킵");
return;
}
// 2) 이미 선택된 일정이 있으면 그대로 두기
if (selectedItem) {
console.log("[CalendarList] selectedItem 이미 있음 → 자동 선택 스킵");
return;
}
// 3) 아무것도 선택 안되어 있을 때만 기본값 세팅
if (sorted.length > 0) {
dispatch({
type: "SET_SELECTED_ITEM",
payload: sorted[0],
});
} else {
dispatch({ type: "SET_SELECTED_ITEM", payload: null });
}
}, [dailyEvents, summaryDailyEvents, isEdit, selectedItem, dispatch]);
정리
- 전역 상태를 여러 컴포넌트가 “자동으로” 수정하면 충돌이 나기 쉽다.
- Memo에서 selectedItem을 설정해 놓았는데
- CalendarList의 자동 선택 로직이 다시 덮어버리면서 버그 발생
- “자동 기본값 세팅” 로직에는 항상 조건을 달자.
- 수정 모드인지(isEdit)
- 이미 선택된 값이 있는지(selectedItem) 체크한 뒤에만 자동 세팅
- 디버깅할 때는 “시간 순서대로 로그 찍기”가 최고다.
- Memo → reducer → CalendarWriteForm 렌더 → 다시 reducer
- 순서대로 로그를 찍어보니
“어디에서 null로 덮어쓰는지”가 정확히 보였다.
- 같은 이름의 상태(isEdit)가 여러 Context에 있으면 혼란을 부른다.
- 이번에도 useRefresh()의 isEdit vs ScheduleContext의 isEdit가 섞여 있었다.
- “수정 모드”와 관련된 건 한 Context에서만 관리하는 게 안전하다.
- 폼 쪽에서도 방어 코드는 항상 유용하다.
- selectedItem이 null일 수 있다는 가정 하에 방어 로직을 넣어두면
- 이런 문제를 디버깅할 때도 “죽지 않고” 에러 메시지를 우리가 통제할 수 있다
'Project > Project01.MailBuddy' 카테고리의 다른 글
| 일반 폼로그인(세션) → JWT 연동 프로세스 (0) | 2025.11.27 |
|---|---|
| detail , summary 이용 FAQ 페이지 구현 (0) | 2025.11.24 |
| loading overlay 구현 해보기 (0) | 2025.11.20 |
| 앵커 이용 Guide_Nav 만들어서 해당 section 이동 (0) | 2025.11.20 |
| Gmail 연동 중복 에러 처리 (0) | 2025.11.18 |