급식의 민족 - [1]한 달 간격 캘린더 만들기

박병관·2022년 5월 1일
1
post-thumbnail

만든 과정이 있어서 내용이 쫌 있음
전체코드는 결과에 있음

결과 미리보기💁

군침이 싹도는 많이 본 적 있는 달력이다
어떻게 만들었는지 자랑 시작하겠다

이유/계획🤷

전에 글에 나왔듯 내가 맡은 페이지는 리뷰 작성 페이지이다

디자인은 위처럼 생겼고, 보이듯이 달력이 들어가있다
달력으로 날짜를 선택해서 리뷰를 하거나 / 날짜의 메뉴 / 날짜의 리뷰를 확인할 수 도록 한다

라이브러리를 선택하지 않은 이유

달력은 크게 특별한게 아니라서 라이브러리가 충분히 있을거지만, 말 그대로 크게 특별한게 아니라서, 훌륭한 사람이 되기 위해(+자유로운 스타일과 로직 적용을 위해) 그냥 구현하기로 했다

구현하며 신경써야 할 부분

메인 페이지이다, 화살표로 표시된 버튼을 사용하면 켈린더가 뜨고 선택할 수 있게된다

이처럼 캘린더의 재사용이 필요해서 딱딱 캘린더를 사용하면 원하는 기능을 수행시킬 수 있도록 하는 부분을 신경썻던 것 같다, 더 자세한 내용은 '과정'에서 확인하자

과정💦

코드를 정말 잘 짜는게 아니고, 설명을 정말 잘하는게 아니라 코드의 주석같은 느낌으로 설명을 이어간다
오직 "캘린더"코드에 대해 더 알고싶다면 다른 멋진 글들을 보자

기본 변수 선언과 기본 형태

대충 이부분

일단 파일은 calendar.tsx 로 설명하도록 하겠다
스타일은 이 글을 보는 이유와 같이 알아서 해보자

기본적인 형태를 먼저 살펴보면, 위의 신경써야 할 부분에 나왔듯 캘린더를 재사용 해야하는데, 선택된 날짜를 다 전역변수로 저장한다면 메인페이지에서 선택된 날짜가 그대로 리뷰페이지에서도 적용이 될 것이다. 더 나은 ux를 위해 리뷰페이지에서는 현재 날짜로 기본 세팅이 되도록 할것이다

또 메인에서는 날짜를 선택하면 그냥 그 날짜를 보여주면 돼서 state값을 사용해도 되지만, 리뷰에서는 선택된 날짜의 메뉴 / 리뷰 / 리뷰달기를 제공해야 하기 때문에 전역변수를 사용해야 한다

결론적으로 메인과 리뷰 페이지에서 서로 다른 값을 props로 넘겨서 캘린더를 사용해야한다

이를 위해 메인개발을 맡은 개발자ㅋㅋ와 말 해놓은 상태였다

// Calendar.tsx
import { useEffect, useState } from "react";

import * as Cal from "./Calendar.style";

import { useRecoilState } from "recoil";
import { recoilCalYear, recoilCalMonth } from "stores/calendar/calLocation";

export default function Calendar({
  // props로 들어오는 값들
  year,
  setYear,
  month,
  setMonth,
  date,
  setDate,
}: {
  year: number;
  setYear: any;
  month: number;
  setMonth: any;
  date: number;
  setDate: any;
}) {
    // 캘린더에 띄울 영어
  const monthes: string[] = [
    "January",
    "February",
    "March",
    "April",
    "May",
    "June",
    "July",
    "August",
    "September",
    "October",
    "November",
    "December",
  ];
  
  
  // 값 초기화 month 그대로 초기화
  useEffect(() => {
  // 오늘 날짜 가져오기
    const today = new Date();

    setYear(today.getFullYear());
    setMonth(today.getMonth());
    setDate(today.getDate());
  }, []);
  
}
// WriteReview.tsx

// 당연하지만 위의 props로 넣어줄 값들이 필요하다
// 나는 WriteReview.tsx라는 곳에서 컴포넌트를 사용하고 넘겨준다

// 위에서도 말이 나왔듯 나는 recoil값을 넣어줘야 한다
// recoil에 저장되는 값은 "오늘" 또는 "선택된 날" 이다
const [year, setYear] = useRecoilState(calendarYear);
const [month, setMonth] = useRecoilState(calendarMonth);
const [date, setDate] = useRecoilState(calendarDate);
.
.
.
<Calendar
  year={year}
  setYear={setYear}
  month={month}
  setMonth={setMonth}
  date={date}
  setDate={setDate}
