몇 달 전 개인 프로젝트(가계부 웹서비스) 제작 중 달력이 필요했는데 시간이 촉박하고 구현도 까다로웠던 기억이 있어 react-calendar
라이브러리를 사용했었다.
하지만 라이브러리 특성상 커스터마이징이 힘들기도 했고, 얼마 전 이전 달로 이동할 때 월별 총 금액이 이상하게 계산되는 이슈를 발견해서 해결하자니 이벤트핸들러를 뜯어보기가 더 까다로웠다.
그래서 내가 만들어보기로..ㅎ
(고민의 흔적)
달력의 한 페이지엔 이전 달 일부
- 내가 선택한 달 전체
- 다음 달 일부
가 들어간다.
날짜 채우는 방식 (선택한 달의 총 일수 = n, 시작 요일 = start)
0. 달력은 7의 배수로 모두 채워져야 한다. (28일 / 35일 / 42일)
1. prev(노출되는 이전 달 날짜 수) : 일요일 ~ 선택한 달의 시작 요일 전날까지 일수 (시작 요일이 일요일일 경우 없음)
➡️ prev = start
2. next(노출되는 다음 달 날짜 수) : 선택한 달의 마지막 요일 ~ 토요일까지 일수 (마지막 요일이 토요일일 경우 없음)
➡️ r = (n + start) % 7
➡️ r === 0 : next = 0
➡️ r !== 0 : next = 7 - r (7일 중 마지막 주에 채워진 요일 제외한 일수)
*n + start : 달력의 첫번째 일요일부터 시작해야하기 때문
const Calendar = () => {
const [newDate, setNewDate] = useState<Date>(new Date());
const [year, setYear] = useState(0);
const [month, setMonth] = useState(0);
const [date, setDate] = useState(0);
const [lastDate, setLastDate] = useState<number>(0);
const [lastDatePrev, setLastDatePrev] = useState<number>(0);
const [firstDay, setFirstDay] = useState<number>(0);
const [displayDate, setDisplayDate] = useState("");
const [openSelect, setOpenSelect] = useState(false);
useEffect(() => {
const yy = newDate.getFullYear();
const mm = newDate.getMonth();
const dd = newDate.getDate();
setYear(yy);
setMonth(mm);
setDate(dd);
setLastDate(new Date(yy, mm + 1, 0).getDate());
setLastDatePrev(new Date(yy, mm, 0).getDate());
setFirstDay(new Date(yy, mm, 1).getDay());
setDisplayDate(`${yy}년 ${mm + 1}월 ${dd}일`);
}, [newDate]);
const handleDateArr = (
target: "prev" | "cur" | "next",
lastDate: number,
lastDatePrev: number,
firstDay: number
) => {
let res: number[] = [];
const r = (lastDate + firstDay) % 7; // remainder
const prev = firstDay; // 이전 달 표기 일수
const next = r === 0 ? 0 : 7 - r; // 다음 달 표기 일수
if (target === "prev") {
res = Array.from({ length: prev }, (_, i) => lastDatePrev - i).sort(
(a, b) => a - b
);
} else if (target === "cur") {
res = Array.from({ length: lastDate }, (_, i) => i + 1);
} else if (target === "next") {
res = Array.from({ length: next }, (_, i) => i + 1);
}
return res;
};
const handleChange = (type: "prev" | "next", target: "year" | "month") => {
if (type === "prev") {
target === "month"
? setNewDate(new Date(year, month - 1, 1))
: setNewDate(new Date(year - 1, month, 1));
} else {
target === "month"
? setNewDate(new Date(year, month + 1, 1))
: setNewDate(new Date(year + 1, month, 1));
}
};
const handleSelect = (target: number) => {
const mm = (month + 1).toString().padStart(2, "0");
const dd = target.toString().padStart(2, "0");
const selectedDate = `${year}-${mm}-${dd}`;
};
const handleChangeDirectly = (e: React.ChangeEvent<HTMLSelectElement>) => {
const { value, name } = e.target;
if (name === "year") setNewDate(new Date(Number(value), month - 1, 1));
// date = 1 : 오늘이 30일일 때 2월로 이동하면 다음달로 넘어감 (없는 값)
if (name === "month") setNewDate(new Date(year, Number(value) - 1, 1));
// date = 1 : 오늘이 30일일 때 2월로 이동하면 다음달로 넘어감 (없는 값)
};
return (
<StCalendar>
<StHeader>
<h2>Calendar</h2>
<h3>{displayDate}</h3>
<StBtnContainer>
<button onClick={() => handleChange("prev", "year")}>
PREV YEAR
</button>
<button onClick={() => handleChange("prev", "month")}>
PREV MONTH
</button>
<button onClick={() => handleChange("next", "month")}>
NEXT MONTH
</button>
<button onClick={() => handleChange("next", "year")}>
NEXT YEAR
</button>
</StBtnContainer>
</StHeader>
<StBody>
<StDayList>
{DAY_LIST.map((val) => {
return <li key={val}>{val}</li>;
})}
</StDayList>
<StDateList>
{handleDateArr("prev", lastDate, lastDatePrev, firstDay).map(
(val) => (
<StPrevDate
key={`prev-${val}`}
onClick={() => handleChange("prev", "month")}
>
<span>{val}</span>
</StPrevDate>
)
)}
{handleDateArr("cur", lastDate, lastDatePrev, firstDay).map(
(val) => (
<StCurDate
key={`current-${val}`}
onClick={() => handleSelect(val)}
>
<span>{val}</span>
</StCurDate>
)
)}
{handleDateArr("next", lastDate, lastDatePrev, firstDay).map(
(val) => (
<StNext
key={`next-${val}`}
onClick={() => handleChange("next", "month")}
>
<span>{val}</span>
</StNext>
)
)}
</StDateList>
</StBody>
</StCalendar>
);
};
export default Calendar;
달력 만들기 성공!
코드 더 다듬어서 프로젝트에 넣어야겠다.
2년 전쯤 처음 자바스크립트, 리액트 공부할 때 달력을 만들려다 (몇 시간을 고민하고) 실패했었다. 그래서 처음 프로젝트 만들 때에도 라이브러리를 사용했던 것..
지금은 한 20분 정도 로직 고민해보고 금방 만들었다. 뿌듯 😎