[React] 캘린더 제작하기

sikkzz·2023년 8월 15일
1

React

목록 보기
3/12
post-thumbnail

🖐️ 시작하며

토이 프로젝트를 진행하던 도중 캘린더를 제작할 일이 생겼습니다. 과거에도 몇 번 캘린더를 제작했던 경험이 있었기에 생각보다 자주 만들게 되는 거 같아서 다시 한번 되짚어보는김에 정리도 해놓기 위해 글을 작성했습니다.

본 게시글에서는 date-fns 자바스크립트 라이브러리를 사용하여 캘린더를 제작하였습니다.

date-fns

왜 date-fns인가?

처음 캘린더를 제작할 당시 서칭을 하다 상당히 많은 라이브러리가 존재하다는 것을 알게 되었습니다. 어떤 라이브러리를 사용하여 캘린더를 제작할지 많은 고민을 했지만 react 라이브러리들 보다는 좀 더 원초적인 학습을 위해 순수 자바스크립트 라이브러리를 사용하기로 결정했습니다.

momentdate-fns 중 고민하다가 moment.js 공식 홈페이지를 들어가보니 다음과 같은 이유로 라이브러리의 사용을 권장하지 않고 있었습니다.

  • 우리는 새로운 기능을 추가하지 않을 것입니다.
  • 우리는 Moment의 API를 변경할 수 없도록 변경하지 않을 것입니다.
  • 우리는 트리 흔들림이나 번들 크기 문제를 다루지 않을 것입니다.
  • 우리는 어떤 주요 변경 사항도 만들지 않을 것입니다 (버전 3 없음).
  • 특히 오랫동안 알려진 문제인 경우 버그나 동작상의 문제를 수정하지 않을 수 있습니다.

글을 보고 정리해봤을 때 moment.js는 추가적인 개발이 없을 것으로 판단했습니다. 공식 홈페이지를 들어가보면 다음과 같은 다른 라이브러리들의 사용을 권장하고 있습니다.

이 중에서 그나마 써본 기억이 있는 date-fns를 사용하기로 결정했습니다.

date-fns란?

date-fns는 JavaScript 날짜 라이브러리 중 Tree Shaking을 지원하고 Functional pattern으로 동작하는 라이브러리입니다.

Tree Shaking은 코드를 빌드할 때 실제로 쓰지 않는 코드들은 제외한다는 뜻으로 나무를 흔들어서 죽은 나뭇잎들을 떨어뜨리는거와 비슷해서 이름이 Tree Shaking으로 붙었다고 합니다.

자세한 함수와 내용들은 공식 홈페이지에 들어가면 보실 수 있지만 대표적으로 많이 사용되는 함수와 해당 프로젝트에서 사용한 함수들만 간단히 설명하겠습니다.

npm install date-fns

npm을 통해 date-fns를 설치하여 사용합니다.

date-fns도 moment.js나 day.js 처럼 다음과 같이 모듈 객체를 불러와서 사용이 가능합니다. 다만 위에서 언급했듯 Tree Shaking을 지원하고 Functional하게 사용이 가능하므로 그 장점을 활용하여 사용에 필요한 함수를 import해서 사용하는 것이 불필요한 함수에 용량을 사용하지 않을 수 있습니다.

const dateFns = require("date-fns");

let date1 = new Date("2023-08-15");
console.log(date1); // Tue Aug 15 2023 00:00:00 GMT+0900 (한국 표준시)

let date2 = add(date1, { days: 1 });
console.log(date2); // Wed Aug 16 2023 00:00:00 GMT+0900 (한국 표준시)

날짜 및 시간 객체 생성 - new Date()

날짜 및 시간 객체 생성은 자바스크립트에서 제공하는 Date()를 사용하여 생성합니다.

const date1 = new Date(); // Tue Aug 15 2023 00:00:00 GMT+0900 (한국 표준시)
const date2 = new Date("2023-12-31"); // Sun Dec 31 2023 00:00:00 GMT+0900 (한국 표준시)

