지난 번에 react-datepicker 라이브러리를 커스텀하여 DatePicker 공통 컴포넌트를 만들었는데, 아무래도 라이브러리를 커스텀하기 때문에 불필요한 스타일을 빼고 원하는 스타일을 넣는 과정 등이 꽤나 불편하게 느껴졌다. (CSS 코드가 많이 더러워졌다...ㅜ)
그래서 캘린더 만든 김에 DatePicker도 직접 만들어 보려고 한다.
물론 라이브러리가 제공하는 기능이 훨씬 많지만 내 프로젝트에서 필요한 기능만 컴팩트하게 만들어보려고 한다. 그래서 일단 사용자 입력은 제외하고 날짜 선택만 가능하도록 했다.
String, Date 타입에 대한 고민
데이터 자체는 Date 타입이어야 date-fns 라이브러리의 날짜 관련 함수를 사용하기 편리하다.
그러나 사용자에게 시각적으로 보여지는 포맷은 “YYYY-MM-DD”이기 때문에 문자열로 바꾸는 과정이 적어도 한 번 필요하다.
→ 데이터 타입 변환 과정을 최소화하기 위한 방법을 고민했다.
Calendar 내부 날짜는 Date 객체로 사용 / 표현 시에는 포매팅
props로 전달 시 Date 객체로 전달한다.
대신 InputBox 안에 표현할 때는 YYYY-MM-DD 형식을 사용해야 하기 때문에 포맷 변환 함수를 실행해줬다.
const getFormatDate = () => {
return (
date.getFullYear() +
"-" +
(date.getMonth() + 1).toString().padStart(2, "0") +
"-" +
date.getDate().toString().padStart(2, "0")
);
};
Popup fadeout 효과 적용
isOpen 변수의 변경에 따라 DatepickerCalendar 요소가 바로 언마운트 되므로 isOpen 변수가 바뀌는 시간에 딜레이를 줘야 한다.
- 닫힘 이벤트를 부여할 isClose 변수를 생성한다.
- Input 박스가 다시 클릭되거나, 새로운 날짜가 선택되면 isClose 변수를 먼저 바꿔준다. (fadeOut 애니메이션 실행)
- 0.3초 뒤에 isOpen 변수를 바꿔주어 컴포넌트를 아예 언마운트 시킨다.
Disabled 추가
일단 임시로 오늘 날짜보다 이전 날짜만 disable 처리해준다.
(이후 공휴일 API 연결하여 해당 날짜에 대한 disable 처리 추가 예정)
const isDisableDate = (day: number) => {
return isPast(
new Date(baseDate.getFullYear(), baseDate.getMonth(), day + 1)
);
};
date-fns 라이브러리의 isPast 함수를 이용했다.
오늘 날짜와 인수로 받은 날짜를 비교하는데, 오늘 날짜와 같아도 isPast가 true를 반환하므로 + 1을 해주었다.
☑ DateRangePicker를 만들며 today와 비교하는 것 뿐 아니라 startDate와 비교해야 할 필요도 있어졌다. (다시 없어졌다.)
하나의 인수를 받아 오늘 날짜와 비교하는 isPast 함수 대신 두 개의 인수를 받아 두 날짜를 비교하는 isBefore 사용으로 변경했다.
// today와 비교하면 isPast 함수 사용과 동일한 결과
const isDisableDate = (day: number) => {
return isBefore(
new Date(baseDate.getFullYear(), baseDate.getMonth(), day + 1),
today
);
};
💥 style은 disable이지만 여전히 선택되는 문제 !!!!!!!
→ 날짜 선택 함수에 disable한 날짜의 경우 아무런 동작이 일어나지 않도록 return문 작성
const changeDate = (day: number) => {
if (isDisableDate(day)) return;
...
}
DateRangePicker
endDate, isRange, selectEndDate 인수 추가 (옵션)
DateRangePicker의 경우 두 날짜를 모두 선택하기 전까지는 picker가 닫히지 않게 했다.
- 두 번째로 선택한 날짜가 startDate보다 이전일 경우 startDate를 변경하고 date picker를 닫지 않고 endDate 선택을 다시 하도록 유도했다.
const [selectOne, setSelectOne] = useState(false); // 시작일 선택 여부
// 날짜 선택
const changeDate = (day: number) => {
if (isDisableDate(day)) return;
if (isRange && selectOne && selectEndDate) {
const newDate = new Date(
baseDate.getFullYear(),
baseDate.getMonth(),
day
);
if (isBefore(newDate, startDate)) {
selectStartDate(newDate);
} else {
selectEndDate(newDate);
}
} else {
selectStartDate(
new Date(baseDate.getFullYear(), baseDate.getMonth(), day)
);
setSelectOne(true);
}
};
스타일링
조건부 클래스 할당
- 범위 포함 여부
- 선택된 시작일
- 선택된 종료일
- 오늘 날짜
DateRangePicker에서 시작일만 선택했을 때, calendar-selected calendar-within selected-start selected-end 클래스가 모두 지정되는 문제가 있었다.
클래스 부여 순서와 return 문 분기 처리를 통해 클래스 할당을 조정하여 해결했다.
// 날짜별 클래스 지정
const setClassName = (day: number) => {
let classNames: string[] = [];
const thisDay = new Date(baseDate.getFullYear(), baseDate.getMonth(), day);
// 오늘 날짜
if (isSameDay(today, thisDay)) classNames.push("calendar-today");
// 선택된 시작일
if (isSameDay(startDate, thisDay))
classNames.push("calendar-selected", "selected-start");
// --- 하나만 선택하는 경우 || 시작일만 선택한 경우 그대로 return ----
if (!isRange || (endDate && isSameDay(startDate, endDate)))
return classNames.join(" ");
// 선택된 종료일
if (endDate && isSameDay(endDate, thisDay))
classNames.push("calendar-selected", "selected-end");
// 범위 포함 여부
if (
endDate &&
isWithinInterval(thisDay, { start: startDate, end: endDate })
) {
classNames.push("calendar-within");
}
return classNames.join(" ");
};
좀 지저분한 것 같기는 하다….
// 범위 내 날짜
.calendar-within {
background-color: ${({ theme }) => theme.teamColors.blueSoft};
}
// 선택된 날짜 하이라이팅
.calendar-selected {
position: relative;
color: white;
z-index: 12;
&::before {
content: "";
position: absolute;
width: 100%; /* 원의 너비 */
height: 100%; /* 원의 높이 */
background-color: ${({ theme }) => theme.teamColors.blueHard};
border-radius: 100%;
transition: 0.2s;
z-index: -1;
}
}
// 범위 선택 시작일
.selected-start {
border-radius: 100% 0 0 100%;
}
// 범위 선택 종료일
.selected-end {
border-radius: 0 100% 100% 0;
}
// 오늘 날짜 표시
.calendar-today {
background-color: ${({ theme }) => theme.colors.grayBg};
border-radius: 100%;
}
6.10 찾아낸 이슈
범위 내에 오늘 날짜가 포함되어 있으면 calendar-within 클래스 스타일보다 calendar-today 스타일이 우선된다.
해결
style.tsx 파일의 클래스 작성 순서 변경
가장 마지막 클래스의 스타일로 오버라이딩되므로 calendar-today를 상위로 올리고 calendar-within이 더 나중에 적용되도록 함
아직 남은 문제
calendar-today 클래스를 상위로 올림으로써 border-radius 속성이 오버라이딩됐다.
순서 조정으로는 원하는 결과를 얻어낼 수 없다….
해결
선택된 날짜에 하이라이팅을 추가했던 방법처럼 before 가상요소를 이용했다.
// 오늘 날짜 표시
.calendar-today {
position: relative;
z-index: 12;
&::before {
content: "";
position: absolute;
width: 100%; /* 원의 너비 */
height: 100%; /* 원의 높이 */
background-color: ${({ theme }) => theme.colors.grayBg};
border-radius: 100%;
transition: 0.2s;
z-index: -1;
}
}
대신 이 경우에는 오늘 날짜 표시가 선택된 영역 안에서도 계속 남아있다는 단점이 있지만… 타협했다.
'React' 카테고리의 다른 글
[ React ] weekSelector 컴포넌트 구현 (0) | 2024.06.21 |
---|---|
useRef로 영역 외 클릭 시 감지 기능 구현하기 (0) | 2024.06.19 |
react-router-dom의 outlet으로 layout 구성하기 (0) | 2024.06.13 |
[ React ] 캘린더 컴포넌트 직접 만들기 (feat. date-fns) (0) | 2024.06.12 |
react-datepicker 라이브러리로 공통 컴포넌트 만들기 (DatePicker, DateRangePicker) (0) | 2024.06.07 |