moment.js를 활용한 커스텀 캘린더 제작
왜 안 패키지? => 디자인이 구려서
React-Calendar 이거 좋은데 weekly download가 150,708라 react-day-picker의 1/3이다.
다만 프로젝트 관리가 react-day-picker보다 훨씬 잘되고 있다.
www.npmjs.com/package/react-calendar
toast-ui 이건 그냥 일정 관리할 때나 편하지. 지금 프로젝트에서는 안 맞는다. 기각. 디자인은 인정 ㅇㅇ
react-day-picker 사용하기 npm weekly download가 399,438이다. 쩐다. 그런데 최근 프로젝트 관리가 잘되고 있지는 않다.
어케 만듦?
딱히 막 어려운 건 없지만, moment.js의 공식 문서가 찾아보기 편하게 색인이 되어있는 편은 아니라 고생했다.
달력을 생성하기 위한 로직에 대해서는 주석 처리했다.
간단히 설명하자면 다음과 같다.
1. 오늘이 속하는 달의 첫 날은 1년 52주 중 몇번 째 주에 해당하는가? (윤년은 알아서 moment에서 처리해준다)
2. 오늘이 속하는 달의 마지막 날은 1년 52주 중 몇번 째 주에 해당하는가?
3. 시작 주부터 마지막 주를 +1 씩 돌아가며 날짜를 7일씩 달력에 뿌린다.
4. profit!
* moment() 값을 처음 state로 넣어줬는데 이는 계산해야 하는 판정값이므로 함수 형태로 넣어주었다. React 에서의 작은 팁이다 ㅋ
* 달을 jump 하는 경우 day를 30일 씩 빼고 더하는 식으로 처리하였다. 0, 1로 넣어서 truthy, falsy함으로 함수를 재사용할 수 있도록 만들었다.
* 현재 날짜를 state로 관리하자.
import React, { useState } from 'react';
import styled from 'styled-components';
import moment, { Moment as MomentTypes } from 'moment';
function Calendar() {
const [date, setdate] = useState<moment.Moment>(() => moment());
// func
const handleDayClick = (current: moment.Moment) => setdate(current);
const returnToday = () => setdate(moment());
const jumpToMonth = (num: number) => (num ? setdate(date.clone().add(30, 'day')) : setdate(date.clone().subtract(30, 'day')));
// chalandar generate logic
function generate() {
// 님 날짜 뭐 눌렀어요? (초기값은 오늘)
const today = date;
// startOf('month') : 이번 달의 첫번 째 날로 설정 set to the first of this month, 12:00 am
// week() : Week of Year. 이번 년도의 몇번째 주인가? => 3월 8일이면 10이겠죠?
const startWeek = today.clone().startOf('month').week();
// endOf('month').week() : 이번 달의 마지막 날로 설정 한 후 그것이 이번 년도의 몇번째 주인지 체크
// 만약 이번 해의 첫번째 주(1월 1일이 속한 주)라면 53으로 세팅, 아니라면 그대로 유지
// 이런 작업의 이유는 마지막 주가 첫 주가 될 수 없기 때문에 당연한 것임
const endWeek = today.clone().endOf('month').week() === 1 ? 53 : today.clone().endOf('month').week();
let calendar = [];
// 시작 주부터 마지막 주까지 +1 씩 증가시킴
// 이제 주마다 일을 표기해야 하므로 len이 7인 arr를 생성 후 index를 기반으로 day를 표기하자
for (let week = startWeek; week <= endWeek; week++) {
calendar.push(
<div className="row" key={week}>
{Array(7)
.fill(0)
.map((n, i) => {
// 오늘 => 주어진 주의 시작 => n + i일 만큼 더해서 각 주의 '일'을 표기한다.
let current = today
.clone()
.week(week)
.startOf('week')
.add(n + i, 'day');
// 오늘이 current와 같다면 우선 '선택'으로 두자
let isSelected = today.format('YYYYMMDD') === current.format('YYYYMMDD') ? 'selected' : '';
// 만약, 이번 달이 아닌 다른 달의 날짜라면 회색으로 표시하자
let isGrayed = current.format('MM') !== today.format('MM') ? 'grayed' : '';
return (
<div className={`box ${isSelected} ${isGrayed}`} key={i} onClick={() => handleDayClick(current)}>
<span className="text">{current.format('D')}</span>
</div>
);
})}
</div>,
);
}
return calendar;
}
return (
<S.Wrapper>
<S.CalendarHead>
<div className="head">
<span className="title">{date.format('MMMM YYYY')}</span>
<div className="util-button">
<button onClick={() => jumpToMonth(0)}>
<i className="fas fa-angle-left"></i>
</button>
<button onClick={returnToday}>Today</button>
<button onClick={() => jumpToMonth(1)}>
<i className="fas fa-angle-right"></i>
</button>
</div>
</div>
</S.CalendarHead>
<S.CalendarBody>
<div className="row">
{['SUN', 'MON', 'TUE', 'WED', 'THU', 'FRI', 'SAT'].map((el) => (
<div className="box" key={el}>
<span className="text">{el}</span>
</div>
))}
</div>
{generate()}
</S.CalendarBody>
</S.Wrapper>
);
}
export default Calendar;
결과물을 다음과 같이 생겼다.
달력이 커서 도중에 짤린거지, 정상적으로 잘 보인다 ㅇㅇ