라이브러리 없이 캘린더 만들기

2023년 5월 15일

캘린더 구현 방법

개인 프로젝트를 하면서, 캘린더 기능이 필요했고, 전에도 그래프와 차트를 SVG태그로 구현한 것처럼 캘린더 기능도 직접 구현해 보기로 했다.

지금까지 혼자 공부를 하거나, 팀플을 할 때 캘린더는 라이브러리를 사용했던거 같다. 많이 써본적은 없어 정확히 기억은 나지 않지만, 항상 React로 개발을 했으니, react-calendar, react-datepicker 등의 라이브러리를 사용했던거 같다.

날짜 관련되서 사용했던 라이브러리는 day.js를 사용했던거 같고, 이제 캘린더를 어떻게 만들어야 할까.. 고민을 시작했다.

처음에는 365일, 1 ~ 12월, 1일 ~ 31일, 월~일 등을 어떻게 월별로 날짜를 나누고, 매월 1일은 월요일이 아닌데.. 4년마다 윤년은 어떻게 하지.. 감이 잡히지 않았다.

그래서 우선 JavaScript 내장 객체인 Date method로 이용하면 되지 않을까? 라는 생각에 MDN에 들어가 보았고, 다양한 인스턴스 메서드들이 있었다. 이제 이것들을 활용하면 캘린더를 구현할 수 있을거 같았고, 그렇게 캘린더 구현이 시작되었다.

Date Method

Date()

Date method는 다양한 인스턴트 객체가 있고, 그 중에서 4가지 정도만 알고 있어도 충분히 캘린더를 구현 가능하다.

먼저 JavaScript 내장 객체인 Date method의 기본인 Date() 이다. Date 객체를 생성하고, 사용하는 방법을 알아보자

console.log(Date()) // Wed May 10 2023 16:36:44 GMT+0900 (한국 표준시)

const day = new Date()
console.log(day) // Wed May 10 2023 16:36:44 GMT+0900 (한국 표준시)

const newDay = new Date(1995, 0, 24, 3, 24, 12)
// Tue Jan 24 1995 03:24:12 GMT+0900 (한국 표준시)

첫 번째 Date()console.log에 찍어보면, 현재 시간을 알려준다.

두 번째 new 키워드를 사용해 객체를 생성해서 사용한다. 역시 console.log에 찍어보니, 요일, 월, 일, 년, 시간 순으로 알려준다. 필자는 stringsplit를 활용해서 날짜의 데이터를 다루기도 했다.

마지막으로 new Date()안에 인자를 넣을때 년, 월, 일, 시, 분, 초 순으로 넣으면 원하는 날짜의 데이터가 형성된다. 주의할점은 월은 제로베이스로 0~11에 맞춰서 입력하면 된다.

getDate()

const newDay = new Date(1995, 0, 24, 3, 24, 12)
// Tue Jan 24 1995 03:24:12 GMT+0900 (한국 표준시)

console.log(newDay.getDate()) // 24

getDate() 메소드는 현지 시간 기준의 일1 ~ 31을 반환하는 메소드이고, 1부터 시작합니다.

getDay()

const newDay = new Date(1995, 0, 24, 3, 24, 12)
// Tue Jan 24 1995 03:24:12 GMT+0900 (한국 표준시)

console.log(newDay.getDay()) // 2

getDay() 메소드는 현지 시간 기준의 요일0~6을 반환하는 메소드이고 0부터 일월화수목금토 순으로 입니다.

getMonth()

const newDay = new Date(1995, 0, 24, 3, 24, 12)
// Tue Jan 24 1995 03:24:12 GMT+0900 (한국 표준시)

console.log(newDay.getMonth()) // 0

getMonth() 메소드는 현지 시간 기준 월0~11을 반환하고 0부터 1월~12월 순입니다.

getFullYear()

const newDay = new Date(1995, 0, 24, 3, 24, 12)
// Tue Jan 24 1995 03:24:12 GMT+0900 (한국 표준시)

console.log(newDay.getFullYear()) // 1995

getFullYear() 메소드는 현지 시간 기준으로 연도를 4자리로 반환합니다.

지금 까지 메소들을 활용해서 캘린더를 구현해 봅시다!

캘린더 만들기

이번달 첫 시작 요일

