WEB/재밌어서 만드는 것

[React/next/ts] react로 라이브러리 없이 달력 구현하는 법

자바칩 프라푸치노 2023. 2. 24. 17:03

오늘은 달력을 만들어볼 것이다. 

달력 만드는 것이 너무 어려워서 챗 gpt한테 코드 좀 작성해달라했는데..

정말 뭔가 잘 작성해주었지만..

프리미엄이 아니라서 끝까지 볼 수 없었다.

그래서 그냥 내가 했다.

 

1) 요일 만들기

빨간 줄 쳐진 요일 부분을 만들어보겠다.

이 달력 만드는 것 중에 가장 쉬운 부분은 여기다. 그래서 이거부터 하겠음.

const DayoftheWeek = () => {
  const date = ["일", "월", "화", "수", "목", "금", "토"];

  return (
    <thead className="days rows flex justify-center my-[10px]">
      <tr>
        {date.map((p) => {
          return (
            <th className="col w-[50px] text-center" key={p}>
              {p}
            </th>
          );
        })}
      </tr>
    </thead>
  );
};

export default DayoftheWeek;

 

요일 배열을 만들어주고 map돌려서 보여주면 된다. 

아주 쉽죠잉?

 

2) 현재 월에서 뒤로가기 앞으로 가기 구현하기

다음에 구현할 것은 2023년 2월 (현재 달) 에서 이전 달로 이동, 다음 달로 이동하는 것을 만들어보겠다.

 

import { addMonths, startOfMonth, startOfWeek, subMonths } from "date-fns";
import { useState } from "react";
import CalendarHeader from "./CalendarHeader";
import CalendarTable from "./CalendarTable";

const Calendar = () => {
//현재 보고 있는 달
  const [currentMonth, setCurrentMonth] = useState(new Date());
  const [selectedDate, setSelectedDate] = useState(new Date());

//이전 달로 이동(currentMonth가 이전 달로 바뀜)
  const prevMonth = () => {
    setCurrentMonth(subMonths(currentMonth, 1));
  };
  
//다음 달로 이동(currentMonth가 다음 달로 바뀜)
  const nextMonth = () => {
    setCurrentMonth(addMonths(currentMonth, 1));
  };
  const onDateClick = (day: React.SetStateAction<Date>) => {
    setSelectedDate(day);
  };
  const monthStart = startOfMonth(currentMonth);
  const startDate = startOfWeek(monthStart);

  return (
    <div className="flex flex-col justify-center">
      <CalendarHeader
        currentMonth={currentMonth}
        prevMonth={prevMonth}
        nextMonth={nextMonth}
      />
      <CalendarTable currentMonth={currentMonth} selectedDate={selectedDate} />
    </div>
  );
};

export default Calendar;

이 코드가 캘린더 전체 코드인데, 이번에 구현하고자 하는 부분이 CalendarHeader 컴포넌트 이다.

CalendarTable은 일단 무시하라.

부모 컨포넌트에서 현재 달을 구하고 prevMonth와 nextMonth로 가는 메서드를 구현했다.

왜냐하면 같은 currentMonth를 밑에오는 CanlendarTable컴포넌트와 공유해야하기 때문이다.

 

import React from "react";
import { format } from "date-fns";

import ArrowBackIosIcon from "@mui/icons-material/ArrowBackIos";
import ArrowForwardIosIcon from "@mui/icons-material/ArrowForwardIos";

interface CalendarHeaderProps {
  currentMonth: Date;
  prevMonth?: () => void;
  nextMonth?: () => void;
}
const CalendarHeader = ({
  currentMonth,
  prevMonth,
  nextMonth,
}: CalendarHeaderProps) => {
  return (
    <div className="flex justify-center m-[20px]">
      <button onClick={prevMonth}>
        <ArrowBackIosIcon sx={{ fontSize: "12px" }} />
      </button>
      <span className="m-[20px] text-[20px]">
        {format(currentMonth, "yyyy")}년{' '}
        <span>{format(currentMonth, "M")}월</span>
      </span>
      <button onClick={nextMonth}>
        <ArrowForwardIosIcon sx={{ fontSize: "12px" }} />
      </button>
    </div>
  );
};

export default CalendarHeader;

 

CalendarHeader는 이런 모습이다. 

arrow버튼을 두어서 클릭을 할 때마다 위에서 본 부모의 메서드를 호출해서 사용하고 있다. 

여기까지는 쉽다.

 

 

 

3) 달력에 날짜 표시하기

 

방법)

[1] 현재 보고 있는 달이 시작하는 날을 구한다. (2월 1일이 수요일이라는 사실을 구한다.)

[2] 현재 보고 있는 달이 끝나는 날을 구한다. (2월이 28일까지 있고 그것은 화요일이라는 사실)

[3] 맨 첫번째 칸의 날짜를 구한다(1월 29일이다)

[4] 맨 마지막 칸의 날짜를 구한다(3월 4일이다)

[5] 3번에서 구한 처음 날부터 7개까지 한 배열로 묶는다. 그걸 끝날때 까지 반복한다.