포맷 지정 - format()

format() 함수를 사용하여 날짜 및 시간을 원하는 형태의 문자열로 변경할 수 있습니다.

import { format } from "date-fns"

let date = new Date();
console.log(date); // Tue Aug 15 2023 00:00:00 GMT+0900 (한국 표준시)
console.log(format(date, "yyyy-MM-d")) // 2023-08-15
console.log(format(date, "yyyy/MM/d")) // 2023/08/15
console.log(format(date, "Y")) // 2023
console.log(format(date, "M")) // 8
console.log(format(date, "d")) // 15

날짜 객체의 원하는 단위 구하기 - get()

get() 함수를 사용하여 날짜 객체에서 원하는 단위의 값을 구할 수 있습니다.

import { getYear, getMonth, getDate, getDay } from "date-fns"

let date = new Date();
console.log(date); // Tue Aug 15 2023 00:00:00 GMT+0900 (한국 표준시)
console.log(getYear(date)); // 2023 (년)
console.log(getMonth(date)); // 7 (월) (0~11) 단위이므로 1을 더해주어야 정상 값 출력
console.log(getDate(date)); // 15 (일)
console.log(getDay(date)); // 2 (요일) 일요일 - 0 토요일 - 6 화요일이므로 2 출력

날짜 객체의 원하는 단위 변경하기 - set()

set() 함수를 사용하여 날짜 객체에서 원하는 단위의 값을 변경할 수 있습니다.

import { setYear, setMonth, setDate, setDay } from "date-fns"

let date = new Date();
console.log(date); // Tue Aug 15 2023 00:00:00 GMT+0900 (한국 표준시)
console.log(setYear(date, 2022)); // Mon Aug 15 2022 00:00:00 GMT+0900 (한국 표준시)
console.log(setMonth(date, 7)); // Sat Jul 15 2023 00:00:00 GMT+0900 (한국 표준시)
console.log(setDate(date, 25)); // Fri Aug 25 2023 00:00:00 GMT+0900 (한국 표준시)
console.log(setDay(date, 6)); // Sat Aug 19 2023 00:00:00 GMT+0900 (한국 표준시)

날짜 객체 더하기 - add()

add() 함수를 사용하여 원하는 날짜 및 시간을 더할 수 있습니다.

import { addYears, addMonths } from "date-fns"

let date = new Date();
console.log(date); // Tue Aug 15 2023 00:00:00 GMT+0900 (한국 표준시)
console.log(addYears(date, 1)); // Thu Aug 15 2024 00:00:00 GMT+0900 (한국 표준시)
console.log(addMonths(date, 1)); // Fri Sep 15 2023 00:00:00 GMT+0900 (한국 표준시)

날짜 객체 빼기 - sub()

sub() 함수를 사용하여 원하는 날짜 및 시간을 뺄 수 있습니다.

import { subYears, subMonths } from "date-fns"

let date = new Date();
console.log(date); // Tue Aug 15 2023 00:00:00 GMT+0900 (한국 표준시)
console.log(subYears(date, 1)); // Mon Aug 15 2022 00:00:00 GMT+0900 (한국 표준시)
console.log(subMonths(date, 1)); // Sat Jul 15 2023 00:00:00 GMT+0900 (한국 표준시)

날짜의 시작, 끝 구하기 - startOf(), endOf()

startOf(), endOf() 함수를 사용하여 날짜의 시작과 끝을 구할 수 있습니다.

import { startOfMonth, startOfWeek, endOfMonth, endOfWeek } from "date-fns"

let date = new Date();
console.log(date); // Tue Aug 15 2023 00:00:00 GMT+0900 (한국 표준시)
console.log(startOfMonth(date)); // Tue Aug 01 2023 00:00:00 GMT+0900 (한국 표준시)
console.log(startOfWeek(date)); // Sun Aug 13 2023 00:00:00 GMT+0900 (한국 표준시)
console.log(endOfMonth(date)); // Thu Aug 31 2023 23:59:59 GMT+0900 (한국 표준시)
console.log(endOfWeek(date)); // Sat Aug 19 2023 23:59:59 GMT+0900 (한국 표준시)