이제 캘린더를 만들려면 위에서 배웠던 메소드들을 활용하면 되는데, 첫 번째로 이번달 첫 시작 요일을 얻는 방법입니다. 이것을 구하는 이유는 매월 1일은 월요일이 아니기 때문에 캘린더에서 보면, 항상 1~31일 까지 적혀 있고, 해당월의 일수에 맞게 채워지고 빈날은 전달과 다음달의 날짜를 가져다 쓰고 있기 때문입니다.

const firstDayOfMonth = new Date(2023, 4, 1).getDay() // 1

예제 보면 2023년 5월 1일의 요일은 월요일인 것을 알 수 있네요. 만약 예제 날짜를 가지고 달력을 만들때, 첫날은 월요일이니까 보통 달력이 일요일 부터 시작하니까, 1일 앞에 전달의 마지막 날이 들어가고 그 다음에 이번달 1일이 들어가겠네요. 이런식으로 요일, 날짜 별 숫자를 통해 배열에 각각 순서대로 값을 넣어서 구현하겠습니다.

이번달 마지막 날짜

const lastDateOfMonth = new Date(2023, 4 + 1, 0).getDate() // 31

예제를 보면 입력 자리에 +1이 되어 있고, 입력자리에 0값이 입력되어 있습니다. 입력자리에 0을 입력하면 전달의 마지막 날짜를 반환하기 때문에, 자리에 +1을 하면 6월의 전달인 5월의 마지막 날짜를 반환하기 위한 것입니다. 이제 배열에 마지막 날짜까지 순환해서 넣어주면 되겠네요.

이번달 마지막 요일

const lastDayOfMonth = new Date(2023, 4, lastDateOfMonth).getDay() // 3

예제에 을 넣는 자리에 앞서 구했던 이번달 마지막 날짜를 인자를 넣어주면, 이번달의 마지막 요일도 알 수 있습니다. 이번달 마지막 요일을 알아야하는 이유는 날짜가 비는 요일 만큼 앞달을 채우기 위함입니다.

지난달 마지막 날짜

const lastDateOfLastMonth = new Date(2023, 4, 0).getDate() // 30

앞서 이번달 마지막 날짜를 구하는 방식과 똑같이 구하면 됩니다. 지난달 마지막 날짜를 구하는 이유는 이번달의 1일이 화요일 이라면 일, 월 날짜가 비어있게 되고, 위에 코드 값을 예로 들어, 월요일에 30이 들어가고, 일요일에 29가 들어가면서 자연스럽게 비어있는 달력에 전달에 날짜가 채워지게 됩니다.

배열로 캘린더 만들기

alt

아주 기본적인 기능만 있는 캘린더를 구현했습니다. 아래 전체 코드를 보면

import { useState } from "react"
import "./styles.css"

export default function App() {
  const [month, setMonth] = useState(new Date().getMonth())
  const [year, setYear] = useState(new Date().getFullYear())

  const calendar = () => {
    const currentCalendar = []
    const firstDayOfMonth = new Date(year, month, 1).getDay() // 이번달 첫 시작 요일
    const lastDateOfMonth = new Date(year, month + 1, 0).getDate() // 이번달 마지막 날짜
    const lastDayOfMonth = new Date(year, month, lastDateOfMonth).getDay() // 이번달 마지막 요일
    const lastDateOfLastMonth = new Date(year, month, 0).getDate() // 지난달 마지막 날짜

    for (let i = firstDayOfMonth; i > 0; i--) {
      currentCalendar.push(lastDateOfLastMonth - i + 1)
    }

    for (let i = 1; i <= lastDateOfMonth; i++) {
      currentCalendar.push(i)
    }

    for (let i = lastDayOfMonth; i < 6; i++) {
      currentCalendar.push(i - lastDayOfMonth + 1)
    }

    const weeks = []
    while (currentCalendar.length) {
      weeks.push(currentCalendar.splice(0, 7))
    }

    return weeks
  }

  const handlePrevMonth = () => {
    if (month === 0) {
      setMonth(11)
      setYear(year - 1)
    } else {
      setMonth(month - 1)
    }
  }

  const handleNextMonth = () => {
    if (month === 11) {
      setMonth(0)
      setYear(year + 1)
    } else {
      setMonth(month + 1)
    }
  }

  console.log(calendar())
  return (
    <div className='App'>
      <div style={{ display: "flex" }}>
        <button onClick={handlePrevMonth}>{`<`}</button>
        <div>{`${year}${month}`}</div>
        <button onClick={handleNextMonth}>{`>`}</button>
      </div>
      <tbody>
        {calendar().map((week, index) => (
          <tr key={index}>
            {week.map((day) => (
              <td className='day'>{day}</td>
            ))}
            {week.length < 7 && Array(7 - week.length).fill(<td></td>)}
          </tr>
        ))}
      </tbody>
    </div>
  )
}

