달력을 만들어보겠습니다.
필요한 변수는 다음과 같습니다.
- date: 달력에 표시해주기 위한 날짜 정보를 담은 변수
- first: 달력이 시작할 때 앞에 남는 부분
- last: 달력 끝날 때 뒤에 남는 부분
- month: first와 last를 제외한 달력의 날짜를 표시할 부분
코딩 시작!
1. 변수 설정
필요한 변수들을 초기화해줍니다.
달력이 켜질 때 오늘 날짜 기준으로 표시해줄 수 있도록 date는 new Date()를 초기값으로 설정해주었습니다.
const dayOfWeek = ["SUN", "MON", "TUE", "WED", "THU", "FRI", "SAT"];
const [date, setDate] = useState(new Date());
const [first, setFirst] = useState([]);
const [last, setLast] = useState([]);
const [month, setMonth] = useState([]);
달력에 표시할 날짜가 정해졌으면 first, last, month에 들어갈 값도 설정해줘야합니다.
다음과 같은 메서드 두 개를 정의해주었습니다.
1) month값 초기화 : setDays()
const setDays = () => {
let arr = new Array(
new Date(date.getFullYear(), date.getMonth() + 1, 0).getDate(),
);
for (let i = 0; i < arr.length; i++) {
arr[i] = i + 1;
}
setMonth(arr);
};
new Date()를 이용하여 날짜를 생성해 줄 때, 일에 들어갈 값이 0이 되면 그 이전 달의 마지막 날을 가르키게 됩니다.
월의 자리에 date.getMonth() + 1하여 월 값을 다음 달로 밀어주면
getDate()를 통해 현재 월의 마지막 날의 일을 알 수 있게 됩니다.
이렇게 얻어온 값들을 배열에 넣어 month 변수에 저장해줍시다.
2) first, last값 초기화 : drawCalendar()
const drawCalendar = () => {
const year = date.getFullYear();
const month = date.getMonth();
const diffFirst = new Date(year, month, 1).getDay(); // 매 월 1일의 요일 -> 0 : 일요일, 6: 토요일
const lastDay = new Date(year, month + 1, 0).getDate(); // 현재 달의 마지막 일
const diffLast = (7 - ((lastDay + diffFirst) % 7)) % 7; // 달력에 마지막 채워지는 칸들
const first = new Array(diffFirst);
for (let i = 0; i < diffFirst; i++) {
first[i] = new Date(year, month, -diffFirst + i + 1).getDate();
}
const last = new Array(diffLast);
for (let i = 0; i < diffLast; i++) {
last[i] = new Date(year, month, i + 1).getDate();
}
setFirst([...first]);
setLast([...last]);
};
- first 구하기
앞의 빈 칸을 계산하기 위해서는 1일이 무슨 요일인지 알아야 합니다.
getDay() 함수를 이용하여 요일을 구해봅시다.
반환값은 일요일의 경우 0, 토요일의 경우 6입니다.
일요일이 달력의 맨 앞에 위치하기 때문에, diffFirst의 크기를 그대로 first의 크기로 사용해줄 수 있습니다.
이제 거기에 들어갈 값을 구해봅시다.
이전 달의 최대 일 수는 new Date()로 생성할 때 일의 값을 0으로 하면 구할 수 있었습니다.
그러면 first배열의 최대 크기부터 이런 식으로 채워주면 되겠죠?
이 경우 diffFirst는 3이고 배열이 -2, -1, 0으로 채워져야 하기 때문에 배열 값은 -diffFirst + i + 1로 채워줄 수 있습니다.
- last 값 구하기
last를 제외한 달력에 채워지는 칸의 개수는 현재 달의 마지막 날 수 + First로 채워지는 칸의 수 이기 때문에 lastDay+diffFirst입니다.
이 중에서 마지막 날에 들어갈 칸 수만 구하면 (lastDay+diffFirst)%7입니다.
달력은 한 줄에 7칸씩 들어가니 7에서 이를 뺀 값을 7로 나눠주면 last값을 구할 수 있겠죠?
하지만 달력이 딱 떨어지는 경우 위의 계산결과가 0이 될 것입니다. 이 때 last가 7이 되어버리기 때문에 이러한 상황을 위하여 전체 결과를 7로 한 번 더 나눠줍니다.
이러한 계산결과를 월이 변경될 때 마다 바꿔줘야 하니 useEffect를 통해 date값이 변경될 때 마다 변수값을 바꿔줍니다.
useEffect(() => {
setDays();
drawCalendar();
}, [date]);
2. 월 바꾸기
월 바꾸기는 간단합니다.
이벤트가 일어날 때 마다 월값을 하나 앞으로 보내거나 하나 뒤로 밀면 됩니다.
const onHandleDecreaseMonth = () => {
// 한 달 앞으로
setDate(new Date(date.getFullYear(), date.getMonth() - 1, date.getDate()));
};
const onHandleIncreaseMonth = () => {
// 한 달 뒤로
setDate(new Date(date.getFullYear(), date.getMonth() + 1, date.getDate()));
};
3. 달력 그리기
이제 배열들을 map함수로 그려주기만 하면 끝입니다.
<>
<CalendarWrapper>
<NextButton
onClick={onHandleDecreaseMonth}
direction="left"
color="#3a578c"
hoverColor="#2b4169"
activeColor="#1a2740"
/>
<CalendarTitle>
{date.getFullYear()}.{" "}
{date.getMonth() + 1 < 10
? "0" + (date.getMonth() + 1)
: date.getMonth() + 1}
</CalendarTitle>
<NextButton
onClick={onHandleIncreaseMonth}
direction="right"
color="#3a578c"
hoverColor="#2b4169"
activeColor="#1a2740"
/>
<CalendarWeek className="grid date_form date_head">
{dayOfWeek.map((item, index) => {
return <DayOfWeek key={index}>{item}</DayOfWeek>;
})}
</CalendarWeek>
<DayOfMonth>
{first.map((index) => {
return (
<OffDay key={index}>
<CalendarDate>{index}</CalendarDate>
</OffDay>
);
})}
{month.map((item, index) => {
return (
<OnDay {...item} key={index}>
<CalendarDate>{item}</CalendarDate>
</OnDay>
);
})}
{last.map((index) => {
return (
<OffDay key={index}>
<CalendarDate>{index}</CalendarDate>
</OffDay>
);
})}
</DayOfMonth>
</CalendarWrapper>
</>
StyledComponent로 CSS까지 적용시켜주면 아래와 같은 달력이 완성됩니다.
전체 소스코드
import { useEffect, useState } from "react";
import styled from "styled-components";
import "./App.css";
// 스타일 지정
const CalendarWrapper = styled.div`
max-width: 1200px;
margin: auto;
padding-bottom: 60px;
`;
const CalendarTitle = styled.div`
display: inline;
font-size: 50px;
font-weight: bold;
color: #778fbd;
font-family: Bebas Neue;
`;
const CalendarWeek = styled.div`
display: grid;
grid-template-columns: repeat(7, 1fr);
margin: 30px auto 0 auto;
`;
const DayOfWeek = styled.div`
color: white;
text-align: center;
font-size: 22px;
line-height: 50px;
height: 50px;
width: 100%;
font-family: Bebas Neue;
border-left: 1px solid white;
background-color: #3a578c;
`;
const DayOfMonth = styled.div`
display: grid;
grid-template-columns: repeat(7, 1fr);
justify-content: center;
margin: auto;
`;
const OnDay = styled.div`
color: #495f7c;
text-align: center;
line-height: 40px;
height: 126px;
width: 100%;
display: inline-block;
border-collapse: collapse;
border: 1px solid #eaf1ff;
:nth-child(even) {
background-color: #eaf1ff;
}
:nth-child(odd) {
background-color: #fff;
}
&:hover {
background-color: #2f64a3;
color: white;
transition: all 0.2s linear;
}
`;
const OffDay = styled.div`
background-color: #f7f7f7;
color: #e3e3e3;
line-height: 40px;
height: 126px;
width: 100%;
border-collapse: collapse;
border: 1px solid #eaf1ff;
`;
const NextButton = styled.button`
background-color: inherit;
border-color: transparent
${(props) => (props.direction === "left" ? props.color : "transparent")}
transparent
${(props) => (props.direction === "right" ? props.color : "transparent")};
border-width: 13px ${(props) => (props.direction === "left" ? "15px" : "0px")}
13px ${(props) => (props.direction === "right" ? "15px" : "0px")};
height: 0;
width: 0;
margin: 0 25px;
border-style: solid;
vertical-align: 15px;
&: hover {
border-color: transparent
${(props) =>
props.direction === "left" ? props.hoverColor : "transparent"}
transparent
${(props) =>
props.direction === "right" ? props.hoverColor : "transparent"};
transition: all 0.2s linear;
}
&:active {
border-color: transparent
${(props) =>
props.direction === "left" ? props.activeColor : "transparent"}
transparent
${(props) =>
props.direction === "right" ? props.activeColor : "transparent"};
}
`;
const CalendarDate = styled.div`
display: flex;
float: right;
margin-top: 3px;
margin-right: 13px;
font-family: Aboreto;
font-weight: bold;
font-size: 16px;
color: ${(props) => props.color};
`;
const Calendar = () => {
const dayOfWeek = ["SUN", "MON", "TUE", "WED", "THU", "FRI", "SAT"];
const [date, setDate] = useState(new Date());
const [first, setFirst] = useState([]);
const [last, setLast] = useState([]);
const [month, setMonth] = useState([]);
useEffect(() => {
setDays();
drawCalendar();
}, [date]);
const setDays = () => {
let arr = new Array(
new Date(date.getFullYear(), date.getMonth() + 1, 0).getDate(),
);
for (let i = 0; i < arr.length; i++) {
arr[i] = i + 1;
}
setMonth(arr);
};
const drawCalendar = () => {
const year = date.getFullYear();
const month = date.getMonth();
const diffFirst = new Date(year, month, 1).getDay(); // 매 월 1일의 요일 -> 0 : 일요일, 6: 토요일
const lastDay = new Date(year, month + 1, 0).getDate(); // 현재 달의 마지막 일
const diffLast = (7 - ((lastDay + diffFirst) % 7)) % 7; // 달력에 마지막 채워지는 칸들
const first = new Array(diffFirst);
for (let i = 0; i < diffFirst; i++) {
first[i] = new Date(year, month, -diffFirst + i + 1).getDate();
}
const last = new Array(diffLast);
for (let i = 0; i < diffLast; i++) {
last[i] = new Date(year, month, i + 1).getDate();
}
setFirst([...first]);
setLast([...last]);
};
const onHandleDecreaseMonth = () => {
// 한 달 앞으로
setDate(new Date(date.getFullYear(), date.getMonth() - 1, date.getDate()));
};
const onHandleIncreaseMonth = () => {
// 한 달 뒤로
setDate(new Date(date.getFullYear(), date.getMonth() + 1, date.getDate()));
};
return (
<>
<CalendarWrapper>
<NextButton
onClick={onHandleDecreaseMonth}
direction="left"
color="#3a578c"
hoverColor="#2b4169"
activeColor="#1a2740"
/>
<CalendarTitle>
{date.getFullYear()}.{" "}
{date.getMonth() + 1 < 10
? "0" + (date.getMonth() + 1)
: date.getMonth() + 1}
</CalendarTitle>
<NextButton
onClick={onHandleIncreaseMonth}
direction="right"
color="#3a578c"
hoverColor="#2b4169"
activeColor="#1a2740"
/>
<CalendarWeek className="grid date_form date_head">
{dayOfWeek.map((item, index) => {
return <DayOfWeek key={index}>{item}</DayOfWeek>;
})}
</CalendarWeek>
<DayOfMonth>
{first.map((index) => {
return (
<OffDay key={index}>
<CalendarDate>{index}</CalendarDate>
</OffDay>
);
})}
{month.map((item, index) => {
return (
<OnDay {...item} key={index}>
<CalendarDate>{item}</CalendarDate>
</OnDay>
);
})}
{last.map((index) => {
return (
<OffDay key={index}>
<CalendarDate>{index}</CalendarDate>
</OffDay>
);
})}
</DayOfMonth>
</CalendarWrapper>
</>
);
};
export default Calendar;