></Calendar>

return (
    <Cal.CalendarContainer>
      <nav>
        <div>{calYear}</div>
        <div>{monthes[calMonth]}</div>
		// ▲ 날짜 출력    ▼ 켈린더 이동(밑에 내용 나옴)
        <div onClick={() => changeMonth(-1)}>
          <ArrowSvg />
        </div>
        <div onClick={() => changeMonth(1)}>
          <ArrowSvg className="rightArrow" />
        </div>
      </nav>

      <Cal.DayOfWeek>
        <div></div>
        <div></div>
        <div></div>
        <div></div>
        <div></div>
        <div></div>
        <div></div>
      </Cal.DayOfWeek>

    </Cal.CalendarContainer>
  );

props세팅과 기본적으로 필요한 변수들이다


캘린더 생성

대충 이 부분

중요한 내용을 설명하겠다!
내가 생각한 한 달 캘린더 생성에 필요한 것은 아래와 같다

  • 이번달 첫날의 Date객체
  • 이번달 마지막날의 Date객체
  • 지난달 마지막날의 Date객체

요기서 Date객체를 사용해보면 알겠지만, 아래와 같이 날짜 정보들을 얻을 수 있다

  • 날짜객체.getFullYear() : 연도를 가져온다
  • 날짜객체.getMonth() : 달을 가져온다(1월이 '0', 2월이 '1' ... 12월이 '11')
  • 날짜객체.getDate() : 일을 가져온다
  • 날짜객체.getDay() : 요일을 가져온다(일요일이 '0' ... 토요일이 '6')

이제 잘 생각해보면 아래처럼 한 달 캘린더에 전에 달의 남는 날짜와 다음 달의 넘는 날짜를 구할 수 있다

글씨가 이상하지만 정성을 봐서라도 한 번 읽어보자
이해가 되면 아래의 코드를 더 쉽게 이해할 수 있다

아무튼 이런 느낌으로 코드를 보면

// Calendar.tsx

// 달력에 띄우기 위한 변수,
// props로 들어오는 값과 독립적이다
const [calYear, setCalYear] = useRecoilState(recoilCalYear);
const [calMonth, setCalMonth] = useRecoilState(recoilCalMonth);
// calYear, calMonth에 대한 설명은 다음 과정에서 더 자세히 다룬다
.
.
.
// 값 초기화 month 그대로 초기화
  useEffect(() => {
    // 오늘 날짜 가져오기
    const today = new Date();
	.
    .
    .
    // cal날짜의 기본값을 오늘로 한다
    setCalYear(today.getFullYear());
    setCalMonth(today.getMonth());
  }, []);
.
.
.
  // 이번달 첫날의
  let currentMonthFirst: Date = new Date(calYear, calMonth, 1);
  // 이번달 마지막날
  let currentMonthLast: Date = new Date(calYear, calMonth + 1, 0);
  // 지난달 마지막날
  let previousMonthLast: Date = new Date(calYear, calMonth, 0);
  // 날짜 배열에 넣기
  let dayArray: number[] = [];
  // 달력 한 면의 색을 채워넣는 배열
  let colorArray: boolean[] = [];

  // 전의 달을 위한 for
  for (
    let i = previousMonthLast.getDate() - (currentMonthFirst.getDay() - 1);
    i <= previousMonthLast.getDate();
    i++
  ) {
    dayArray.push(i);
    colorArray.push(false);
  }
  // 이번달을 위한 for
  for (
    let i = currentMonthFirst.getDate();
    i <= currentMonthLast.getDate();
    i++
  ) {
    dayArray.push(i);
    colorArray.push(true);
  }
  // 다음 달을 위한 for
  for (let i = 1; i < 7 - currentMonthLast.getDay(); i++) {
    dayArray.push(i);
    colorArray.push(false);
  }
.
.
.
	<Cal.Calendar>
        {dayArray?.map((day, idx) => (
          <Cal.Day
            key={idx}
            visable={colorArray[idx]}
            // ▲ 날짜 띄우기    ▼ 날짜 선택(밑에 내용 나옴)
            selected={isSameDate([
              calYear,
              calMonth,
              idx - currentMonthFirst.getDay() + 1,
              year,
              month,
              date,
              tempMonth,
            ])}
            onClick={() => selectDay(idx)}
          >
            {day}
          </Cal.Day>
        ))}
      </Cal.Calendar>