대략 설명하자면 useState를 활용해서 현재 년도를 기본값으로 설정하고, calendar함수에서 배열을 통해 캘린더를 생성하고, handle 함수를 통해 button 클릭시 전달, 다음달로 이동이 가능합니다. 마지막으로 table 태그를 통해서 배열로 렌더링해주고 있습니다.

const calendar = () => {
  const currentCalendar = []
  const firstDayOfMonth = new Date(year, month, 1).getDay() // 이번달 첫 시작 요일
  const lastDateOfMonth = new Date(year, month + 1, 0).getDate() // 이번달 마지막 날짜
  const lastDayOfMonth = new Date(year, month, lastDateOfMonth).getDay() // 이번달 마지막 요일
  const lastDateOfLastMonth = new Date(year, month, 0).getDate() // 지난달 마지막 날짜

  for (let i = firstDayOfMonth; i > 0; i--) {
    currentCalendar.push(lastDateOfLastMonth - i + 1)
  }

  for (let i = 1; i <= lastDateOfMonth; i++) {
    currentCalendar.push(i)
  }

  for (let i = lastDayOfMonth; i < 6; i++) {
    currentCalendar.push(i - lastDayOfMonth + 1)
  }

  const weeks = []
  while (currentCalendar.length) {
    weeks.push(currentCalendar.splice(0, 7))
  }

  return weeks
}

이부분이 가장 중요하다고 생각하는데, 지금 까지 알아보았던, Date method를 활용해서 캘린더의 날짜들을 배열안에 push해주고 있는데요.

첫 번째 for 문을 보면, firstDayOfMonth 변수를 통해 5월 1일이 월요일이고, 보통 달력은 일요일 부터 시작하니까 월요일이 시작이면 일요일 날짜가 비워지니 그만큼 지난달의 마지막 날짜push를 해줘서 달력 첫줄을 채워주고 있고

두 번째 for 문은 이번달 마지막 날짜까지 달력을 채워주고 있습니다.

세 번째 for 문은 현재 달의 를 만들어줍니다. 6보다 작다는 뜻은 5주로 배열을 만든다는 의미입니다.

마지막 while 문splice를 통해 달력을 7일 기준으로 배열을 나눠주면서 weeks를 반환합니다.

이렇게 되면 전달과 현재달, 다음달의 날짜 까지 모두 볼 수 있는 캘린더를 구현할 수 있습니다.

지금 위에 gif를 보면 캘린더의 크기가 일정하지 않은데 이것은 전달과 다음달의 일자 수가 더 많기 때문에 몇몇 캘린더는 그렇게 보이는 것이고, 이것을 전체 배열 length를 잡아주면 크기는 변하지 않고, 안에 캘린더 값들만 변하게 할 수 있겠네요.

회고

라이브러리를 쓰지 않고 캘린더를 구현하면서 어려움을 많이 느꼈던거 같다. 위에 예제들은 기본만 사용했지만, 개인 프로젝트에서 구현했던 캘린더는 날짜 뿐만 아니라, 그안에 운동 루틴 데이터 들도 들어있고, 달력이 클릭도 되고, 색도 빨강색, 파란색 스타일도 입히고, 좀 더 구현할게 많았었다.

지금 생각해보면, 라이브러리는 어떤식으로 구현했나 코드를 보았다면 좀 더 빠르게 이해하고 구현할 수 있었을 것 같은데.. 그래도 구현하였고, 공부가 많이 되었다. 라이브러리를 만든 사람들도 대단해 보이고, 앞으로도 종종 혼자 공부할겸 프로젝트를 할 때 라이브러리 없이 구현하는 연습을 더 해봐야 겠다.

참고 자료