React-Calendar
와 같은 Calendar 라이브러리가 있지만, date를 다루는 date-fns
자바스크립트 라이브러리를 사용해서 내가 원하는대로 만들어봤다.
date-fns
는 npm 다운로드 수가 1600만(8월 13일 기준)이 넘어가고 179개의 버전이 있는만큼 공신력있고 꾸준히 업데이트를 하고 있는 라이브러리다. 공식문서가 매우 잘 되어 있고, 있을 법한 함수를 모두 찾아 사용할 수 있다.
전체적인 구조는 Header | Days | Cells Component로 나누고, 이 3개의 Component를 감싸는 Calendar Component가 있다.
Calendar Component에서는 Calendar에 필요한 변수와 함수를 선언하고 Header | Days | Cells Component를 감싼다.
currentMonth
selectedDate
onDateClick
preMonth
, nextMonth
goToday
import { addMonths, format, subMonths } from "date-fns"
import CalendarCells from "./CalendarCells"
import CalendarDays from "./CalendarDays"
import CalendarHeader from "./CalendarHeader"
import { useState } from "react"
const MoneyBookCalendar = () => {
const [currentMonth, setCurrentMonth] = useState(new Date())
const [selectedDate, setSelectedDate] = useState("")
const onDateClick = (day: Date) => setSelectedDate(format(day, "yyyy-MM-dd"))
const preMonth = () => setCurrentMonth(subMonths(currentMonth, 1))
const nextMonth = () => setCurrentMonth(addMonths(currentMonth, 1))
const goToday = () => setCurrentMonth(new Date())
const [click, setModal] = useState(false)
const clickModal = () => setModal(!click)
return (
<section className="bg-white min-w-[62.5rem] lg:ml-[14rem] ml-[3.5rem] flex justify-center items-center pb-10" onClick={() => setModal(false)}>
<div className="flex flex-col items-center justify-center w-full max-w-[65rem] lg:px-10 px-10" onClick={(e) => e.stopPropagation()}>
<div className="w-full gap-3 mt-10 mb-5 text-4xl font-semibold">
Calendar
</div>
<CalendarHeader currentMonth={currentMonth} preMonth={preMonth} nextMonth={nextMonth} goToday={goToday} />
<CalendarDays />
<CalendarCells click={click} clickModal={clickModal} currentMonth={currentMonth} onDateClick={onDateClick} selectedDate={selectedDate} />
</div>
</section>
)
}
export default MoneyBookCalendar
Header Component에서는 현재달을 보여주고, 지난달, 오늘, 다음달로 넘어갈 수 있는 기능을 넣는다.
import { HiOutlineArrowCircleLeft, HiOutlineArrowCircleRight } from 'react-icons/hi'
import format from 'date-fns/format'
type Props = {
currentMonth: Date,
preMonth: () => void,
nextMonth: () => void,
goToday: () => void
}
const CalendarHeader = ({ currentMonth, preMonth, nextMonth, goToday }: Props) => {
return (
<div className='flex items-center justify-between w-full gap-3 p-5 text-3xl font-semibold border rounded rounded-b-none'>
<div className='gap-3'>
<span className='mr-3'>
{format(currentMonth, 'yyyy')}년
</span>
<span>
{format(currentMonth, 'M')}월
</span>
</div>
<div className='flex items-center gap-2 cursor-pointer'>
<HiOutlineArrowCircleLeft onClick={preMonth} />
<button className='text-lg' onClick={goToday}>오늘</button>
<HiOutlineArrowCircleRight onClick={nextMonth} />
</div>
</div>
)
}
export default CalendarHeader
Days Component는 요일을 표시한다.
const CalendarDays = () => {
const date = ['일', '월', '화', '수', '목', '금', '토']
return (
<div className="flex items-center justify-between w-full p-1 border border-t-0 border-collapse rounded-t-none">
{date.map((day: string) => (
<div className="w-full" key={day}>
<div className="w-[calc(100% / 7)] text-end mr-4">
{day}
</div>
</div>
))}
</div>
)
}
export default CalendarDays
Cells Component는 30개 또는 31개의 day를 그린다.
Cells Component의 핵심컨셉은
startDate
부터 시작해서 7개의 day를 가지고 있는 week 배열을 만든 뒤, 배열 변수 rows에 담아 위에서 아래로 그려나가는 것이다.
startDate
endDate
monthStart
monthEnd
startDate
를 초기값으로 가지고 endDate
까지 변하는day
변수day
를 담을 배열 변수 days
days
배열을 담을 배열 변수 rows
formattedDate
num
import { addDays, endOfMonth, endOfWeek, format, startOfMonth, startOfWeek } from "date-fns"
import CalendarCellItem from "./CalendarCellItem"
import CalendarModal from "./CalendarModal"
interface Props {
currentMonth: Date,
onDateClick: (day: Date) => void,
selectedDate: string,
clickModal: () => void,
click: boolean
}
const CalendarCells = ({ currentMonth, onDateClick, selectedDate, clickModal, click }: Props) => {
const monthStart = startOfMonth(currentMonth)
const monthEnd = endOfMonth(monthStart)
const startDate = startOfWeek(monthStart)
const endDate = endOfWeek(monthEnd)
const today = new Date()
const rows = []
let days = []
let day = startDate
let formattedDate = ''
let num = 0;
while (day <= endDate) {
for (let i = 0; i < 7; i++) {
formattedDate = format(day, 'yyyy-MM-dd')
const cloneDay = day
num++
// days 배열에 day 추가
days.push(
// 현재 day값이 현재 달과 다르면 회색으로, 같으면 흰색으로 style 설정
<div className={`w-full h-[10.5rem] relative gap-1 border rounded pb-[12%] text-end ${format(currentMonth, 'M') !== format(day, 'M') ? 'bg-gray-100 text-gray-300' : 'cursor-pointer'}`} key={num} onClick={() => {
clickModal()
onDateClick(cloneDay)
}} >
// 현재 날짜는 text를 빨간색으로 표시
<span className={`p-1 flex flex-col px-2 ${format(today, 'yyyy-MM-dd') === format(day, 'yyyy-MM-dd') && "text-red-600 font-bold"}`}>
{format(day, 'd')}
</span>
<CalendarCellItem day={day} />
{click && formattedDate === selectedDate && format(currentMonth, 'M') === format(day, 'M') && <CalendarModal select={selectedDate} date={day} num={num} />}
</div>
)
// day를 다음날로 업데이트
day = addDays(day, 1)
}
// 7일을 채운 days 배열을 rows배열에 추가
rows.push(
<div className="flex w-full gap-1" key={num}>
{days}
</div>
)
// days 배열 초기화
days = []
}
return (
<div className="flex flex-col items-center justify-between w-full gap-1 p-1 border border-t-0 border-collapse rounded rounded-t-none">
{rows}
</div>
)
}
export default CalendarCells