프로젝트/하루한냥

[하루한냥] 캘린더 구현 : 달력의 시작 요일과 마지막날 구하기

알파카털파카 2023. 8. 1. 02:06
[하루한냥]
캘린더 구현 : 달력의 시작 요일과 마지막날 구하기

 

 

본격적으로 달력 컴포넌트를 구현하기에 앞서, 달력을 구현하려면 생각해야 할 중요한 포인트가 몇 가지 있다. 한 주의 시작을 어떤 요일로 할 것인지, 한 달이 무슨 요일부터 시작하는지, 매달 며칠까지 있는지 등을 고려해야 한다. 달력을 구현하려면 자바스크립트 Date 객체와 다양한 메소드를 이용하게 되기 때문에 해당 부분에 대한 이해가 필요하다. 이번 글에서는 날짜의 상태값 처리 내용을 담지 않았다. Zustand를 이용한 상태관리 구현은 다음 포스트에서 작성할 예정이다.

 

 


 

 

1. 한 주의 시작 요일 알아보기

맥북 기본 달력을 참고해 달력을 어떻게 만들면 좋을지 생각해 보았다. 한 주의 시작은 대체로 많은 달력이 그러하듯 일요일부터 시작하도록 정했다. 많이 통용되는 형식이 눈에 익기 때문에 사용자의 편의성을 해치지 않는다. 종교에 따라 나라별로 차이가 있지만, 국제표준화기구(ISO)와 한국에서는 월요일이 한 주의 시작이라고 보고 있다. 그렇지만 달력의 경우는 일요일부터 시작하는 것이 자연스럽기 때문에 일요일부터 시작하는 배열을 만들어 map을 이용해 화면에 렌더링 시켰다. 

 

맥북 기본 달력

 

헤더와 요일

 

const dayName = ['일', '월', '화', '수', '목', '금', '토'];

// 렌더링 부분
<WeekRow>
  <>
    {dayName.map((day) => (
      <span key={day}>{day}</span>
    ))}
  </>
</WeekRow>

 

 

 

 

2. 한 달의 시작 요일 구하기

매달 한 달의 시작 지점이 달라지기 때문에 이를 구해서 렌더링에 반영해주어야 한다. 날짜 부분을 <WeekRow> 컴포넌트로 감싸고 이를 한 줄에 7개의 아이템이 반복되는 Grid 형태로 CSS를 구성했다. 시작 지점 이전의 빈 칸은 <div/> 태그를 이용해 채워주었다. 매달 시작 지점을 구하기 위해 Date 객체를 이용해 해당 달의 시작 요일을 구하고, 그 수만큼 <div/>를 뿌려준다.

 

2023년 8월 달력

 

// 시작 날짜 구하기
const getTargetMonthFirstDay = () => {
  const targetDate = new Date();
  return new Date(targetDate.getFullYear(), targetDate.getMonth(), 1);
};

// 시작 요일 구하기
const firstDayOfMonth = getTargetMonthFirstDay().getDay();

// 렌더링 부분
<WeekRow>
  <>
     {
       Array(firstDayOfMonth)
         .fill(0)
         .map((_, index) => (
           <div key={index} />
       ))
     }
  </>
</WeekRow>

 

시작 날짜를 구하는 함수를 만들어 타겟 날짜의 연도와 달을 찾고, 날짜는 1일로 하는 Date 객체를 생성한다. getDay 메소드를 사용해 이 날짜의 요일을 구했다. 시작 요일만큼 달력에 빈 <div/>를 생성해주면 된다. 달력 날짜를 구현할 때도 range 함수가 이용되기 때문에 유틸 함수로 분리했다. 

 

// src/lib/utils/range.ts

const range = (n: number) =>
  Array(n)
    .fill(0)
    .map((_, index) => index + 1);
    
export default range;
{range(firstDayOfMonth).map((day) => (
  <div key={day} />
))}

 

 

 

 