이외에도 다양한 함수들이 있으니 공식 홈페이지에서 확인하시기 바랍니다.

캘린더 제작

전체 레이아웃 구성을 먼저 해보면 HeaderBody로 나눌 수 있습니다.
Header는 오늘 날짜와 달력을 이동할 수 있는 아이콘이 들어갑니다.
Body는 요일을 나타내는 Week와 각 날짜를 나타내는 Day로 나누었습니다.

스타일링에는 styled-components를 사용했습니다.

날짜를 구하기 위한 변수 선언

캘린더는 다음과 같은 정보들로 한 달을 그릴 수 있습니다.

  • 현재 달의 시작 날짜
  • 현재 달의 마지막 날짜
  • 현재 달의 첫 주 시작 날짜
  • 현재 달의 마지막 주 마지막 날짜

해당 데이터들을 먼저 선언해주었습니다.

const [currentDate, setCurrentDate] = useState(new Date()); // 현재 달 (2023-08-15)
const monthStart = startOfMonth(currentDate); // 현재 달의 시작 날짜 (2023-08-01)
const monthEnd = endOfMonth(monthStart); // 현재 달의 마지막 날짜 (2023-08-31)
const startDate = startOfWeek(monthStart); // 현재 달의 첫 주 시작 날짜 (2023-07-30)
const endDate = endOfWeek(monthEnd); // 현재 달의 마지막 주 마지막 날짜 (2023-09-02)

요일 출력

요일을 그리기 위해서 요일 데이터를 선언하고 map 함수를 사용해 요일을 그려줄 수 있습니다.

const week = ["일", "월", "화", "수", "목", "금", "토"]; // 요일 데이터

// week 배열을 순회하면서 요일을 하나씩 출력
const weeks = week.map((item, index) => {
  return <Week key={index}>{item}</Week>;
})

날짜 출력

날짜를 그리기 위해 앞에서 선언해준 변수들을 이용해 날짜를 그릴 수 있습니다.

const day = []; // 한 달의 전체 데이터
let startDay = startDate; // 현재 달의 첫 주 시작 날짜
let days = []; // 한 주의 전체 데이터
let formattedDate = ""; // 배열 삽입용 하루 날짜의 데이터

// while문은 현재 달의 첫 주 시작 날짜부터 하루씩 더해가다가 현재 달의 마지막 주 마지막 날짜보다
// 커지면 한 달의 날짜가 끝난 것이므로 종료됩니다.
while (startDay <= endDate) {
  for (let i = 0; i < 7; i++) { // 한 주는 7일이므로 7번의 반복문 실행
    formattedDate = format(startDay, "d"); // 날짜의 데이터는 숫자로 format됩니다.
    days.push( // 한 주의 배열에 하루씩 날짜 삽입합니다.
      <Day>
        <DaySpan
          style={{
            color:
              // 현재 날짜가 이번 달의 데이터가 아닐 경우 회색으로 표시
              // date-fns의 함수를 사용해 일요일이면 빨간색, 토요일이면 파란색으로 표시
              // 나머지 날짜는 이번 달의 정상적인 데이터이므로 검은색으로 표시
              format(currentDate, "M") !== format(startDay, "M")
                ? "#ddd"
                : isSunday(startDay)
                ? "red"
                : isSaturday(startDay)
                ? "blue"
                : "#000",
          }}
        >
          {formattedDate}
        </DaySpan>
      </Day>
    );
    startDay = addDays(startDay, 1); // 하루를 삽입하고 날짜를 하루 더해줍니다.
  }
  // for문이 종료되면 7일의 날짜가 한 주의 데이터에 모두 삽입된 것
  // 한 주의 데이터를 한 달의 전체 데이터에 삽입해줍니다.
  day.push(<DayBox key={startDay}>{days}</DayBox>);
  
  // 다음 주의 데이터를 삽입하기 위해 한 주의 데이터를 초기화 시켜줍니다.
  days = [];
}

