Calendar 만들기

김민기·2025년 1월 4일
0

달력이 필요하다...

사이드 프로젝트로 POS 시스템을 개발하던 중, 특정 날짜의 매출을 확인할 수 있는 달력 UI가 필요했다. 처음에는 라이브러리 사용을 고려했지만, 라이브러리를 설치해서 간단하게 사용하는 것보다 이를 직접 구현해보는 것이 더 좋은 학습 기회가 될 것 같아 자바스크립트로 직접 만들어봤다.

문제 상황

처음에는 간단할 것 같았지만, 몇 가지 복잡한 문제에 직면했다

월별 일수 차이

각 월의 마지막 날짜가 28일, 30일, 31일로 다양하다.
날짜는 고정이기 때문에 로직으로 계산할 수는 있지만, 좋은 방법이라고 생각하지 않는다.

연도별 요일 변화:

같은 날짜라도 연도에 따라 요일이 달라진다.
예를 들어
2020년 1월 31일: 금요일
2021년 1월 31일: 일요일
2022년 1월 31일: 월요일

달력의 시작점:

일반적인 달력에서 1일이 항상 첫 칸에 오지 않는다. 시작점을 계산해야 한다.

해결 방안

마지막 날짜 구하기

자바스크립트의 Date 객체를 활용하여 간단하게 해결할 수 있다
Date 객체에서 일(day)에 0을 전달하면 이전 달의 마지막 날을 반환한다.

new Date(2025, 1, 0) // 2025년 1월의 마지막 날 (1월 31일)

첫 날의 요일 찾기

달력의 시작점을 결정하기 위해 해당 월 1일의 요일을 알아야 한다

const firstDay = new Date(2025, 1, 1)
const day = firstDay.getDay();
// 0 (일요일) 부터 6 (토요일)까지의 값 반환

이 정보를 바탕으로 달력의 시작 지점을 조정할 수 있습니다.

달력 UI 구현

TailwindCSS, shadcn/ui 사용

Prop으로 전달받는 이유는, Calendar 컴포넌트에서 year, month, date를 단순히 받아서 사용하는것이 아니라 변경 가능하고 year, month, date를 다른 컴포넌트에서도 사용하기 때문이다.
만약 단순 보여주기용 Calendar 컴포넌트라면 다음과 같이 컴포넌트 내부에서 직접 계산해도 된다.
const today = new Date();
setYear(today.getFullYear());
setMonth(today.getMonth() + 1);
setDate(today.getDate());

"use client";
import { SquareChevronLeft, SquareChevronRight } from "lucide-react";
import { Dispatch, SetStateAction, useEffect, useState } from "react";

interface CalendarProps {
  year: number;
  setYear: Dispatch<SetStateAction<number>>;
  month: number;
  setMonth: Dispatch<SetStateAction<number>>;
  date: number;
  setDate: Dispatch<SetStateAction<number>>;
}

export const Calendar = ({
  year,
  setYear,
  month,
  setMonth,
  date,
  setDate,
}: CalendarProps) => {
  const [, setStartDate] = useState<Date>();
  const [, setLastDate] = useState<Date>();
  const [dayArray, setDayArray] = useState<string[]>([]);

  useEffect(() => {
    const start = new Date(year, month - 1, 1);
    const last = new Date(year, month, 0);

    setStartDate(start);
    setLastDate(last);

    const indentCount = start.getDay();
    const days = Array.from({ length: last.getDate() }, (_, i) =>
      (i + 1).toString()
    );
    for (let i = 0; i < indentCount; i++) {
      days.unshift(" ");
    }

    const moreDaysCount = 7 - (days.length % 7);

    for (let i = 0; i < moreDaysCount; i++) {
      days.push(" ");
    }

    setDayArray(days);
  }, [year, month]);

  return (
    <div className="flex flex-col gap-4">
      <div className="flex justify-between items-center px-16">
        <SquareChevronLeft
          onClick={() => {
            if (month === 1) {
              setYear((prev) => prev - 1);
              setMonth(12);
            } else {
              setMonth((prev) => prev - 1);
            }
          }}
        />
        <span>
          {year}{month}</span>
        <SquareChevronRight
          onClick={() => {
            if (month === 12) {
              setYear((prev) => prev + 1);
              setMonth(1);
            } else {
              setMonth((prev) => prev + 1);
            }
          }}
        />
      </div>

      <div className="grid grid-cols-7 gap-1 justify-center">
        {["일", "월", "화", "수", "목", "금", "토"].map((day, index) => (
          <div
            key={index}
            className={`text-center ${index === 0 ? "text-red-500" : ""}`}
          >
            {day}
          </div>
        ))}
        {dayArray.map((arr, i) => {
          return (
            <button
              key={i}
              className={`border aspect-square`}
              onClick={() => {
                setDate(+arr);
              }}
            >
              <span
                className={`w-full h-full flex justify-center items-center ${
                  date === +arr ? "bg-red-300 border rounded-full" : ""
                }`}
              >
                {arr}
              </span>
            </button>
          );
        })}
      </div>
    </div>
  );
};

grid 레이아웃을 사용하고 grid-cols-7을 통해 7개의 컬럼을 사용하도록 만들었다.
첫 번째 row 에는 "일", "월", "화", "수", "목", "금", "토" 배열이 들어가고
두 번째 row 부터는 dayArray의 내용이 채워진다.

useEffect에서 prop으로 전달 받은 year, month, date를 통해 dayArray를 계산하고 있다.
last는 new Date(year, month+1, 0) 를 통해 구할 수 있고

indentCount는 getDay() 를 통해 구할 수 있다.

indentCount 만큼 dayArray 앞에 " " 공백을 추가해주면 된다.

추가적으로 마지막에 공백을 추가해줘야한다.

예시 달력 이미지처럼 마지막 날짜의 요일이 토요일이 아닌 경우 앞의 공백도 필요하지만 뒤에 공백 또한 필요하게 된다.

뒤에 공백을 구하는 방법은 7에서 앞에 공백을 추가한 배열의 length를 구해서 7로 나눈 나머지값을 빼주면 된다.

const moreDayCount = 7 - (days.length % 7);
for(let i = 0; i < moreDaysCount; i++) {
  days.push(" ");
}

따라서 2025년 1월 달력을 구하면 다음과 같은 배열이 만들어진다.

완성된 달력 이미지

마치며...

자바스크립트에서 날짜를 구하기 위해서 Date()는 자주 사용했지만 마지막 날짜와 요일을 구하는 방법은 이번에 처음 알게되었고 라이브러리를 설치해서 사용하지 않고 직접 달력을 구현해보는 좋은 기회였다고 생각한다.
days를 구할 때 unshift를 사용하고 push를 사용하는 방법보다 Array.from으로 배열을 생성할 때 공백을 추가하는 방법을 사용할 수 있을것 같아서 수정해봐야겠다.

0개의 댓글