본문 바로가기

Projects

Day.js를 이용한 커스터마이즈 달력 구현

내가 담당했던 스터디밍 마이페이지

회원 mypage에서는 팔로우 목록, 회원 탈퇴 및 비밀번호 수정 기능을 기본적으로 넣고, 공부 기록 조회나 통계를 한눈에 보여주는 기능을 구현하려 했다.

달력의 첫 틀은 노션 캘린더를 많이 참고했다. 달력의 각 날짜를 누르면 그날의 공부 시간과 기록해둔 코멘트가 나오는데, 노션과 달리 이벤트를 카드 형식으로 미리 보여주거나 드래그 앤 드롭으로 옮기는 기능은 없기 때문에 전체적으로 썰렁(?)했다.

그러다 생각해낸 것이 깃헙의 잔디심기였다. 개발자들이 빼곡한 잔디를 뿌듯해하듯이, 사용자들이 우리 서비스를 이용해 열심히 공부 기록을 남기고 마이페이지에서 뿌듯함과 재미를 느끼는 요소가 될 수 있을 것 같았다.

 

필요한 API(한 달 단위로 매일의 공부 시간을 받아오는)를 백엔드 팀원에게 요청하고 좋은 레퍼런스를 참고하여 만들어나가기 시작했다.

 

A. 사전 준비

1. import

처음엔 moment로 작성하다가 용량이나 속도 이슈를 접하고 dayjs로 변경해 이용했다.

import dayjs from "dayjs";
const weekOfYear = require("dayjs/plugin/weekOfYear");
dayjs.extend(weekOfYear);

2. state & constant

이전 달 / 다음 달로 선택한 월의 기준이 되는 standard는 상태로, 변하지 않는 값인 today는 상수로 선언했다.

  const [standard, setStandard] = useState(() => dayjs()); 
  const [grape, setGrape] = useState([]);
  const today = dayjs();

3. API request

데이터 받아오기

한 달의 각 날짜 별 공부 시간(분단위)을 배열로 받아 구간마다 정해진 숫자로 변환했다.

스타일링에 styled-component를 이용해서, props로 전달된 숫자(농도)를 global style로 지정한 컬러명(color-main-xx)에 대입했다.

 const getReport = (moment) => {
    const year = parseInt(moment.format("YYYY"));
    const month = parseInt(moment.format("MM"));
    const offset = new window.Date().getTimezoneOffset();
    statisticsAPI
      .getMonthlyReport(year, month, offset)
      .then((res) => {
        const report = res.data.report.map((time) => {
          if (time <= 0) return 0;
          else if (0 < time && time <= 120) return 25;
          else if (120 < time && time <= 240) return 50;
          else if (240 < time && time <= 360) return 60;
          else if (360 < time) return 75;
          else return 0;
        });
        setGrape(report);
      })
      .catch(() => {
        dispatch(loginStateChange(false));
        navigate("/home");
        dispatch(signinModalOpen(true));
      });
  };
const Date = styled.div`
  background-color: ${(props) => {
    if (props.isThisMonth) return "var(--color-gray-bg)";
    else {
      return `var(--color-main-${props.grape})`;
    }
  }};
`

 

B. 달력 구현하기

1. sub functions

(1) handleDayClick

날짜를 클릭해 모달이 렌더링 될 때 해당 날짜의 코멘트와 공부 기록을 불러와야 하므로, dailyLog 상태 관리 redux action은 boolean값과 클릭된 날짜를 인자로 받는다.

(2) returnToday

'오늘'을 눌러 오늘이 포함된 달로 이동하는 버튼을 클릭하면 standard를 다시 오늘로 설정해줌으로써 재 렌더링 시킨다.

이때 state 업데이트가 비동기일 수 있으므로 함수 반환 값으로 상태 값을 갱신한다.

(3) jumpToMonth

월 이동시 날짜 갱신보다 styled-component 색상 반영이 느려, 이전 달 색상이 잠깐 남아있다 깜빡이며 바뀌는 버그가 있었다.

이 부분을 해결하기 위해 standard 상태 변경 이전에 먼저 색상 상태를 모두 0으로 초기화하는 작업을 추가했다.

