5 분 소요

date-fns 사용해서 달력 구현하기

구현 설명

이 화면에서 달력 부분을 구현해보자

1

설명

1. Header: 월을 바꾸는 부분이다.
2. Days: 요일을 나타내는 부분이다.
3. Cells: 달력 셀을 나타내는 부분이다. 당연하게도 이 부분이 가장 중요하다.

구현 기능

1. Header에서 월을 바꿀 수 있다.
2. 셀을 클릭하면 그 셀이 선택된다.
3. 오른쪽 일별 박스에서 화살표를 눌렀을 때도 선택되는 셀이 변경되게 한다.

라이브러리 소개

date-fns 깃허브 링크

깃허브에 나와있듯이 200개 이상의 기능이 있지만 여기서는 달력에 한정지어서 설명하려고 한다.


1. format: 날짜 객체를 여러 가지 방법으로 문자열로 서식화하는 함수

const selectedDate = new Date(); //괄호에 아무것도 없으면 현재 날짜로 객체를 생성하는 것
console.log(format(selectedDate, "yyyy-MM-dd"));
// 출력결과 2023-09-08

format 형식

1) 년도와 월

  • “yyyy-MM-dd”: “2023-10-13”
  • “yy-MM-dd”: “23-10-13”
  • “MMMM yyyy”: “October 2023”

2) 월별 형식

  • “MMM yyyy”: “Oct 2023”
  • “MM/yyyy”: “10/2023”

3) 요일과 날짜

  • “EEEE, dd MMMM yyyy”: “Thursday, 13 October 2023”
  • “EEE, dd MMM yyyy”: “Thu, 13 Oct 2023”

4) 일자 및 시간 분리

  • “dd/MM/yyyy”: “13/10/2023”
  • “HH:mm”: “15:30”

5) 시간과 분

  • “HH:mm”: “15:30”
  • “h:mm a”: “3:30 PM”

6) AM/PM 표시

  • “h:mm a”: “3:30 PM”
  • “hh:mm a”: “03:30 PM”

7) 24시간 형식

  • “HH:mm”: “15:30”
  • “HH:mm:ss”: “15:30:00”

8) 시간대 표시

  • “HH:mm ZZZZ”: “15:30 UTC”

2. addMonths, subMonths: 월을 원하는 만큼 빼고 더하는 함수

const [currentMonth, setCurrentMonth] = useState(new Date());

const prevMonth = () => {
  setCurrentMonth(subMonths(currentMonth, 1));
};
const nextMonth = () => {
  setCurrentMonth(addMonths(currentMonth, 1));
};

3. addDays, subDays: 일을 원하는 만큼 뺴고 더하는 함수

const [selectedDate, setSelectedDate] = useState(new Date());

const prevDay = () => {
  setSelectedDate(subDays(selectedDate, 1));
};
const nextDay = () => {
  setSelectedDate(addDays(selectedDate, 1));
};

4. startOfMonth, endOfMonth, startOfWeek, endOfWeek: 달력 셀에 관련된 함수

startOfMonth: 현재 달의 첫번째 날짜
endOfMonth: 현재 달의 마지막 날짜
startOfWeek: 현재 날짜 주의 첫번째 날짜
endOfWeek: 현재 날짜 주의 마지막 날짜

2

const monthStart = startOfMonth(currentMonth); //8월 1일
const monthEnd = endOfMonth(currentMonth); //8월 31일
const startDate = startOfWeek(monthStart); //7월 30일
const endDate = endOfWeek(monthEnd); //9월 2일

최종 구현 코드

Calendar.tsx: 메인 컴포넌트로 월과 일을 변경하기 위해 date-fns 라이브러리 사용함

import React, { useState, useEffect } from "react";
import { format, addMonths, subMonths, addDays, subDays } from "date-fns";
import {
  RenderHeader,
  RenderDays,
  RenderCells,
} from "components/calendar/CalendarRender";
import {
  RenderDetail,
  RenderUnscheduled,
} from "components/calendar/DetailRender";
import {
  CalendarPage,
  CalendarContainer,
  DayContainer,
} from "components/calendar/CalendarStyledComponents";