.
.
.

보기 역할 수 있을거 안다,
dayArray가 날짜를 넣은 배열, colorArray 가 이번달과 다른 달을 비교하기 위한 색을 넣은 배열 이라는걸 이해하면 된다

노마드코더 함수형 프로그래밍 영상을 보고 '명령형 코드(for..) 보다 선언형 코드(map..) 을 사용해야 하구나!' 라는 걸 이 때 알았지만 이 for문 덩어리 부분에서 어떻게 개선해야할지 생각이 안 났다


캘린더 이동

대충 이 부분

사실 캘린더 이동은 그냥 위에나온 캘린더를 만드는 과정을 계속 하면 된다,
calYearcalMonth 를 바꾸면 된다

tmi로 그냥 아까 캘린더를 만들 때 전역변수로 선언된 year,month,date,day 를 사용하지 않고,
굳이 calYearcalMonth 를 사용한 이유를 설명하자면 다음과정에 나오는 날짜 선택과 관련있다
대충 어떤 문제가 있었냐면, 이전 달의 날짜를 선택하면 달력이 이전 달로 가는 것이다
물론 그런 달력도 매력이 있겠지만 ux를 위해 이전 달 선택해도 달력이 넘어가지 않도록,
전역변수오 별개로 캘린더에서만 쓰이는 year, month를 사용하기로 했다


// Calendar.tsx
  function changeMonth(changeMonth: number) {
    const tempDate = new Date(calYear, calMonth + changeMonth);
    console.log(tempDate);
    setCalYear(tempDate.getFullYear());
    setCalMonth(tempDate.getMonth());
  }
.
.
.
		<div onClick={() => changeMonth(-1)}>
          <ArrowSvg />
        </div>
        <div onClick={() => changeMonth(1)}>
          <ArrowSvg className="rightArrow" />
        </div>
.
.
.

그냥 calYear, calMonth만 변경


날짜 선택

대충 이 부분

사실 이 부분때문에 캘린더를 한 번 다시 만들었다, 자세히는 위에 나온 calYear calMonth 을 만들지 않고 전역 변수로 캘린더를 관리해서이다

아무튼 날짜선택은 idx를 받아서 계산을 한 후 전역변수 year month
date 를 변경해주면 된다

계산하는 방법을 자세히 알아보면, 날짜 생성과 비슷한 메커니즘을 사용해서(더쉬움)

  • 지난 달 : 이번 달 첫째날의 요일보다 idx가 작으면(idx는 0부터 시작)
  • 이번 달 : (이번 달 첫째날의 요일 + 이번 달 마지막날의 날짜)보다 idx가 작으면(지난 달은 위의 if문에서 걸러짐)
  • 다음 달 : 위의 것들이 아닐 때
.
.
.
  function selectDay(idx: number) {
      // 날짜 선택시 날짜를 바꾸고 그 날짜의 정보를 가져옴
      // 저번, 이번, 다음 달에 따라 조정
      //
      // selectDay함수에서는 year,month,date를 
      //calYear,calMonth를 사용하기 때문에 선택이 켈린더에 영향을 끼치지 않는다
  
      // 지난 달 선택
      if (idx < currentMonthFirst.getDay()) {
        if (calMonth == 0) {
          setYear(calYear - 1);
          setMonth(11);
        } else {
          setYear(calYear);
          setMonth(calMonth - 1);
        }
        setDate(
          previousMonthLast.getDate() - 
          currentMonthFirst.getDay() + idx + 1
        );
  
        // 이번 달 선택
      } else if (idx < currentMonthFirst.getDay() + 
                 currentMonthLast.getDate()) {
        setYear(calYear);
        setMonth(calMonth);
        setDate(idx - currentMonthFirst.getDay() + 1);
  
        // 다음 달 선택
      } else {
        if (calMonth == 1) {
          setYear(calYear + 1);
          setMonth(0);
        } else {
          setYear(calYear);
          setMonth(calMonth + 1);
        }
        setDate(
          idx - currentMonthFirst.getDay() 
          - currentMonthLast.getDate() + 1
        );
      }
    }
.
.
.
  // 객체를 생성해서 같은 날인지 판단
  function isSameDate([
    calYear,
    calMonth,
    calDate,
    year,
    month,
    date,
  ]: number[]) {
    const cal = new Date(calYear, calMonth, calDate);
    const select = new Date(year, month, date);

    if (
      cal.getFullYear() == select.getFullYear() &&
      cal.getMonth() == select.getMonth() &&
      cal.getDate() == select.getDate()
    ) {
      return true;
    } else {
      return false;
    }
  }