전체 로직은 다음과 같습니다.

  • 현재 달의 첫 주 시작 날짜부터 현재 달의 마지막 주 마지막 날짜까지 반복문 실행
  • 날짜를 하루하루 더해가면서 한 주의 데이터 삽입
  • 1주는 7일이므로 7일의 데이터가 삽입되면 한 달의 전체 데이터에 1주 데이터 삽입
  • 1주의 데이터를 초기화시키고 다음 주의 데이터를 동일하게 다시 삽입
  • 한 달의 마지막 날짜까지 데이터가 삽입되면 한 달이 끝난 것이므로 반복문 종료

달을 변경할 수 있는 함수 제작

화살표를 눌렀을 때 달이 바뀌어야 하기에 달을 변경하는 함수들도 만들어 주었습니다.

// date-fns 함수인 subMonths를 사용하여 클릭 시 현재 달에서 1달을 빼줌
const prevMonth = () => {
  setCurrentDate(subMonths(currentDate, 1));
};

// date-fns 함수인 addMonths를 사용하여 클릭 시 현재 달에서 1달을 더해줌
const nextMonth = () => {
  setCurrentDate(addMonths(currentDate, 1));
};

📝 전체 코드

Calendar.jsx

import { useState } from "react";
import {
  format,
  addMonths,
  subMonths,
  startOfMonth,
  endOfMonth,
  startOfWeek,
  endOfWeek,
  addDays,
  isSaturday,
  isSunday,
} from "date-fns";

import {
  Layout,
  Header,
  Title,
  Button,
  CalendarBox,
  WeekLayout,
  Week,
  DayLayout,
  DayBox,
  Day,
  DaySpan,
} from "./CalendarElements";

import { AiOutlineLeft, AiOutlineRight } from "react-icons/ai";

const Calendar = () => {
  const [currentDate, setCurrentDate] = useState(new Date()); // 현재 달 (2023-08-15)
  const monthStart = startOfMonth(currentDate); // 현재 달의 시작 날짜 (2023-08-01)
  const monthEnd = endOfMonth(monthStart); // 현재 달의 마지막 날짜 (2023-08-31)
  const startDate = startOfWeek(monthStart); // 현재 달의 첫 주 시작 날짜 (2023-07-30)
  const endDate = endOfWeek(monthEnd); // 현재 달의 마지막 주 마지막 날짜 (2023-09-02)
  const week = ["일", "월", "화", "수", "목", "금", "토"]; // 요일 데이터

  // week 배열을 순회하면서 요일을 하나씩 출력
  const weeks = week.map((item, index) => {
    return <Week key={index}>{item}</Week>;
  });

  const day = []; // 한 달의 전체 데이터
  let startDay = startDate; // 현재 달의 첫 주 시작 날짜
  let days = []; // 한 주의 전체 데이터
  let formattedDate = ""; // 배열 삽입용 하루 날짜의 데이터

  // while문은 현재 달의 첫 주 시작 날짜부터 하루씩 더해가다가 현재 달의 마지막 주 마지막 날짜보다
  // 커지면 한 달의 날짜가 끝난 것이므로 종료됩니다.
  while (startDay <= endDate) {
    for (let i = 0; i < 7; i++) {
      // 한 주는 7일이므로 7번의 반복문 실행
      formattedDate = format(startDay, "d"); // 날짜의 데이터는 숫자로 format됩니다.
      days.push(
        // 한 주의 배열에 하루씩 날짜 삽입합니다.
        <Day>
          <DaySpan
            style={{
              color:
                // 현재 날짜가 이번 달의 데이터가 아닐 경우 회색으로 표시
                // date-fns의 함수를 사용해 일요일이면 빨간색, 토요일이면 파란색으로 표시
                // 나머지 날짜는 이번 달의 정상적인 데이터이므로 검은색으로 표시
                format(currentDate, "M") !== format(startDay, "M")
                  ? "#ddd"
                  : isSunday(startDay)
                  ? "red"
                  : isSaturday(startDay)
                  ? "blue"
                  : "#000",
            }}
          >
            {formattedDate}
          </DaySpan>
        </Day>
      );
      startDay = addDays(startDay, 1); // 하루를 삽입하고 날짜를 하루 더해줍니다.
    }
    // for문이 종료되면 7일의 날짜가 한 주의 데이터에 모두 삽입된 것
    // 한 주의 데이터를 한 달의 전체 데이터에 삽입해줍니다.
    day.push(<DayBox key={startDay}>{days}</DayBox>);

    // 다음 주의 데이터를 삽입하기 위해 한 주의 데이터를 초기화 시켜줍니다.
    days = [];
  }

  // date-fns 함수인 subMonths를 사용하여 클릭 시 현재 달에서 1달을 빼줌
  const prevMonth = () => {
    setCurrentDate(subMonths(currentDate, 1));
  };

  // date-fns 함수인 addMonths를 사용하여 클릭 시 현재 달에서 1달을 더해줌
  const nextMonth = () => {
    setCurrentDate(addMonths(currentDate, 1));
  };

  return (
    <Layout>
      <Header>
        <Button onClick={prevMonth}>
          <AiOutlineLeft size={24} color="#000" />
        </Button>
        <Title>
          {format(currentDate, "yyyy")}{format(currentDate, "M")}</Title>
        <Button onClick={nextMonth}>
          <AiOutlineRight size={24} color="#000" />
        </Button>
      </Header>
      <CalendarBox>
        <CalendarBox>
          <WeekLayout>{weeks}</WeekLayout>
          <DayLayout>{day}</DayLayout>
        </CalendarBox>
      </CalendarBox>
    </Layout>
  );
};