const Calendar = () => {
  const [currentMonth, setCurrentMonth] = useState(new Date());
  const [selectedDate, setSelectedDate] = useState(new Date());
  const calendarData = [api 반환값];
  const detailData = [api 반환값];

  //달력 값 변경
  const prevMonth = () => {
    setCurrentMonth(subMonths(currentMonth, 1));
  };
  const nextMonth = () => {
    setCurrentMonth(addMonths(currentMonth, 1));
  };
  const onDateClick = (day: any) => {
    setSelectedDate(day);
  };
  const prevDay = () => {
    setSelectedDate(subDays(selectedDate, 1));
  };
  const nextDay = () => {
    setSelectedDate(addDays(selectedDate, 1));
  };

  return (
    <CalendarPage>
      {/* 월 변경 부분 */}
      <RenderHeader
        currentMonth={currentMonth}
        prevMonth={prevMonth}
        nextMonth={nextMonth}
      />
      <div className="calendar-detail">
        <CalendarContainer>
          {/* 요일 부분 */}
          <RenderDays />
          {/* 달력 셀 부분 */}
          <RenderCells
            currentMonth={currentMonth}
            selectedDate={selectedDate}
            onDateClick={onDateClick}
            calendarData={calendarData}
          />
        </CalendarContainer>

        <DayContainer>
          {/* 오른쪽 일별 박스 부분 */}
          <RenderDetail
            selectedDate={selectedDate}
            prevDay={prevDay}
            nextDay={nextDay}
            detailData={detailData}
          />
        </DayContainer>
      </div>
    </CalendarPage>
  );
};

export default Calendar;

CalendarRender.tsx

1. import 부분: 달력 셀을 위해 date-fns 라이브러리를 사용함

import React from "react";
import {
  format,
  startOfMonth,
  endOfMonth,
  startOfWeek,
  endOfWeek,
  addDays,
} from "date-fns";
import headerLeft from "assets/main/main_left_arrow.svg";
import headerRight from "assets/main/main_right_arrow.svg";
import dayLeft from "assets/calendar/calendar_day_left_arrow.svg";
import dayRight from "assets/calendar/calendar_day_right_arrow.svg";
import {
  RenderHeaderContainer,
  RenderDaysContainer,
  Cell,
  Event,
  Row,
  RenderCellsContainer,
} from "./CalendarStyledComponents";
import { ICalendarData } from "types/interfaces/CalendarProcess";

2. Header: 월 변경 부분 3

Calendar.tsx의 prevMonth, nextMonth 함수와 통신하며
월을 변경할 수 있다

// RenderHeader(월)
interface RenderHeaderProps {
  currentMonth: Date;
  prevMonth: () => void;
  nextMonth: () => void;
}

export const RenderHeader: React.FC<RenderHeaderProps> = ({
  currentMonth,
  prevMonth,
  nextMonth,
}) => {
  return (
    <RenderHeaderContainer>
      <img src={headerLeft} alt="headerLeft" onClick={prevMonth} />
      <div className="month">{format(currentMonth, "MMMM")}</div>
      <img src={headerRight} alt="headerRight" onClick={nextMonth} />
    </RenderHeaderContainer>
  );
};

2. Days: 요일 부분 4

// RenderDays(요일)
export const RenderDays = () => {
  const date = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];

  return (
    <RenderDaysContainer>
      {date.map((day, i) => (
        <div key={i}>{day}</div>
      ))}
    </RenderDaysContainer>
  );
};

3. Cells: 달력 셀 부분 5

Calendar.tsx의 selectedDate 함수와 통신하며 셀 클릭했을 때 변경할 수 있게 한다

1. date-fns의 내장 함수들로 달력의 한 페이지의 시작과 마지막 날짜를 알아낸다
2. day를 startDate부터 endDate까지 while문으로 돌리면서 안에 for문도 같이 돈다
3. for문에서 days 배열에 하루씩 값을 넣어주다가 일주일을 채우면 rows 배열에 days 배열을 추가하고 days 배열을 초기화시킨다
4. day를 하루씩 늘려주는 방법은 addDays(day, 1)를 사용한다

// RenderCells(달력 셀)
interface RenderCellsProps {
  currentMonth: Date;
  selectedDate: Date;
  onDateClick: (date: Date) => void;
  calendarData: [];
}

export const RenderCells: React.FC<RenderCellsProps> = ({
  currentMonth,
  selectedDate,
  onDateClick,
  calendarData,
}) => {
  const monthStart = startOfMonth(currentMonth); //8월 1일
  const monthEnd = endOfMonth(monthStart); //8월 31일
  const startDate = startOfWeek(monthStart); //7월 30일
  const endDate = endOfWeek(monthEnd); //9월 2일

  const rows = [];
  let days = [];
  let day = startDate;
  let formattedDate = "";

  while (day <= endDate) {
    for (let i = 0; i < 7; i++) {
      formattedDate = format(day, "d");
      const cloneDay = new Date(day);

      days.push(
        <Cell
          $day={day}
          $monthStart={monthStart}
          $selectedDate={selectedDate}
          $currentMonth={currentMonth}
          key={day.toDateString()}
          onClick={() => onDateClick(cloneDay)}
        >
          <div className="date">{formattedDate.padStart(2, "0")}</div>
        </Cell>
      );
      day = addDays(day, 1);
    }
    rows.push(<Row key={day.toDateString()}>{days}</Row>);
    days = [];
  }
  return <RenderCellsContainer>{rows}</RenderCellsContainer>;
};

