토이 프로젝트를 진행하던 도중 캘린더를 제작할 일이 생겼습니다. 과거에도 몇 번 캘린더를 제작했던 경험이 있었기에 생각보다 자주 만들게 되는 거 같아서 다시 한번 되짚어보는김에 정리도 해놓기 위해 글을 작성했습니다.
본 게시글에서는 date-fns
자바스크립트 라이브러리를 사용하여 캘린더를 제작하였습니다.
처음 캘린더를 제작할 당시 서칭을 하다 상당히 많은 라이브러리가 존재하다는 것을 알게 되었습니다. 어떤 라이브러리를 사용하여 캘린더를 제작할지 많은 고민을 했지만 react 라이브러리들 보다는 좀 더 원초적인 학습을 위해 순수 자바스크립트 라이브러리를 사용하기로 결정했습니다.
moment와 date-fns 중 고민하다가 moment.js 공식 홈페이지를 들어가보니 다음과 같은 이유로 라이브러리의 사용을 권장하지 않고 있었습니다.
- 우리는 새로운 기능을 추가하지 않을 것입니다.
- 우리는 Moment의 API를 변경할 수 없도록 변경하지 않을 것입니다.
- 우리는 트리 흔들림이나 번들 크기 문제를 다루지 않을 것입니다.
- 우리는 어떤 주요 변경 사항도 만들지 않을 것입니다 (버전 3 없음).
- 특히 오랫동안 알려진 문제인 경우 버그나 동작상의 문제를 수정하지 않을 수 있습니다.
글을 보고 정리해봤을 때 moment.js는 추가적인 개발이 없을 것으로 판단했습니다. 공식 홈페이지를 들어가보면 다음과 같은 다른 라이브러리들의 사용을 권장하고 있습니다.
이 중에서 그나마 써본 기억이 있는 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 (한국 표준시)
날짜 및 시간 객체 생성은 자바스크립트에서 제공하는 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()
함수를 사용하여 날짜 및 시간을 원하는 형태의 문자열로 변경할 수 있습니다.
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()
함수를 사용하여 날짜 객체에서 원하는 단위의 값을 구할 수 있습니다.
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()
함수를 사용하여 날짜 객체에서 원하는 단위의 값을 변경할 수 있습니다.
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()
함수를 사용하여 원하는 날짜 및 시간을 더할 수 있습니다.
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()
함수를 사용하여 원하는 날짜 및 시간을 뺄 수 있습니다.
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()
함수를 사용하여 날짜의 시작과 끝을 구할 수 있습니다.
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 (한국 표준시)
이외에도 다양한 함수들이 있으니 공식 홈페이지에서 확인하시기 바랍니다.
전체 레이아웃 구성을 먼저 해보면 Header
와 Body
로 나눌 수 있습니다.
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));
};
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;
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
참조