export default Calendar;

CalendarElements.jsx

import styled from "styled-components";

const Layout = styled.div`
  max-width: 1300px;
  margin: 100px auto;
`;

const Header = styled.div`
  display: flex;
  justify-content: center;
`;

const Title = styled.div`
  font-size: 36px;
  color: #000;
  font-weight: 700;
  display: flex;
  align-items: center;
`;

const Button = styled.div`
  margin: 0 24px;
  display: flex;
  justify-content: center;
  align-items: center;
`;

const CalendarBox = styled.div`
  margin-top: 40px;
`;

const WeekLayout = styled.div`
  display: flex;
`;

const Week = styled.div`
  width: 14.28%;
  color: #8f8f8f;
  display: flex;
  justify-content: center;
  border: 1px solid #ddd;
  border-radius: 10px;
  padding: 10px;
`;

const DayLayout = styled.div`
  width: 100%;
  margin-top: 10px;
`;

const DayBox = styled.div`
  display: flex;
  flex-direction: row;
  align-items: center;
`;

const Day = styled.div`
  width: 14.28%;
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: center;
  border: 1px solid #ddd;
`;

const DaySpan = styled.span`
  padding: 10px;
  border-radius: 50%;
  position: relative;
  font-weight: 700;
`;

export {
  Layout,
  Header,
  Title,
  Button,
  CalendarBox,
  WeekLayout,
  Week,
  DayLayout,
  DayBox,
  Day,
  DaySpan,
};

🔚 마치며

date-fns 라이브러리를 사용하여 캘린더 제작을 해봤습니다. 기본 캘린더만 구성한 것이므로 특수한 기능들은 추후에 다시 작업해볼 예정입니다.

추가로 다뤄볼 내용들은 다음과 같습니다.

  • 일요일 시작 캘린더가 아닌 월요일 시작 캘린더 제작
  • 이번 달의 데이터만 출력한 캘린더 제작
  • 타 라이브러리를 사용한 캘린더 제작

전체 코드는 깃허브에서도 확인이 가능합니다. 감사합니다 :D

참조

profile
FE Developer

0개의 댓글