달력 모양으로 만들려면 아래와 같이 스타일을 주면 된다

Row는 일주일이므로 days(하루)들을 가로로 flex를 준다
RenderCellsContainer는 달력 모양이므로 rows(일주일)들을 세로로 flex를 준다

export const Row = styled.div`
  display: flex;
`;
export const RenderCellsContainer = styled.div`
  display: flex;
  flex-direction: column;
`;

Detail: 오른쪽 일별 박스 부분

Calendar.tsx의 prevDay, nextDay 함수와 통신하며 셀 클릭했을 때 변경할 수 있게 한다

// 일별로 띄우기
interface RenderDetailProps {
  selectedDate: Date;
  prevDay: () => void;
  nextDay: () => void;
  detailData: [];
}

export const RenderDetail: React.FC<RenderDetailProps> = ({
  selectedDate,
  prevDay,
  nextDay,
  detailData,
}) => {
  return (
    <DetailContainer>
      <div className="selectDate">
        <img src={dayLeft} alt="dayLeft" onClick={prevDay} />
        <div className="selectedDate">
          {format(selectedDate, "d, eee").toLowerCase()}
        </div>
        <img src={dayRight} alt="dayRight" onClick={nextDay} />
      </div>
    </DetailContainer>
  );
};

추가 구현 코드

1. 달력 셀을 보면 선택된 날짜는 파랗게 표시가 되어있다.
2. 10월에 해당하는 날짜는 진한 회색이지만 9월이나 11월에 속하는 날짜는 옅은 회색으로 되어있다.

이 두 부분을 추가적으로 구현해보자

1


CalendarRender.tsx > RenderCells > Cell

- 앞에 $가 붙는 속성은 js에는 사용되지 않지만 CSS-in-JS 라이브러리 (예: styled-components, emotion)나 CSS 모듈에 props로 넘겨줄 때 쓰이는 속성이다
- $가 특정한 기능을 갖는 것은 아니고 관례적으로 js와 css에 쓰이는 속성을 분류하기 위해 약속한 기호이다

day: 현재 cell의 날짜
selectedDate: 선택된 날짜
currentMonth: 현재 보여지고 있는 month

<Cell
  $day={day}
  $selectedDate={selectedDate}
  $currentMonth={currentMonth}
  key={day.toDateString()}
  onClick={() => onDateClick(cloneDay)}
>
  <div className="date">{formattedDate.padStart(2, "0")}</div>
</Cell>

CalendarStyledComponents.ts > Cell

1. 선택된 날짜는 파랗게 표시하기
isSameDay(props.$day, props.$selectedDate)
현재 셀 날짜가 선택된 날짜라면 파랗게 표시

2. 해당 월에 속하는 날짜는 진한 회색, 그 외 날짜는 옅은 회색으로 나타내기
!isSameMonth(props.$day, props.$currentMonth)
현재 셀 날짜의 월이 현재 보여지는 월과 다르다면 옅은 회색으로 표시

import styled from "styled-components";
import { format, isSameMonth, isSameDay } from "date-fns";

interface CellProps {
  $day: Date;
  $selectedDate: Date;
  $currentMonth: Date;
}

export const Cell =
  styled.div <
  CellProps >
  `
  width: 100%;
  height: 111px;
  border: 0.5px solid #ccc;
  font-size: 12px;
  font-weight: 500;
  color: #969696;
  cursor: pointer;
  ${(props) =>
    isSameDay(props.$day, props.$selectedDate) &&
    `
    border: 0.5px solid rgb(var(--main));
    color: rgb(var(--main));
    box-shadow: 0 0 6px 3px rgba(50, 83, 255, 0.225);
    .date{
      font-size: 14px;
      font-weight: 600;
    }
  `}
  ${(props) =>
    !isSameMonth(props.$day, props.$currentMonth) &&
    `
    color: rgb(var(--border));
  `}
`;

카테고리:

업데이트:

댓글남기기