[6] 그리고 다른 배열에 그 배열들을 넣는다. (이차원 배열이 된다.)

예를 들면 아래와 같은 배열이 만들어진다.

[[29, 30, 31, 1, 2, 3, 4],
 [5, 6, 7, 8, 9, 10, 11],
 [12, 13, 14, 15, 16, 17, 18],
 [19, 20, 21, 22, 23, 24, 25],
 [26, 27, 28, 1, 2, 3, 4],
]

[7] 그것을 렌더링한다.

[8] 오늘 날짜랑 비교해서 오늘은 다른 색을 넣어준다.

[9] 현재 보고 있는 달과 비교해서 달이 다르면 투명한 글자를 넣어준다.

 

const CalendarTable = ({ currentMonth, selectedDate }: CalendarProps) => {
  return (
    <table className="calendar text-[16px]">
      <DayoftheWeek />
      <Tbody>
        <Tr currentMonth={currentMonth} selectedDate={selectedDate} />
      </Tbody>
    </table>
  );
};

아까봤던 CalendarTable 컴포넌트이다.

table 태그로 만들었다.

DayoftheWeek 컴포넌트는 위에서 했던 요일 만드는 컴포넌트이다.

 

 

Tbody부터 확인해보자.

interface TbodyProps {
  children: ReactNode;
}

const Tbody = ({ children }: TbodyProps) => {
  return <tbody className="body">{children}</tbody>;
};

별거없이 tbody 태그에 children만 넣어주고 있다.

 

 

Tr태그로 가보자.

interface CalendarProps {
  currentMonth: Date;
  selectedDate: Date;
}
interface WholeDateArray {
  date: Date;
  formattedDate: string;
}

const Tr = ({
  currentMonth,
  selectedDate,
}: PropsWithChildren<CalendarProps>) => {
  const monthStart = startOfMonth(currentMonth); //현재 보고 있는 달의 시작하는 날
  const monthEnd = endOfMonth(monthStart); //현재 보고 있는 달의 끝나는 날
  const startDate = startOfWeek(monthStart); //현재 보고 있는 달력에서 맨 앞칸
  const endDate = endOfWeek(monthEnd); //현재 보고 있는 달력에서 마지막 칸

 // 한달에 몇 주인지 체크
  const countOfWeek = Math.ceil(differenceInDays(endDate, startDate) / 7);

  let day = startDate;

//이차원 배열 
  let wholeDate: WholeDateArray[][] = [];
  for (let i = 0; i < countOfWeek; i++) {
    let arr: WholeDateArray[] = [];
    //7번 반복
    for (let j = 0; j < 7; j++) {
    //추후에 날짜를 선택해서 저장할 것을 고려하여 date정보와 format된 정보를 객체로 저장했다.
      arr.push({
        date: day,
        formattedDate: format(day, "d"),
      });

	//날짜를 다음날로 옮긴다.
      day = addDays(day, 1);
    }
    
    //위에서 만든 길이 7짜리 배열을 이차원 배열에 넣음
    wholeDate.push(arr);
  }

  return (
    <>
      {wholeDate.map((p) => {
        return (
          <tr className="row flex justify-center" key={Math.random()}>
            <Td weekDate={p} currentMonth={currentMonth}></Td>
          </tr>
        );
      })}
    </>
  );
};

 

 

Td태그로 가보자

interface TdProps {
  weekDate: WholeDateArray[];
  currentMonth: Date;
}

const Td = ({ weekDate, currentMonth }: TdProps) => {
  return (
    <>
      {weekDate.map((p) => {
        const isToday =
          format(new Date(), "yyyy-M-d") === format(p.date, "yyyy-M-d");

        const isThisMonth =
          format(currentMonth, "yyyy-M") === format(p.date, "yyyy-M");
        return (
          <td
            key={p.formattedDate}
            className={`w-[50px] text-center h-[80px] ${
              !isThisMonth && "text-transparent"
            }`}
          >
            <span className={`${isToday && "text-[#663399]"}`}>
              {p.formattedDate}
            </span>
          </td>
        );
      })}
    </>
  );
};

여기서 날짜를 실질적으로 보여주는 코드가 있다.

그리고 돌때마다 오늘인지와, 보고 있는 달이 맞는지를 확인해주는데 

이게 이렇게 해도 되는 건지는 모르겠다. 

확인 후 텍스트 색깔을 바꿔준다. 

 

 

 

4) 결과 화면

 

 

 

 

 


달력을 만드는 것에 대해 되게 엄두를 못내고 있었는데, 라이브러리가 내 맘대로 디자인을 할 수 없어서 직접 구현하기로 했다. 

처음에는 너무 이해가 안됐는데 다 만들고 난 지금은 왜 이해가 안됐지? 하는 생각이 든다. 

그런데 props로 넘기는 것도 너무 많고 코드가 복잡해서 전역 상태를 사용해볼까 싶다. 

다음 번에는 더 깔끔한 코드로 리팩토링을 해서 와보겠다. 

728x90