유틸성 기능을 가진 캘린더를 제작해야하는 일이 생겼다. 웹에서 제공해주는 React 서드파티 라이브러리들중에 내가 원하는 기능을 가진 캘린더가 딱히 없어서 제작하면서 궁금했던 것들을 정리해봅니다.
원했던 기능
- 일, 월, 화, 수, 목, 금, 토 를 클릭하면 보여지고있는 달력의 클릭한 요일을 모든 일자를 선택된 상태로 만드는 것
- 공휴일을 선택할수 없게 만드는 것
- 범위로 선택하는 것이아닌 각각의 여러개의 날짜를 선택할 수 있게 만드는 것
캘린더 라이브러리
- airbnb/react-dates
라이브되고있는 서비스에서 지속적으로 사용하고있는 라이브러리라 업데이트에 걱정이없다.
원활한 유저와 소통이 되고있고 의외로 커스텀할때 자료들이 많다. - material-ul/Date-pickers
- Hacker0x01/react-datepicker
예제가 엄청많아서 보기 좋았다 여러가지의 케이스별로 보고서 커스텀할 수 있다는 장점이있다.
🤾♂️ Let’s go airbnb/react-dates
라이브러리의 지속성, multicheck 관련된 예제를 찾았기 때문에 바로 실행할 수 있었다는 생각이 들어서 바로 airbnb/react-dates
를 선택했다.
👀 Preview
MultiCheck 구현하기
기본 캘린더에서 MultiCheck가 되는 캘린더로 구성해야하는 것이 첫번째 목표였다.
https://github.com/airbnb/react-dates/issues/190#issuecomment-392323696
내가 원하는 니즈를 한꺼번애 담고있는 예제였다.
Components
DayPickerSingleDateController
를 가지고 각각의 renderCalendarDay
를 렌더링할때 MultipleCalendar에서 상태값을 확인해서 체크 상태를 추가해서 적용하는 방식이다.
Class Type
import React from 'react'
import PropTypes from 'prop-types'
import { DayPickerSingleDateController, CalendarDay } from 'react-dates'
export class MultiDatePicker extends React.Component {
static defaultProps = {
dates: []
}
constructor (props) {
this.state = {
dates: props.dates
}
this.handleChange = this.handleChange.bind(this)
}
handleChange (date) {
const { dates } = this.state
const newDates = dates.includes(date) ? dates.filter(d => !date.isSame(d)) : [...dates, d]
this.setState({ dates: newDates })
this.props.onChange && this.props.onChange(newDates.toJS())
}
render () {
return (
<DayPickerSingleDateController
numberOfMonths={1}
onDateChange={this.handleChange}
renderCalendarDay={props => {
const { day, modifiers } = props
if (this.state.dates.includes(day)) {
modifiers && modifiers.add('selected')
}
else {
modifiers && modifiers.delete('selected')
}
return (
<CalendarDay { ...props } modifiers={modifiers} />
)
}} />
)
}
}
export default MultiDatePicker
Code language: JavaScript (javascript)
Hook Type
import React, { useState, useEffect } from 'react';
import { DayPickerSingleDateController, CalendarDay } from 'react-dates';
const MultiDatePickerCalendar = () => {
const [dates, setDates] = useState([]);
const handleChange = (date) => {
const newDates = dates.includes(date)
? dates.filter((d) => !date.isSame(d))
: [...dates, date];
setDates(newDates);
};
return (
<DayPickerSingleDateController
numberOfMonths={1}
onDateChange={handleChange}
hideKeyboardShortcutsPanel
noBorder
monthFormat="YYYY년 MMMM"
renderCalendarDay={(props) => {
const { day, modifiers } = props;
if (dates.includes(day)) {
modifiers && modifiers.add('selected');
} else {
modifiers && modifiers.delete('selected');
}
return <CalendarDay {...props} modifiers={modifiers} />;
}}
/>
);
};
export default MultiDatePickerCalendar;
Code language: JavaScript (javascript)
이제 각각의 날짜를 선택할수 있는 캘린더가 만들어졌다.
이제 이 date 컴포넌트를 기준으로 시작!
월단위 해당 요일 모두 선택하기
맨처음에 일,월,화.. 와 같이 주간 요일나타나는 곳에 선택할 수 있는 버튼컨트롤러를 만들어서 추가하면 어떨까? 고민을했다. 하지만 각 날짜를 체크하고 그 체크한 것을 통해서 선택해야할 것같은데? 그럼 매번 클릭 이벤트에 로직을 바꿔야하나? 고민을 했다.
결국 렌더링후에 해당 버튼컨트롤에 이벤트를 계속 바꾸기가 좋지 않았다. 그래서 각 월마다 이미 이벤트가 추가된 상태의 바뀌지않는 버튼컨트롤러를 만들어서 추가하기로 했다. 월 기준으로 element를 추가할 수 있는 renderMonthElement
속성에 추가하기로 했다.
월단위 캘린더에서 체크할 수 있는 체크 박스 추가하기
<DayPickerSingleDateController
numberOfMonths={1}
onDateChange={handleChange}
hideKeyboardShortcutsPanel
noBorder
renderMonthElement={(props) =>
MonthHeader({ ...props, handleChangeMulti })
}
Code language: HTML, XML (xml)
calendar.js
에서 MothHeader에 이벤트핸들러까지 전달해서 캘린더의 상태를 변경하도록 하였다.
const MultipleDatePickerCalendar = () => {
const [dates, setDates] = useState([]);
const handleChangeMulti = (days) => {
// dates array
// 선택되어있지 않은 값들만 전달받음
setDates([...dates, ...days]);
};
Code language: JavaScript (javascript)
선택되어있지 않는 값들은 MonthHeader에서 버블링해서 상위 컴포넌트에 전달될 것이다.
const WeekHeaderInput = ({ month, handleChangeMulti }) => {
const dayHandle = (dayName) => {
const dddd = document.querySelectorAll(
`table.CalendarMonth_table td[aria-label*="/${month}-${dayName}요일"]:not(.CalendarDay__blocked_calendar):not(.CalendarDay__blocked_out_of_range)`,
);
let result = [];
dddd.forEach((day) => {
const date = day.getAttribute('aria-label').replace(/\/.*/g, '');
result.push(
moment(date).set({
hour: 12,
minute: 0,
second: 0,
millisecond: 0,
}),
);
});
handleChangeMulti(result);
};
return (
<UL className="DayPicker_weekHeader_ul DayPicker_weekHeader_ul_1">
<li
className="DayPicker_weekHeader_li DayPicker_weekHeader_li_1"
style={{ width: '39px' }}
>
<Btn onClick={() => dayHandle('일')}>
<FaCheck />
</Btn>
</li>
<li
className="DayPicker_weekHeader_li DayPicker_weekHeader_li_1"
style={{ width: '39px' }}
>
<Btn onClick={() => dayHandle('월')}>
<FaCheck />
</Btn>
</li>
<li
className="DayPicker_weekHeader_li DayPicker_weekHeader_li_1"
style={{ width: '39px' }}
>
<Btn onClick={() => dayHandle('화')}>
<FaCheck />
</Btn>
</li>
<li
className="DayPicker_weekHeader_li DayPicker_weekHeader_li_1"
style={{ width: '39px' }}
>
<Btn onClick={() => dayHandle('수')}>
<FaCheck />
</Btn>
</li>
<li
className="DayPicker_weekHeader_li DayPicker_weekHeader_li_1"
style={{ width: '39px' }}
>
<Btn onClick={() => dayHandle('목')}>
<FaCheck />
</Btn>
</li>
<li
className="DayPicker_weekHeader_li DayPicker_weekHeader_li_1"
style={{ width: '39px' }}
>
<Btn onClick={() => dayHandle('금')}>
<FaCheck />
</Btn>
</li>
<li
className="DayPicker_weekHeader_li DayPicker_weekHeader_li_1"
style={{ width: '39px' }}
>
<Btn onClick={() => dayHandle('토')}>
<FaCheck />
</Btn>
</li>
</UL>
);
};
const MonthHeader = ({ month, handleChangeMulti }) => {
return (
<>
<strong id={`MonthTitle${month.format('MM')}`}>
{month.format('YYYY년 MMMM')}
</strong>
<WeekHeaderInput
month={month.format('MM')}
handleChangeMulti={handleChangeMulti}
/>
</>
);
};
export default MonthHeader;
Code language: JavaScript (javascript)
여기서 문제점은 리액트로 렌더링된 요소들을가지고 document.querySelectorAll로 접근했다는것이 내가 생각할 수 있는 최선의 해결방법이었다.
const dayHandle = (dayName) => {
const dddd = document.querySelectorAll(
`table.CalendarMonth_table td[aria-label*="/${month}-${dayName}요일"]:not(.CalendarDay__blocked_calendar):not(.CalendarDay__blocked_out_of_range)`,
);
let result = [];
dddd.forEach((day) => {
const date = day.getAttribute('aria-label').replace(/\/.*/g, '');
result.push(
moment(date).set({
hour: 12,
minute: 0,
second: 0,
millisecond: 0,
}),
);
});
handleChangeMulti(result);
};
Code language: JavaScript (javascript)
이 부분이 핵심 로직이다. 각 요일로 받은 값을 가지고 react-date에서 만들어낸 규칙적인 요소들을 선택하고 해당 선택된 요소들을 moment를 통해서 date의 값을 규칙적으로 수정해서 calendar.js
상태로 넘긴다. handleChangeMulti
는 calendar.js
에서 받은 함수이다. setDates
를 적용시키는
이렇게 각 월마다 요일을 모두 선택할 수 있는 버튼이 생성되었다!