여행 계획 페이지로 넘어가기 전에 여행 계획 일정을 사용자가 설정할 수 있도록 하기 위해 캘린더를 띄워야 하는데, 화면 설계를 다음과 같이 하였다.
달력 컴포넌트를 다음과 같이 구성했다.
import React , { useState } from 'react' ;
import { DateRangePicker } from 'react-date-range-ts' ;
import { addDays } from 'date-fns' ;
import 'react-date-range-ts/dist/styles.css' ; // main style file
import 'react-date-range-ts/dist/theme/default.css' ; // theme css file
const CalendarModal = () => {
const [selectionRange , setSelectionRange ] = useState ({
startDate : new Date (),
endDate : addDays ( new Date (), 1 ),
key : 'selection' ,
});
const handleSelect = (ranges : any ) => {
console . log ( ranges );
setSelectionRange ( ranges . selection );
};
return (
< div >
< h2 > 여행 날짜 선택 </ h2 >
< DateRangePicker
months = { 2 }
direction = "horizontal"
//disabledDates={}
ranges = { [ selectionRange ] }
moveRangeOnFirstSelection = {true}
onChange = { handleSelect }
/>
</ div >
);
};
export default CalendarModal ;
그리고 테스트로 실행시켜봤는데 빈 화면이 나온다.
보니까 react-date-range-ts를 사용해서 생긴 오류같다. react-date-range-ts보다 react-date-range를 사용하는 것이 더 낫겠다고 판단되어 라이브러리를 재설치하였다.
npm install --save react-date-range
npm install --save date-fns
이와 같이 라이브러리를 다운받았다.
근데 이렇게 하면 나는 타입스크립트를 사용하기 때문에 타입이 없다고 오류가 난다. 따라서 src폴더 밑에 react-date-range.d.ts 파일을 생성하고 다음과 같이 타입을 직접 등록해주면 된다.
declare module 'react-date-range' {
import * as React from 'react' ;
export interface DateRange {
startDate : Date | null ;
endDate : Date | null ;
key : string ;
}
export interface DateRangePickerProps {
months ?: number ;
direction ?: 'horizontal' | 'vertical' ;
ranges : DateRange [];
moveRangeOnFirstSelection ?: boolean ;
onChange : ( range : { selection : DateRange }) => void ;
}
export class DateRangePicker extends React . Component < DateRangePickerProps > {}
}
위에서 props는 내가 사용할 것들에 따라 설정해주면 된다.
이렇게 만들어서 실행시켜보면 그냥 기본 캘린더 화면이 나온다. 하지만 나는 이런 느낌의 달력을 원한 게 아니기 때문에 css를 변경해주어야 한다.
우선 app.tsx에 내가 작성한 css파일을 먼저 import 해주겠다.
import '../src/styles/date-range-picker.css' ;
const queryClient = new QueryClient ();
export default function App () {
return (
css 파일에서 첫번째로, 내가 사용하지 않는 div들을 display: none으로 설정한다.
.rdrDefinedRangesWrapper {
display : none ;
}
.rdrDateDisplayWrapper {
display : none ;
}
.rdrMonthAndYearPickers {
display : none ;
}
원래는 prevButton, nextButton을 위의 설계도와 같이 MonthName 옆에 두고 싶었는데 그렇게 하려면 date range picker 말고 그냥 date-range를 사용해서 header를 내가 직접 custom하든가 calendar을 사용하는 게 더 나았을 것 같아서 그 부분은 그냥 넘어가기로 했다..
그리고 어찌저찌 디자인을 조금 바꿔봤는데 문제는 내가 날짜 범위를 선택할 때 hover 시에 글자색이 변경되어야하는데 변경이 안 되는 문제가 생겼다.
보니까 내가 구조를 잘못 이해하고 있었다.
.rdrDayStartPreview .rdrDayNumber span ,
.rdrDayInPreview .rdrDayNumber span ,
.rdrDayEndPreview .rdrDayNumber span {
color : white !important ;
}
이렇게 쓰면 될 줄 알았는데 보니까
구조가 이러했다. Preview랑 rdrDayNumber은 형제 노드인데 내가 부모 자식 관계라고 착각하고 있었다.
그래서 형제 관계에 있을 때만 스타일을 적용시킬 수 있는지 찾아보니까 :has()라는 게 있다는 걸 알게 되었다.
:has()는 최근에 추가된 선택자라서 지원하지 않는 브라우저가 존재하기 때문에 권장하지는 않는다고 한다. 웬만해서는 자바스크립트로 작성하는 걸 추천하지만 유용한 기능인 것 같아 써보기로 했다.
.rdrDayNumber:has ( + .rdrDayStartPreview ) span ,
.rdrDayNumber:has ( + .rdrDayInPreview ) span ,
.rdrDayNumber:has ( + .rdrDayEndPreview ) span {
color : white !important ;
}
이렇게 썼는데 적용이 안 되길래
.rdrDay:has ( .rdrDayStartPreview ) .rdrDayNumber span ,
.rdrDay:has ( .rdrDayInPreview ) .rdrDayNumber span ,
.rdrDay:has ( .rdrDayEndPreview ) .rdrDayNumber span {
color : white !important ;
}
이렇게 변경하니까 잘 된다.
전체 코드
react-date-range.d.ts
declare module 'react-date-range' {
import * as React from 'react' ;
export interface DateRange {
startDate : Date | null ;
endDate : Date | null ;
key : string ;
}
export interface DateRangePickerProps {
locale : object ;
months ?: number ;
direction ?: 'horizontal' | 'vertical' ;
ranges : DateRange [];
moveRangeOnFirstSelection ?: boolean ;
onChange : ( range : { selection : DateRange }) => void ;
minDate : Date ;
maxDate : Date ;
}
export class DateRangePicker extends React . Component < DateRangePickerProps > {}
}
CalendarModal.tsx
import React , { useState } from 'react' ;
import { DateRangePicker } from 'react-date-range' ;
import { addDays , addYears , subDays } from 'date-fns' ;
import { ko } from 'date-fns/locale' ;
import 'react-date-range/dist/styles.css' ; // main style file
import 'react-date-range/dist/theme/default.css' ; // theme css file
const CalendarModal = () => {
const today = new Date ();
const [ selectionRange , setSelectionRange ] = useState ({
startDate : today ,
endDate : today ,
key : 'selection' ,
});
const [ isRangeSelected , setIsRangeSelected ] = useState ( true );
const [ minDate , setMinDate ] = useState ( today );
const [ maxDate , setMaxDate ] = useState ( addYears ( today , 5 ));
const handleSelect = ( ranges : any ) => {
setSelectionRange ( ranges . selection );
setIsRangeSelected ( ! isRangeSelected );
console . log ( isRangeSelected );
if ( isRangeSelected ) {
//아직 set~~이 반영되지 않았기 때문에 조건이 !isRangeSelected가 아님.
if ( today . getTime () < subDays ( ranges . selection . startDate . getTime (), 13 )) {
setMinDate ( subDays ( ranges . selection . startDate . getTime (), 13 ));
}
setMaxDate ( addDays ( ranges . selection . startDate , 13 ));
} else {
setMinDate ( today );
setMaxDate ( addYears ( today , 5 ));
}
};
return (
< div >
< h2 > 여행 날짜 선택 </ h2 >
< DateRangePicker
locale = { ko }
months = { 2 }
direction = "horizontal"
ranges = { [ selectionRange ] }
moveRangeOnFirstSelection = {false}
onChange = { handleSelect }
minDate = { minDate }
maxDate = { maxDate }
/>
</ div >
);
};
export default CalendarModal ;
date-range-picker.css
/* DataRangePicker */
.rdrDefinedRangesWrapper {
display : none ;
}
.rdrDateDisplayWrapper {
display : none ;
}
.rdrMonthAndYearPickers {
display : none ;
}
.rdrMonthAndYearWrapper {
height : 0px ;
}
.rdrMonthAndYearWrapper {
background-color : white ;
border-radius : 4px 4px 0px 0px ;
}
.rdrMonthPicker ,
.rdrYearPicker {
background-color : white ;
border-radius : 3px ;
}
.rdrNextPrevButton ,
.rdrPprevButton {
background-color : white !important ;
}
.rdrDayToday .rdrDayNumber span:after {
position : absolute ;
bottom : 4px ;
left : 50% ;
transform : translate ( -50% , 0 );
width : 18px ;
height : 2px ;
border-radius : 2px ;
background : none !important ;
}
.rdrMonth {
width : 400px ;
}
.rdrMonthName {
text-align : center ;
font-size : 20px ;
font-weight : 600 ;
color : #1b1b1b ;
}
.rdrWeekDay {
font-size : 15px ;
color : #1b1b1b ;
}
.rdrDay {
height : 50px ;
}
.rdrDayNumber {
font-size : 15px ;
}
.rdrSelected ,
.rdrStartEdge ,
.rdrEndEdge {
background : #1b1d1f ;
}
.rdrInRange {
background : #b2b9c0 ;
}
.rdrSelected ,
.rdrInRange ,
.rdrStartEdge ,
.rdrEndEdge {
left : 3px ;
right : 3px ;
border-radius : 5px ;
}
.rdrDayStartOfMonth .rdrInRange ,
.rdrDayStartOfMonth .rdrEndEdge ,
.rdrDayStartOfWeek .rdrInRange ,
.rdrDayStartOfWeek .rdrEndEdget {
border-radius : 5px ;
}
.rdrDayEndOfMonth .rdrInRange ,
.rdrDayEndOfMonth .rdrStartEdge ,
.rdrDayEndOfWeek .rdrInRange ,
.rdrDayEndOfWeek .rdrStartEdge {
border-radius : 5px ;
}
.rdrDayStartOfMonth .rdrDayInPreview ,
.rdrDayStartOfMonth .rdrDayEndPreview ,
.rdrDayStartOfWeek .rdrDayInPreview ,
.rdrDayStartOfWeek .rdrDayEndPreview {
border-radius : 5px ;
}
.rdrDayEndOfMonth .rdrDayInPreview ,
.rdrDayEndOfMonth .rdrDayStartPreview ,
.rdrDayEndOfWeek .rdrDayInPreview ,
.rdrDayEndOfWeek .rdrDayStartPreview {
border-radius : 5px ;
}
.rdrDayInPreview {
background : #b2b9c0 ;
border-radius : 5px ;
left : 3px ;
border : none ;
z-index : 0 ;
}
.rdrDayStartPreview ,
.rdrDayEndPreview {
background : #1b1d1f ;
border-radius : 5px ;
left : 3px ;
border : none ;
z-index : 0 ;
}
.rdrDay:has ( .rdrDayStartPreview ) .rdrDayNumber span ,
.rdrDay:has ( .rdrDayInPreview ) .rdrDayNumber span ,
.rdrDay:has ( .rdrDayEndPreview ) .rdrDayNumber span {
color : white !important ;
}
.rdrDayStartOfWeek:not ( .rdrDayDisabled , .rdrDayPassive ) .rdrDayNumber span {
color : red ;
}
.rdrDayEndOfWeek:not ( .rdrDayDisabled , .rdrDayPassive ) .rdrDayNumber span {
color : blue ;
}
dateRangePicker을 사용한 캘린더 결과물(참고로 날짜는 최대 2주만 선택할 수 있도록 설정함)
이렇게 date range picker을 사용해서 날짜 선택 기능이 있는 캘린더를 만들었지만 만약에 다음에 또 만들어야 한다면 date-picker 또는 calendar을 사용하는 게 더 편할 것 같다.
참고
1. https://www.npmjs.com/package/react-date-range
2. https://velog.io/@e_juhee/data-range-picker-datepicker-custom-1
3. https://inpa.tistory.com/entry/%F0%9F%8C%9F-css-where-is-has-not#is_%EA%B0%80%EC%83%81_%EC%84%A0%ED%83%9D%EC%9E%90