.
.
.
		   <Cal.Day
            key={idx}
            visable={colorArray[idx]}
            // year,month,date모두 같을 때 selected
            // tempYear/Month는 지난 달/ 다음달 선택 시를 고려
            selected={isSameDate([
              calYear,
              calMonth,
              idx - currentMonthFirst.getDay() + 1,
              year,
              month,
              date,
            ])}
            onClick={() => selectDay(idx)}
          >
            {day}
          </Cal.Day>
.
.
.

클릭 됐을 때 selectDay함수를 실행

selected로 선택된 날짜의 css를 바꿔주는느낌인데,
이 날짜가 맞는지 확인하는 함수가 isSameDate이다

isSameDate는 파라미터로
calYear calMonth calDate : 캘린더에서의 날짜
year month date : 선택된 날짜

그냥 이 두개를 비교한다!

결과🏅

빠진 부분이 뭔가 있을것 같다, 위에서 과정 막 설명해도 전체 코드 보고 이해하는게 더 편할거다, 나도 블로그를 볼 때 그랬다

// 날짜를 캘린더에서

import { useEffect, useState } from "react";

import * as Cal from "./Calendar.style";

import ArrowSvg from "../../../assets/image/review/arrow.svg";

import { useRecoilState } from "recoil";
import { recoilCalYear, recoilCalMonth } 
from "stores/calendar/calLocation";

export default function Calendar({
  year,
  setYear,
  month,
  setMonth,
  date,
  setDate,
}: {
  year: number;
  setYear: any;
  month: number;
  setMonth: any;
  date: number;
  setDate: any;
}) {
  // 캘린더에 띄울 영어
  const monthes: string[] = [
    "January",
    "February",
    "March",
    "April",
    "May",
    "June",
    "July",
    "August",
    "September",
    "October",
    "November",
    "December",
  ];

  // 달력에 띄우기 위한 변수,
  // props로 들어오는 값과 독립적이다
  const [calYear, setCalYear] = useRecoilState(recoilCalYear);
  const [calMonth, setCalMonth] = useRecoilState(recoilCalMonth);

  // 값 초기화 month 그대로 초기화
  useEffect(() => {
    // 오늘 날짜 가져오기
    const today = new Date();

    setYear(today.getFullYear());
    setMonth(today.getMonth());
    setDate(today.getDate());

    setCalYear(today.getFullYear());
    setCalMonth(today.getMonth());
  }, []);



  useEffect(() => {
    console.log("year", year);
    console.log("month", month);
    console.log("date", date);
  }, [year, month, date]);

  // 이번달 첫날의
  let currentMonthFirst: Date = new Date(calYear, calMonth, 1);
  // 이번달 마지막날
  let currentMonthLast: Date = new Date(calYear, calMonth + 1, 0);
  // 지난달 마지막날
  let previousMonthLast: Date = new Date(calYear, calMonth, 0);
  // 날짜 배열에 넣기
  let dayArray: number[] = [];
  // 달력 한 면의 색을 채워넣는 배열
  let colorArray: boolean[] = [];

  for (
    let i = previousMonthLast.getDate() - 
    (currentMonthFirst.getDay() - 1);
    i <= previousMonthLast.getDate();
    i++
  ) {
    dayArray.push(i);
    colorArray.push(false);
  }
  for (
    let i = currentMonthFirst.getDate();
    i <= currentMonthLast.getDate();
    i++
  ) {
    dayArray.push(i);
    colorArray.push(true);
  }
  for (let i = 1; i < 7 - currentMonthLast.getDay(); i++) {
    dayArray.push(i);
    colorArray.push(false);
  }

  function changeMonth(changeMonth: number) {
    const tempDate = new Date(calYear, calMonth + changeMonth);
    console.log(tempDate);
    setCalYear(tempDate.getFullYear());
    setCalMonth(tempDate.getMonth());
  }
  
    // -- 아래는 날짜 선택
  
    // 선택된 날짜를 비교할 때 더해질 달
    const [tempMonth, setTempMonth] = useState(0);
  
    function selectDay(idx: number) {
      // 날짜 선택시 날짜를 바꾸고 그 날짜의 정보를 가져옴
      // 저번, 이번, 다음 달에 따라 조정
      //
      // selectDay함수에서는 year,month,date를 
      //calYear,calMonth를 사용하기 때문에 선택이 켈린더에 영향을 끼치지 않는다
  
      // 지난 달 선택
      if (idx < currentMonthFirst.getDay()) {
        if (calMonth == 0) {
          setYear(calYear - 1);
          setMonth(11);
        } else {
          setYear(calYear);
          setMonth(calMonth - 1);
        }
        setDate(
          previousMonthLast.getDate() -
          currentMonthFirst.getDay() + idx + 1
        );
  
        // 이번 달 선택
      } else if (idx < currentMonthFirst.getDay() 
                 + currentMonthLast.getDate()) {
        setYear(calYear);
        setMonth(calMonth);
        setDate(idx - currentMonthFirst.getDay() + 1);
  
        // 다음 달 선택
      } else {
        if (calMonth == 1) {
          setYear(calYear + 1);
          setMonth(0);
        } else {
          setYear(calYear);
          setMonth(calMonth + 1);
        }
        setDate(
          idx - currentMonthFirst.getDay() 
          - currentMonthLast.getDate() + 1
        );
      }
    }
  
  // 객체를 생성해서 같은 날인지 판단
  function isSameDate([
    calYear,
    calMonth,
    calDate,
    year,
    month,
    date,
    // tempMonth,
  ]: number[]) {
    const cal = new Date(calYear, calMonth , calDate);
    // const cal = new Date(calYear, calMonth - tempMonth, calDate);
    const select = new Date(year, month, date);

    if (
      cal.getFullYear() == select.getFullYear() &&
      cal.getMonth() == select.getMonth() &&
      cal.getDate() == select.getDate()
    ) {
      return true;
    } else {
      return false;
    }
  }

  return (
    <Cal.CalendarContainer>
      <nav>
        <div>{calYear}</div>
        <div>{monthes[calMonth]}</div>
        <div onClick={() => changeMonth(-1)}>
          <ArrowSvg />
        </div>
        <div onClick={() => changeMonth(1)}>
          <ArrowSvg className="rightArrow" />
        </div>
      </nav>

      <Cal.DayOfWeek>
        <div></div>
        <div></div>
        <div></div>
        <div></div>
        <div></div>
        <div></div>
        <div></div>
      </Cal.DayOfWeek>

      <Cal.Calendar>
        {dayArray?.map((day, idx) => (
          <Cal.Day
            key={idx}
            visable={colorArray[idx]}
            // year,month,date모두 같을 때 selected
            // tempYear/Month는 지난 달/ 다음달 선택 시를 고려
            selected={isSameDate([
              calYear,
              calMonth,
              idx - currentMonthFirst.getDay() + 1,
              year,
              month,
              date,
            ])}
            onClick={() => selectDay(idx)}
          >
            {day}
          </Cal.Day>
        ))}
      </Cal.Calendar>
    </Cal.CalendarContainer>
  );
}

