const [today, setToday] = useState(dayjs())
const daysInMonth = today.daysInMonth();
const firstDayOfMonth = dayjs(today).startOf('month').locale('ko');
const dates = [];
for (let i = 1; i <= daysInMonth; i++) {
const date = dayjs(firstDayOfMonth).add(i - 1, 'day');
dates.push(date);
}
이번 포스트는 지난번 포스트에 이어서 진행된다. 간단하게 지난번 포스트를 복기하자면, useState(dayjs())를 통하여 사용자가 화면을 로드한 시점이 상태에 저장되도록 하였다.
그리고 만들어진 버튼 3개와 input을 통하여 제어되는 함수부를 기록했었다.
// 함수
const preMonth = () => {
setToday(dayjs(today).subtract(1,"month"))
setInputValue('')
}
const nextMonth = () => {
setToday(dayjs(today).add(1,"month"))
setInputValue('')
}
const presentMonth = () => {
setToday(dayjs())
setInputValue('')
}
const [inputValue, setInputValue] = useState('')
const onChangeInputHandle = (e) => {
setInputValue(e.target.value)
}
useEffect(()=> {
if(inputValue) {
setToday(dayjs(inputValue))
}
}, [inputValue])
// 해당 함수가 선언되는 버튼과 input
<button onClick={preMonth}>이전달</button>
<button onClick={nextMonth}>다음달</button>
<button onClick={presentMonth}>현재</button>
<input type='date' value={inputValue} onChange={onChangeInputHandle}/>
이제는 dates에 담겨있는 값을 통해서 화면엔 그려주기만 하면 된다. 이때 주의할 점은 dayjs() 자체로는 다양한 값을 가지고 있기 때문에 format('YYYY-MM-DD') 메서드로 값을 제어해야 한다는 점이다.
달력을 만들기 위해 가장 첫번째 할 일은 선언된 달의 총일수 며칠인지 추출하는 것이다. 그러나 이 역시도 dayjs()를 통하면 쉽게 추출할 수 있다. 바로 daysInMonth(); 메서드를 활용하는 것이다.
2023년 04월 기준, 해당달의 총일수는 30일이다.
공식문서의 설명에 따르면 startOf('month')는 해당 달의 첫번째 날에 대한 정보를 전달한다.
startOf('week')는 해당 주차의 첫번째 날에 대한 정보를 전달하는데 일요일에 대한 정보가 전달된다.
startOf('year')는 해당 연도의 첫번째 날에 대한 정보를 전달받는다.
정리하면, 해당달에 대한 정보를 전달받기 위해서는 해당 달의 첫날의 정보에 해당되는 dayjs().startOf('month').locale('ko')로 부터 시작해서 해당 달의 정보만큼을 반복하는 값을 배열에 담을 수 있다면, 달력을 그리기 위한 준비는 마무리되는 것이다. 이를 위해서 가장 단순한 방법인 for문을 사용했지만, 이를 조금 더 직관적으로 보여주기 위해서 Array.from() 구문으로 변경해 보았다.
유사배열객체(EDWIN베로그)를 생성하는 방법이다. ES6(2015년)에서 도입된 Array.from 메서드는 유사 배열 객체, 이터러블(ilteravle object) 객체를 인수로 전달받아 배열로 반환한다.daysInMonth()를 통해서 먼저 희소배열을 생성하고, 각 배열을 각각의 날의 정보로 채우는 방식이다.
const dates = Array.from({length:daysInMonth}, (_, index) =>
dayjs(firstDayOfMonth).add(index, 'day'));
여기서 주의할 점이 있다. 첫날의 정보를 얻은 것에는 다 이유가 있다. 바로 첫번째 날이 몇 요일에 시작하는지에 대한 값을 찾기 위함이었다. 금달의 경우에는 6번째, 즉 토요일이다. 그렇다면 무엇인가 표를 그릴 때 선행되는 6개의 값이 있어야 한 주의 끝인 토요일부터 해당 달의 정보를 반복하여 화면에 그릴 수 있다는 것이다.
즉 금달의 총요일이 30일인데, 6번째(토요일)부터 시작이라면, 앞에 선행되는 6개의 값을 더하여, 36개의 배열이 있어야 달력을 그리기 위한 공식이 만들어진다는 것이다. 이때 0번째날인 일요일이 그 값으로 0을 가지기에, 6번째 날인 토요일은 순서상으로는 7번째 위치에 있기에, 선행되는 6개의 값을 더한 것이다. 만약 다음달인 2023년 5월의 경우에는 월요일(1번째)부터 해당 달이 시작되는데, 이때는 하나의 값만 선행되는 곳에 더해주면 된다. 이를 위해서 역시 null 으로 채워진 희소배열을 만들 필요가 있었고, 다음과 같이 작성했다.
const emptyDates = new Array(firstDayOfMonth.day()).fill(null);
이후, 전개구문으로 [...emptyDates, ...dates] 를 하나의 구문으로 합쳐서 이를 변수 calendarLists에 담았다. 이제 기능로직은 끝이났다. 이제는 UI로직을 구현하면 된다.
캘린더를 화면에 그리는 방법은 다양하다. table 태그를 사용하는 방법부터 시작해서 말이다. 그러나 이는 수많은 tr 태그와 th, td 태그를 생성해야 하기에 좋은 방법은 아니다. 사실 구글링을 찾아보았지만, 어려운 코드 뿐이었다. 기록하는 사람 자체도 해당 코드를 이해하고 썼는지 의문이 들었다.
나는 이 부분에서는 GPT에게 요청했다. 리액트에서 캘린더를 만드는 방법 가운데 가장 선호되는 방법이 무엇인지 질문했다. 이에 대해서 GPT는 display:grid, grid-templates-columns: repeat(7, 1fr) 이라고 대답해 주었다. 아. grid... 질문하기 전까지만 해도 그리드에 대해서는 공부해서 알았지만, 이를 이렇게 사용할 줄은 몰랐다. 다시 말해.. grid...에 대한 새로운 관점이었다.
const App = () => {
const 요일 = ["일요일","월요일","화요일","수요일","목요일","금요일","토요일"]
return
<>
<h1>{today.format('YYYY년 MMMM')}</h1>
<button onClick={preMonth}>이전달</button>
<button onClick={nextMonth}>다음달</button>
<button onClick={presentMonth}>현재</button>
<input type='date' value={inputValue} onChange={onChangeInputHandle}/>
<CalenderLayout>
{요일.map((요일,index) => (<DateHeaed key={index} height="40">{요일}</DateHeaed>))}
{calendarLists.map((date, index) => {
if(date === null) {
return <DateDiv key={index}></DateDiv>
}
else {
return <DateDiv color='lightsalmon' key={index} onClick={()=>alert(date.format())}>{date.format("DD")}</DateDiv>
}
})}
</CalenderLayout>
}
</>
export default App;
const CalenderLayout = styled.div`
display: grid;
grid-template-columns: repeat(7, 1fr);
gap: 5px;
border: 1px solid black;
box-sizing: border-box;
padding: 10px;
max-width: 600px;
min-width: 600px;
max-height: 500px;
min-height: 500px;
`
const DateHeaed = styled.div`
background-color: lightsalmon;
text-align: center;
color: white;
font-weight: 900;
height: ${prpos=> prpos.height}px;
line-height:${prpos=> prpos.height}px;
min-height: 30px;
height: 50px;
`
const DateDiv = styled.div`
text-align: center;
display: flex;
justify-content: center;
align-items: center;
height: 50px;
`
확인해 본 결과.. 400년 전 날짜도 일단 맥북에서 제공하고 있는 날짜와 동일한 것으로 볼 때, 날짜값을 불러와서 사용하는 것임으로 제한이 없는 것 같다. 이후 각각의 날짜에 onClick={()=>{}}을 부여하여 함수기능을 수행할 수 있는데, 예를 들어 선택한 날짜의 정보를 서버로 보내서 날짜에 해당하는 검색을 실행한다거나 하는 기능을 추가할 수 있는 것이다. 이상 여기까지가 리액트에서 캘린더를 작성하는 가장 쉬운 방법이었다.