react-calendar + Supabase 연동 오류 해결하기
지금 만들고 있는 개인 프로젝트에서 위 사진처럼 감정표시달력을 만들어보고 싶었다.
react-calendar 라이브러리를 사용했고 Supabase의 데이터를 가져와서 달력에 표시할 계획이었다.
내가 구현하려고 하는 기능은 비교적 단순하고 시각적 UI가 중요하다. 그래서 의존성이 가볍고 커스터마이징이 쉬운 라이브러리가 좋다고 생각했다.
처음에는 날짜를 클릭한다고 생각해서 react-datepicker 를 떠올렸으나, 감정 아이콘을 넣을 때 어려울 것 같았다.
react-calendar 는 tileContent={({ date }) => { ... }}
처럼 커스터마이징도 비교적 간단해서 빠르게 구현할 수 있을 것이라 판단했다.
yarn add react-calendar
export const getDiariesByMonth = async (userId: string, year: number, month: number) => {
const start = new Date(year, month - 1, 1).toISOString(); // month는 0-index
const end = new Date(year, month, 0, 23, 59, 59).toISOString();
const { data, error } = await supabase
.from("diaries")
.select("date, mood")
.eq("user_id", userId)
.gte("date", start)
.lte("date", end);
if (error) throw new Error(error.message);
return data || [];
};
"use client";
import Calendar from "react-calendar";
import "react-calendar/dist/Calendar.css";
import { useState } from "react";
import useUser from "@/hooks/useUser";
export default function CalendarView() {
const [selectedDate, setSelectedDate] = useState<Date | null>(null);
const [diaryMap, setDiaryMap] = useState<Record<string, string>>({});
const [viewDate, setViewDate] = useState(new Date());
const { user } = useUser();
const formatDate = (date: Date) => {
return date.toLocaleDateString("sv-SE"); // 'YYYY-MM-DD'
};
const handleChange = (value: Date) => {
setSelectedDate(value);
console.log("선택한 날짜", formatDate(value));
};
// 월별 감정 불러오기
useEffect(() => {
if (!user) return;
const fetchMonthlyDiaries = async () => {
const year = viewDate.getFullYear();
const month = viewDate.getMonth();
const data = await getDiariesByMonth(user.id, year, month);
const mapped: Record<string, string> = {};
data.forEach((entry) => {
mapped[formatDate(entry.date)] = entry.mood;
});
setDiaryMap(mapped);
};
fetchMonthlyDiaries();
}, [user, viewDate]);
return (
<div className="flex flex-col items-center">
<Calendar
locale="ko"
onClickDay={handleChange}
value={selectedDate}
calendarType="gregory"
formatDay={(locale, date) => date.getDate().toString()}
className="rounded-lg border p-2 shadow-md"
onActiveStartDateChange={({ activeStartDate }) => {
if (activeStartDate) setViewDate(activeStartDate);
}}
tileContent={({ date }) => {
const key = formatDate(date);
const mood = diaryMap[key];
return mood ? <img src={`/mood/${mood}.png`} alt={mood} className="mx-auto mt-1 h-4 w-4" /> : null;
}}
/>
</div>
);
};
viewDate
: 현재 보고 있는 달력의 monthonActiveStartDateChange
로 갱신diaryMap
의 Record
타입 : "2024-04-29": "happy"
이런 식으로 문자열 key
에 감정 문자열이 mapping 되도록 설정react-calendar을 사용하여 일기장을 달력 UI로 보여주고, 해당 날짜에 작성된 일기가 있다면 감정 아이콘을 렌더링하려고 했는데 문제는 감정 아이콘이 나타나지 않았다.
✅ diaryMap[key] 가 정상적으로 값이 들어가는지 확인하기
data.forEach((entry) => {
mapped[formatDate(entry.date)] = entry.mood;
});
✅ tileContent에 전달된 key가 정확히 일치하는지 확인하기
분명 일기 데이터가 들어가 있어야하는데 mood
가 undefined
로 출력됐다.
이 말은 날짜는 잘 들어갔는데 매핑했던 날짜에 해당하는 값이 없다는 뜻이라고 생각했다.
✅ Supabase 데이터 값 확인하기
data.forEach((entry) => {
console.log("Supabase entry.date:", entry.date);
console.log("Supabase row:", entry);
mapped[formatDate(new Date(entry.date))] = entry.mood;
});
console.log
로 확인해보려고 했는데 아무것도 찍히지 않았다.
그러면 data
자체는 잘 받아와지는지, useEffect
는 잘 실행되는지 디버깅해보았다.
✅ data
배열 확인
data
를 확인하니 빈 배열로 들어오고 있었고
useEffect
에서 user
같은 값이 설정되는지를 보니 이 부분에서는 문제가 없었다.
“useEffect
는 정상적으로 실행되어 user.id
나 viewDate
는 제대로 들어갔지만, Supabase에서 받아온 data
가 문제라는 것이다.”
export const getDiariesByMonth = async (userId: string, year: number, month: number) => {
// const start = new Date(year, month - 1, 1).toISOString();
// const end = new Date(year, month, 0, 23, 59, 59).toISOString();
const start = new Date(year, month, 1).toISOString(); // 해당 월 1일
const end = new Date(year, month + 1, 1).toISOString(); // 다음 달 1일
// ... 생략
🚨 기존 코드가 작동하지 않았던 이유
const start = new Date(year, month - 1, 1).toISOString();
const end = new Date(year, month, 0, 23, 59, 59).toISOString();
month - 1
: 이미 0-based로 처리된 month
가 다시 -1 처리 되어서 잘못 인식 됐을 수 있다. (ex 5월 → 4월)new Date(year, month, 0)
는 이번 달의 0일 , 즉 지난 달의 마지막날을 의미한다. 따라서 end
값은 실질적으로 이전 달의 마지막 날로 인식되어서 조회 시 문제가 발생할 수 있다.✏️ 바꾼 코드의 장점
month
는 4여야한다.month
값을 처리했다면 바꾼 코드로 했을 때 값 오류가 발생하지 않는다.⭐️ 요약하자면, 사용하는 month
값을 이미 1-12 형식으로 처리했다면 지금처럼 month
, month + 1
그대로 사용하는 방식이 정확하다.
원인을 수정하고 다시 브라우저를 확인하면 이런 TypeError 가 발생하는데,
이는 타입스크립트와 Supabase 데이터 타입 차이때문에 발생한 문제이다.
data.forEach((entry) => {
mapped[formatDate(entry.date)] = entry.mood;
});
만약 이렇게 작성했다면 타입 오류가 발생했을 확률이 높다.
selected(”date”)
로 가져오면 string
타입이다.entry.date
는 Date
객체가 아닌 문자열로 들어오게 된다. (ISO형식)전체적인 formatDate()
를 보면
const formatDate = (date: Date) => {
return date.toLocaleDateString("sv-SE"); // YYYY-MM-DD
};
이렇게 Date
객체가 들어오게 만들었기 때문에 new Date()
로 한 번 감싸서 문자열을 Date
객체로 변환시켜주면 해결된다.
data.forEach((entry) => {
mapped[formatDate(new Date(entry.date))] = entry.mood;
});
개인프로젝트를 하면서 써보지 않았던 라이브러리 사용하면서 다양한 경험을 해볼 수 있어서 좋다.
이것 저것 부담없이 사용해 볼 수 있어서 큰 도움이 되는 것 같다..!
프로젝트 방향성은 잃지 않으면서 다양한 라이브러리를 경험할 수 있도록 해야지
(이제 디자인 만지작하러 가자… 💨)