react-calendar + Supabase 연동 트러블슈팅

하영·2025년 5월 24일
0

etc.

목록 보기
26/26

react-calendar + Supabase 연동 오류 해결하기

지금 만들고 있는 개인 프로젝트에서 위 사진처럼 감정표시달력을 만들어보고 싶었다.
react-calendar 라이브러리를 사용했고 Supabase의 데이터를 가져와서 달력에 표시할 계획이었다.

🤔 왜 react-calendar 를 사용했는가?

내가 구현하려고 하는 기능은 비교적 단순하고 시각적 UI가 중요하다. 그래서 의존성이 가볍고 커스터마이징이 쉬운 라이브러리가 좋다고 생각했다.

처음에는 날짜를 클릭한다고 생각해서 react-datepicker 를 떠올렸으나, 감정 아이콘을 넣을 때 어려울 것 같았다.

react-calendar 는 tileContent={({ date }) => { ... }} 처럼 커스터마이징도 비교적 간단해서 빠르게 구현할 수 있을 것이라 판단했다.


👩🏻‍💻 react-calendar 설치 및 Supabase 함수 작성

1. react-calendar 라이브러리 설치

yarn add react-calendar

2. API 함수 작성하기

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 || [];
};

3. CalendarView 컴포넌트 작성하기

"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 : 현재 보고 있는 달력의 month
  • onActiveStartDateChange 로 갱신
  • diaryMapRecord 타입 : "2024-04-29": "happy"이런 식으로 문자열 key에 감정 문자열이 mapping 되도록 설정

🚨 발생한 문제 + 원인 찾기

react-calendar을 사용하여 일기장을 달력 UI로 보여주고, 해당 날짜에 작성된 일기가 있다면 감정 아이콘을 렌더링하려고 했는데 문제는 감정 아이콘이 나타나지 않았다.


1. 데이터 값 들어가는지 확인

✅ diaryMap[key] 가 정상적으로 값이 들어가는지 확인하기

data.forEach((entry) => {
  mapped[formatDate(entry.date)] = entry.mood;
});

✅ tileContent에 전달된 key가 정확히 일치하는지 확인하기

분명 일기 데이터가 들어가 있어야하는데 moodundefined 로 출력됐다.
이 말은 날짜는 잘 들어갔는데 매핑했던 날짜에 해당하는 값이 없다는 뜻이라고 생각했다.


✅ 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.idviewDate는 제대로 들어갔지만, Supabase에서 받아온 data 가 문제라는 것이다.”


2. Supabase API 수정하기

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 값은 실질적으로 이전 달의 마지막 날로 인식되어서 조회 시 문제가 발생할 수 있다.

✏️ 바꾼 코드의 장점

  • JS Date 객체는 월(month)를 0-based로 처리한다. 따라서 5월을 입력하면 month는 4여야한다.
  • 만약 사용하는 곳에서 이미 0-based로 month 값을 처리했다면 바꾼 코드로 했을 때 값 오류가 발생하지 않는다.

⭐️ 요약하자면, 사용하는 month 값을 이미 1-12 형식으로 처리했다면 지금처럼 month, month + 1 그대로 사용하는 방식이 정확하다.


타입 오류 해결하기

원인을 수정하고 다시 브라우저를 확인하면 이런 TypeError 가 발생하는데,
이는 타입스크립트와 Supabase 데이터 타입 차이때문에 발생한 문제이다.

data.forEach((entry) => {
  mapped[formatDate(entry.date)] = entry.mood;
});

만약 이렇게 작성했다면 타입 오류가 발생했을 확률이 높다.

  • Supabase에서 selected(”date”)로 가져오면 string 타입이다.
  • entry.dateDate 객체가 아닌 문자열로 들어오게 된다. (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;
});

✅ 결과확인

개인프로젝트를 하면서 써보지 않았던 라이브러리 사용하면서 다양한 경험을 해볼 수 있어서 좋다.
이것 저것 부담없이 사용해 볼 수 있어서 큰 도움이 되는 것 같다..!
프로젝트 방향성은 잃지 않으면서 다양한 라이브러리를 경험할 수 있도록 해야지

(이제 디자인 만지작하러 가자… 💨)

profile
왕쪼랩 탈출 목표자의 코딩 공부기록

0개의 댓글