3. 한 달의 마지막날 구하기 

한 달에 며칠까지 있는지 계산하려면, 다음 달의 첫 날을 찾고 거기서 하루 이전 날짜를 구하면 된다. 이런 방식으로 해당 월의 마지막 날짜를 찾을 수 있다. 예를 들자면 2023년 8월이 며칠까지 있는지 찾고 싶을 경우, 다음달인 9월 1일로 가서 하루를 빼주면 된다. 참고로 Date 객체에는 자동 고침 기능이 있기 때문에 날짜를 0일로 설정하면 자동으로 이전 달의 마지막 날짜가 된다. getMonth 메소드로 반환되는 값이 0부터 시작된다는 점도 염두해두자. 값을 받아 화면에 렌더링해주려면 +1을 해야한다.

 

// 마지막 날짜 구하기 
const getTargetMonthLastDay = () => {
  const nextMonth = new Date();
  nextMonth.setMonth(nextMonth.getMonth() + 1);
  const lastDateInTargetMonth = new Date(nextMonth.getFullYear(), nextMonth.getMonth(), 0);
  return lastDateInTargetMonth.getDate();
};

const daysInMonth = lastDateInTargetMonth();

// 렌더링 부분
<WeekRow>
  <>
    {range(firstDayOfMonth).map((day) => (
      <div key={day} />
    ))}
    {range(daysInMonth).map((date) => (
      <DateColumn key={date} date={date} type="available" />
    ))}
  </>
</WeekRow>

 

구한 마지막 날짜의 수만큼 날짜 컴포넌트(DateColumn)를 렌더링해준다. 지금까지 구현된 코드는 아래와 같다. 녹색 부분의 요일과 날짜 컴포넌트 부분만 완료가 되었다. 아직 상태값 처리가 되지 않았다. 다음 글에서는 헤더(빨간색 부분)에서 화살표를 클릭해 날짜를 변경하면, 변경 값에 따라 달력 컴포넌트가 변경되는 로직을 구현해볼 것이다. 

 

 

컴포넌트 구조

 

const dayName = ['일', '월', '화', '수', '목', '금', '토'];

export default function CalendarPage() {
  const getTargetMonthFirstDay = () => {
    const targetDate = new Date();
    return new Date(targetDate.getFullYear(), targetDate.getMonth(), 1);
  };

  const getCurrentMonthLastDay = () => {
    const nextMonth = new Date();
    nextMonth.setMonth(nextMonth.getMonth() + 1);
    const lastDateInCurrentMonth = new Date(nextMonth.getFullYear(), nextMonth.getMonth(), 0);
    return lastDateInCurrentMonth.getDate();
  };

  const firstDayOfMonth = getTargetMonthFirstDay().getDay();

  const daysInMonth = getCurrentMonthLastDay();

  return (
    <>
      <Header />
      <Container>
        <WeekRow>
          <>
            {dayName.map((day) => ( // 요일 이름 렌더링
              <span key={day}>{day}</span>
            ))}
          </>
        </WeekRow>
        <WeekRow>
          <>
            {Array(firstDayOfMonth) // 달력의 시작 지점 맞춰 렌더링하기
              .fill(0)
              .map((_, index) => (
                <div key={index} />
              ))}
            {range(daysInMonth).map((date) => ( // 달력 날짜 컴포넌트 렌더링
              <DateColumn key={date} date={date} type="available" />
            ))}
          </>
        </WeekRow>
      </Container>
      <Menu />
    </>
  );
}

 

 

 

마치며

Date 객체와 메소드를 자세하게 학습하고 다양하게 활용하다보니 뇌가 활성화되는 기분이다. 어려워보였던 로직이 점점 완성되어가니 재미있었다. 어려운 부분은 챗GPT의 도움을 받아가며 구현하고 있다. 프로젝트를 진행하면서 챗GPT를 효과적으로 활용하는 방법도 함께 배워가고 있다.