Dropdown과 Datepicker로 기간 선택하기

ChaeChae·2022년 11월 5일
1
post-thumbnail

회사의 재활 운동 관련 서비스에서 리포트 페이지를 개발하게 되었다. 기간을 설정하면 해당 기간 내에 생성된 운동 기록들이 뜨고, 그중에 몇 개를 선택하여 날짜별로 리포트를 볼 수 있는 기능이 있다. 기간 설정 기능을 개발하면서 JavaScript Date 객체와 내장 메서드, date picker 라이브러리 등을 활용하여 날짜를 다루는 방법에 대해 배울 수 있었다.

예시 코드 전체 보기: https://codesandbox.io/s/date-picker-with-dropdown-3u6ssf

요구사항 분석

  • 기간을 선택하는 드롭다운과, 시작일과 종료일을 나타내는 두 개의 date picker가 있다.
  • 드롭다운에는 전체, 일주일, 1개월, 3개월, 6개월 옵션이 있다. 옵션을 선택하면 종료일은 오늘, 시작일은 오늘을 기준으로 해당 기간을 뺀 날짜로 설정한다. 전체를 선택하면 date picker 안의 텍스트를 모두 "YYYY/MM/DD"로 설정한다.
  • date picker를 통해 사용자가 날짜를 지정할 수 있다. 시작일은 종료일보다 같거나 작아야 한다.

React Datepicker 라이브러리 사용하기

여러 가지 date picker 라이브러리가 있지만, HackerOne에서 만든 React Datepicker 라이브러리를 추천한다. CSS 클래스명으로 스타일을 커스터마이징하기도 편하고, 기능적으로도 다양한 옵션이 있으며 홈페이지에 문서화도 잘 되어있다. 웬만한 기능들은 다 내장되어 있으니 문서에서 필요한 내용을 찾아 예시대로 사용하면 된다.

React Datepicker 공식 사이트 thtps://reactdatepicker.com/

DatePicker.js

  • 요구사항에 date picker input 안의 텍스트를 "YYYY/MM/DD"라는 string으로 나타내는 부분이 있어 custom input을 사용했다.
  • 스타일을 커스터마이징하기 위해 기본 스타일인 react-datepicker.css를 먼저 import하고, 직접 작성한 CSS 파일을 이어서 import 했다.
import { forwardRef } from "react";
import DatePicker from "react-datepicker";
import "react-datepicker/dist/react-datepicker.css";
import "./DatePicker.css";

const CustomDatePicker = (props) => {
  const CustomInput = forwardRef((props, ref) => (
    <button className="datepicker-input" onClick={props.onClick} ref={ref}>
      {props.value}
    </button>
  ));

  return (
    <div>
      <DatePicker
        selected={props.selectedDate}
        onChange={props.setSelectedDate}
        dateFormat="yyyy/MM/dd"
        customInput={<CustomInput />}
        showPopperArrow={false}
      />
    </div>
  );
};

export default CustomDatePicker;

App.js

  • 시작일과 종료일을 상위 컴포넌트에서 props로 전달하여 각각의 date picker에서 설정할 수 있도록 했다.
  • 기간을 설정하고 검색하는 버튼이 있는데, 시작일이 종료일보다 이후이면 안 되므로 disabled 조건을 설정해주었다.
import { useState } from "react";
import CustomDatePicker from "./DatePicker";
import "./styles.css";

const App = () => {
  const [startDate, setStartDate] = useState(new Date());
  const [endDate, setEndDate] = useState(new Date());

  return (
    <div className="App">
      <CustomDatePicker
        selectedDate={startDate}
        setSelectedDate={setStartDate}
      />
      <span>-</span>
      <CustomDatePicker
        selectedDate={endDate}
        setSelectedDate={setEndDate}
      />
	  <button className="search-button" disabled={startDate > endDate}>
        검색
      </button>
    </div>
  );
}

export default App;

기간 선택 Dropdown 구현하기

드롭다운을 구현하는 통상적인 방법은 널리 알려져 있으므로 자세한 설명은 생략하였다.

import { BsChevronDown } from "react-icons/bs";
import { PERIOD } from "./constants";
import "./Dropdown.css";

const Dropdown = (props) => {
  return (
    <div className="dropdown">
      <button onClick={props.toggleDropdown}>
        {props.selectedPeriod}
        <BsChevronDown />
      </button>
      {props.isDropdownOpen && (
        <ul>
          {PERIOD.map((item) => (
            <li key={item.id}>
              <button value={item.name} onClick={props.onClickPeriod}>
                {item.name}
              </button>
            </li>
          ))}
        </ul>
      )}
    </div>
  );
};

export default Dropdown;

App.js

const [isDropdownOpen, setIsDropdownOpen] = useState(false);
const [selectedPeriod, setSelectedPeriod] = useState(PERIOD[3].name);

const toggleDropdown = () => {
  setIsDropdownOpen((prev) => !prev);
};

const onClickPeriod = (e) => {
  const { value } = e.target;
  setSelectedPeriod(value);
  toggleDropdown();
};

return (
  <div className="App">
    <Dropdown
      isDropdownOpen={isDropdownOpen}
      toggleDropdown={toggleDropdown}
      selectedPeriod={selectedPeriod}
      onClickPeriod={onClickPeriod}
    />

	{/* DatePicker 생략 */}
  </div>
);

이제 가장 핵심적인 로직만 남았다. 드롭다운에서 기간을 선택하면 date picker의 날짜가 이에 맞게 자동으로 세팅되는 부분이다. JavaScript의 Date 객체와 내장 메서드인 getDate, setDate, getMonth, setMonth를 사용하여 구현했다.

App.js

  • 우선 오늘 0시를 start 변수로 설정한다. 그냥 new Date()를 사용하면 현재 시각 정보까지 포함되어 있어 날짜를 더하거나 뺄 때 예상치 못한 결과가 나올 수 있다. 그래서 Datestring으로 변환했다가 다시 Date로 변환하여 0시로 맞춰주었다.
  • 일주일을 선택하면 date에서 7을 빼주고, 1, 3, 6개월 옵션을 선택하면 month에서 N만큼 빼주었다.
  • 이렇게 계산된 start 값을 시작일로 설정한다. 전체를 선택한 경우엔 검색이 가능한 최소 날짜로 설정한다. 종료일은 항상 오늘 0시로 설정한다.
const setDateRange = (period) => {
  const start = new Date(formatDate(new Date()));
  
  if (period === "1주일") {
    start.setDate(start.getDate() - 7);
  } else if (period.includes("개월")) {
    start.setMonth(start.getMonth() - Number(period[0]));
  }
  
  setStartDate(period === "전체" ? new Date("2020-01-01") : start);
  setEndDate(new Date(formatDate(new Date())));
};

const onClickPeriod = (e) => {
  const { value } = e.target;
  setSelectedPeriod(value);
  setDateRange(value);
  setIsDropdownOpen((prev) => !prev);
};

여기서 주의할 점은 setDate, setMonth 등의 메서드가 원본 Date 객체를 바꿔버린다는 것이다. state는 setState 함수로만 업데이트해야 하므로 startDate, endDate state에 메서드를 직접 사용하면 에러가 발생한다. start 변수를 새로 만들어 조작한 뒤 setStartDate로 저장하고, setEndDate에도 새로 생성한 Date를 저장하는 것이 핵심이었다. 이번 기회를 통해 Date 객체의 이모저모를 살펴볼 수 있었다.

profile
정리 장인 && FE 개발자

0개의 댓글