TIL33, React: react-dates 커스텀 하기

sunghoonKim·2021년 1월 3일
7

react-dates를 가지고 노는 중 배운 것들을 정리한다. 스토리북을 참고하면서 공부하였다.


데이트 피커 종류

react-dates는 여러 종류의 데이트 피커를 제공한다.

1. DRP (Date Range Picker)

인풋 창이 포함된, 범위를 선택할 수 있는 데이트 피커.

2. SDP (Single Date Picker)

인풋 창이 포함된, 한가지 날짜를 선택할 수 있는 데이트 피커.

3. Day Picker Range Controller

인풋 창이 포함되지 않은, 범위를 선택할 수 있는 데이트 피커.

4. Day Picker Single Date Controller

인풋 창이 포함되지 않은, 한가지 날짜를 선택할 수 있는 데이트 피커.

우리 프로젝트에서는 인풋창이 따로 필요하지 않으므로, DayPickerRangerController 를 선택하여 사용하였다.


시작날짜, 종료날짜 토글링

시작날짜와 종료날짜는 focusedInputonFocusChange란 프롭스를 통해서 컨트롤 할 수 있다.

focusedInput"startDate", "endDate" 라는 스트링 혹은 null 을 받는다. (이거 알아내려고 2시간 동안 소스코드를 뒤져야 했다.)

onFocusChange 는 위의 3가지 값중 하나를 리턴한다.

focusedInputnull 일 경우, 당연하게도, 날짜를 선택할 수 없다. 그렇기 때문에, onFocusChange 프롭스에 넘겨주는 콜백함수를 통해서, focusedInputstartDateendDate 사이를 오가도록 로직을 구현해 주어야 한다.

state = {
  {...}
  focusedInput: "startDate",
};

...

<DayPickerRangeController
  focusedInput={this.state.focusedInput} // PropTypes.oneOf([START_DATE, END_DATE]) or null,
  onFocusChange={focusedInput => this.setState({ focusedInput: focusedInput || 'startDate' })} // PropTypes.func.isRequired,
/>

특정 날짜 블록하기(feat. moment.js)

react-dates 에서 특정 날짜를 블록하기 위해서는 isDayBlocked 라는 프롭스를 넘겨주면 된다.

해당 프롭스에는 콜백함수가 들어간다. 이 콜백 함수는 달력이 렌더링 되는 과정에서 각각의 날짜에 대해서 실행이 되며, boolean 값을 리턴한다.

따라서, 이 함수를 이용, 각각의 날짜를 특정 조건을 통해 비교하여 블록이 되어야 한다면 true 값을, 그렇지 않다면 false 값을 리턴하면 된다.

한 가지 복잡했던 부분은 각각의 날짜는 moment 객체이므로, 패키지를 설치해야하고, moment.js 에 대한 공부가 필요했다는 점.

moment 객체란, 간단히 말해서 자바스크립트에서 기본으로 제공하는 Date 와 비슷하게 날짜정보를 다루는 패키지인데, 좀 더 직관적이고 사용하기 쉽다. 또한 더 다양한 메소드를 제공한다.

우리 프로젝트의 경우, 상정해야할 경우의 수가 4가지 였는데, 1) 체크인 날짜, 체크 아웃 날짜가 둘다 선택되었을 때, 2) 체크인 날짜만 선택되었을 때, 3) 체크 아웃 날짜만 선택되었을 때, 4) 둘 다 선택되지 않았을 때 이다.

1) 의 경우, 체크인 날짜와 체크아웃 날짜, 그리고 체크인 날짜와 체크아웃 사이의 날짜를 제외한 나머지는 블록된다.
2) 의 경우, 체크인 날짜 전과 가장 가까운 예약 불가한 날짜 이후 (없다면 끝까지) 를 블록한다.
3) 의 경우, 체크 아웃 이후 날짜와 체크 아웃에서 가장 가까운 예약 불가 전 날짜들을 (없다면 오늘날짜 전 날짜들을) 블록한다.
4) 의 경우, 오늘 전 날짜 와 예약 불가한 날짜들을 블록한다.

위 경우의 수를 상정하여, 특정날짜를 블록하는 하는 함수의 로직은 아래와 같다.

  state = {
    {...}
    blockedDate: ["2021-02-01", "2021-02-02", "2021-02-28"],
  };

  const isBlocked = day => {
    let bool = false;
    const newDay = moment(day.format("YYYY-MM-DD"));

    // Case #1. 체크인 체크아웃이 둘다 선택.
    if (startDate !== null && endDate !== null) {
      const newStart = moment(startDate.format("YYYY-MM-DD")).subtract(1, "days");
      const newEnd = moment(endDate.format("YYYY-MM-DD")).add(1, "days");
      bool = !newDay.isBetween(newStart, newEnd);
      return bool;

      // Case #2. 체크인만 선택.
    } else if (startDate !== null) {
      const newStart = moment(startDate.format("YYYY-MM-DD")).subtract(1, "days");
      const next = closestNextBlockedDate(newStart, blockedDate);
      if (next) {
        bool = !newDay.isBetween(newStart, next);
      } else {
        newStart.add(1, "days");
        bool = newDay.isBefore(newStart);
      }
      return bool;

      // Case #3. 체크아웃만 선택.
    } else if (endDate !== null) {
      const newEnd = moment(endDate.format("YYYY-MM-DD")).add(1, "days");
      const prev = closestPrevBlockedDate(newEnd, blockedDate);
      if (prev) {
        bool = !newDay.isBetween(prev, newEnd);
      } else {
        bool = !newDay.isBetween(moment(), newEnd);
      }
      return bool;

      // Case #4. 둘다 선택 되지 않음.
    } else {
      bool = blockedDate?.some(date => day.format("YYYY-MM-DD") === date) || day.isBefore(moment());
      return bool;
    }
  };  

  render (
    return (
      <DayPickerRangeController
        {...}
        isDayBlocked={this.isBlocked}
      />
    )
  )