day.js는 immutable 하기 때문에 기존 객체에 add 하거나 substract 할 수 없어 clone 하여 사용한다.

  const handleDayClick = (moment, isFuture) => {
    if (!isFuture) dispatch(dailyLogOpen(true, moment));
  };

  const returnToday = () => {
    setStandard(() => dayjs());
  };

  const jumpToMonth = (num) => {
    setGrape(Array(31).fill(0));
    if (num > 0) {
      setStandard(standard.clone().add(1, "month"));
    } else {
      setStandard(standard.clone().subtract(1, "month"));
    }
  };

 

2. main function

(1) 월 생성 기준인 standard으로 한 달의 달력을 완성하는 로직

1. 함수 인자로 standard를 받고, 기준값으로 그날이 속한 달의 첫째 주(startWeek)와 마지막 주(endWeek)를 찾는다.
2. 이때 12월 마지막 주인 53번째 주는 다음 해 첫 번째 주로 반환되므로 삼항 연산자로 확인 후 53으로 바꿔준다.
3. startweek부터 endweek까지 순회하며 길이가 7인 배열에 해당 주를 채워 넣는다.
    그 주의 첫째 날을 찾아서(startOf('week')), 인덱스 값만큼 더해주는 것(add(n + idx, 'day'))!
    따라서 [[첫째 주 일 - 월], [둘째 주 일 - 월], [셋째 주 일 - 월], [넷째 주 일 - 월]]이 완성된다. 

(2) UI 분류

1. 오늘인가? : 빨간 원으로 표시한다.
2. 이번 달인가? : 이번 달이 아니지만 현재 페이지에 포함된 이전 달, 다음 달의 날짜는 채도를 낮추고 클릭되지 않도록 한다.
3. 오늘 이후인가? : 아직 오지 않은 날은 기본 색으로 채우고 클릭되지 않도록 한다.(모달을 띄우지 않는다)
 const generate = (date) => {
    const startWeek = date.clone().startOf("month").week();
    const endWeek =
      date.clone().endOf("month").week() === 1
        ? 53
        : date.clone().endOf("month").week();
    const calendar = [];

    for (let week = startWeek; week <= endWeek; week += 1) {
      calendar.push(
        <Week key={week}>
          {Array(7)
            .fill(null)
            .map((n, idx) => {
              const current = date
                .clone()
                .week(week)
                .startOf("week")
                .add(n + idx, "day");

              const isThisMonth = current.format("MM") === date.format("MM");
              const isToday =
                today.format("YYYYMMDD") === current.format("YYYYMMDD")
                  ? "today"
                  : "";
              const isFuture =
                current.format("YYYYMMDD") > today.format("YYYYMMDD")
                  ? "future"
                  : "";
              const targetDate = parseInt(current.format("D"));

              return (
                <Date
                  key={idx}
                  isThisMonth={!isThisMonth}
                  isFuture={isFuture}
                  grape={grape[targetDate - 1]}
                  onClick={() => handleDayClick(current, isFuture)}
                >
                  <div className={isToday}>
                    <span className={`date-text ${isFuture}`}>
                      {targetDate}
                    </span>
                  </div>
                </Date>
              );
            })}
        </Week>
      );
    }
    return calendar;
  };

 

3. JSX

월 이동은 화살 아이콘으로 나타내고, 요일은 배열로 만들어 map으로 span 요소로 만들어줬다.

  useEffect(() => {
    getReport(standard);
  }, [standard]);

  return (
    <Container>
      <Head>
        <span id="year-month">
          {standard.format("YYYY")}년 {standard.format("MM")}월
        </span>
        <div className="util-button">
          <button className="jump-to-button" onClick={() => jumpToMonth(-1)}>
            <VscChevronLeft />
          </button>
          <button id="return-today" onClick={returnToday}>
            오늘
          </button>
          <button className="jump-to-button" onClick={() => jumpToMonth(1)}>
            <VscChevronRight />
          </button>
        </div>
      </Head>
      <Body>
        <div className="day-box">
          {["일", "월", "화", "수", "목", "금", "토"].map((day, idx) => (
            <span className="day-text" key={idx}>
              {day}
            </span>
          ))}
        </div>
        {generate(standard)}
      </Body>
    </Container>
  );

 

 

C. 개선

코멘트를 미리보기 형식으로 달력에 나타내서 조금 더 직관적이고 휑하지 않은 온라인 캘린더의 구색을 갖출 예정이다.

 

 

링크

Github 레포지토리 github.com/codestates/studeaming

완성 배포 서비스 studeaming.com

'Projects' 카테고리의 다른 글

redux-persist 초기값 변경 및 버전 관리하기  (0) 2022.01.05