앞으로 리펙터링이 더 필요할 것이고, 선택 등을 할 때 넘어가는 달, 년을 예외처리하기 위해 다 Date 객체를 그냥 계속쓴거같다,
잘 모르지만 객체를 계속 생성하는게 성능에 그렇게 좋지 못할거같은 느낌이 들었다,

객체를 사용해서 이와같이 이와같이 선택이 된다

그래도 정말 만들고 애들한테 자랑을 엄청 했는데, 요기서 적는것도 어떻게보면 자랑이기 때문에 조금 부끄럽다ㅋㅋ, 그만큼 뿌듯했다

느낀점🙃

  • 내용이 많고, 이 글의 목적이 자랑 인수인계가 된다면 그때를 위해 내가 나중에 볼때를 위해(이해/이런거 했구나/이렇게 한 이유가 이거구나) 를 위해서라 독자를 생각하지 않고 글을 써서 전체적인 어투가 조금씩 달랐던 것 같다ㅋㅋ

  • 원하던 대로 안 됐을때 적당히 타협해서 끝낼 수도 있었지만, ux를 고려했기도 했고, CNS 동아리에서 하는 첫 프로젝트이고 오랬동안 생각해왔던 프로젝트라 더 정성을 들이게 된 것 같다

  • 이렇게 내가 구현을 해봤는데, 한 번 라이브러리를 사용해 구현해보고싶다, 구현한 부분을 생각하며 라이브러리를 사용하면 새로운 어떤 걸 깨닫을것만 같다ㅋㅋ

profile
괴물신인

0개의 댓글