각각의 조건마다, newStart, newEnd 와 같이 새로운 moment 객체를 생성하고, 해당 객체들로 로직을 구현하였다. 그 이유는, isBetween 와 isBefore 등의 메소드가 기준이 되는 날짜를 포함하거나 포함 하지 않는 기준이 달라, 그때그때 subtract 혹은 add 와 같은 moment 메소드로 날짜를 수정해주어야 했는데, 이 메소드들은 원본 객체를 변환시켰다. (will mutate original object) 그래서 기존의 원본 객체을 이용할 경우, 체크인 체크아웃 날짜들이 로직을 실행할 때 마다 변했다. 이런 부분에 대해서 moment 공식 문서에서는 moment 객체를 다시 한번 moment() 의 인자로 넘겨주는 것으로 객체를 복사하는 방법을 제시한다. 따라서, 로직을 실행하기 전 항상 newStart, newEnd 와 같이 기존의 객체를 복사하고, 해당 복사 객체들을 사용하여, 원본 객체가 변하는 것을 방지하였다.

이후 체크인 날짜와 체크 아웃 날짜를 입력하면 아래와 같이 잘 작동한다. ^-^ 👍


스타일 바꾸기

react-dates 공식문서에서는 소스 코드의 css 파일을 오버라이드 하는 것으로 커스텀 스타일링을 하라고 제시한다. 그리고선 예시로 몇가지의 클래스 네임과 그 역할만 알려주고, 나머지는 직접 찾아보라고 적혀있었다.

그래서 직접 소스 코드를 뜯어가면서 (900줄 가량의 css..), 조금씩 스타일링을 변경해보면서 각각의 클래스 네임이 어떤 역할을 하는지 알아내어야 했다.

캘린더를 커스텀 하면서 필요했던 부분들만 옮겨본다.

// 오른쪽 구석의 화살표를 안보이게 한다.
.DayPickerKeyboardShortcuts_buttonReset {
    display: none;
}

// 달력 각 칸의 기본 스타일.
.CalendarDay__default {
  border: none;
  border-radius: 50%;
  vertical-align: middle;
  outline: none; 
}

// 달력 각 칸에 호버가 되었을 때 스타일
.CalendarDay__default:hover {
  background: transparent;
  border: none;
  color: black;
  box-shadow: inset 0 0 0 1px black;
}

// 체크인 체크아웃이 선택되었을 때 그 사의 날짜들에 대한 스타일
.CalendarDay__selected_span {
  background-color: #f7f7f7;
  border: none;
  color: black;
}

// 체크인 체크아웃이 선택되었을 때 그 사의 날짜들에 호버 혹은 클릭했을 시 스타일
.CalendarDay__selected_span:active,
.CalendarDay__selected_span:hover {
  color: black;
  background-color: #f7f7f7;
}

// 선택된 체크인 체크아웃 날짜에 대한 스타일
.CalendarDay__selected,
.CalendarDay__selected:active,
.CalendarDay__selected:hover {
  background: black;
  border: none;
  color: white;
}

// 블록된 날짜에 대한 스타일링
.CalendarDay__blocked_calendar,
.CalendarDay__blocked_calendar:active,
.CalendarDay__blocked_calendar:hover {
  background: white;
  border: none;
  color: #d2d2d2;
  box-shadow: none;
  text-decoration: line-through;
}

// 선택될 범위에 대한 스타일링
.CalendarDay__hovered_span,
.CalendarDay__hovered_span:hover {
  color: black;
  background-color: #f7f7f7;
  border: none;
}

// 요일 표시 부분에 대한 스타일.
.CalendarMonth_caption {
  margin-bottom: 10px;
}

해당 스타일링이 적용된 결과물은 아래와 같다.

기존의 캘린더는 이렇게 생겼다.


2일째 소스코드만 주구장창 뜯어보고 있다. 살려도..

3개의 댓글

comment-user-thumbnail
2021년 1월 4일

성훈님! 위협받고 계시다면 당근을 흔들어주세요!

답글 달기
comment-user-thumbnail
2021년 1월 7일

성훈님! 위협받고 계시다면 당근을 흔들어주세요!

답글 달기

성훈님! 위협받고 계시다면 당근을 흔들어주세요!

답글 달기