회사의 재활 운동 관련 서비스에서 리포트 페이지를 개발하게 되었다. 기간을 설정하면 해당 기간 내에 생성된 운동 기록들이 뜨고, 그중에 몇 개를 선택하여 날짜별로 리포트를 볼 수 있는 기능이 있다. 기간 설정 기능을 개발하면서 JavaScript Date
객체와 내장 메서드, date picker 라이브러리 등을 활용하여 날짜를 다루는 방법에 대해 배울 수 있었다.
예시 코드 전체 보기: https://codesandbox.io/s/date-picker-with-dropdown-3u6ssf
여러 가지 date picker 라이브러리가 있지만, HackerOne에서 만든 React Datepicker
라이브러리를 추천한다. CSS 클래스명으로 스타일을 커스터마이징하기도 편하고, 기능적으로도 다양한 옵션이 있으며 홈페이지에 문서화도 잘 되어있다. 웬만한 기능들은 다 내장되어 있으니 문서에서 필요한 내용을 찾아 예시대로 사용하면 된다.
React Datepicker 공식 사이트 thtps://reactdatepicker.com/
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;
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;
드롭다운을 구현하는 통상적인 방법은 널리 알려져 있으므로 자세한 설명은 생략하였다.
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;
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
를 사용하여 구현했다.
new Date()
를 사용하면 현재 시각 정보까지 포함되어 있어 날짜를 더하거나 뺄 때 예상치 못한 결과가 나올 수 있다. 그래서 Date
를 string
으로 변환했다가 다시 Date
로 변환하여 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 객체의 이모저모를 살펴볼 수 있었다.