프로젝트 일지

[🐾 일지] 리액트 스크롤 달력 만들기

Kamea 2022. 6. 27. 14:29

 

 

프로젝트 TODO LIST 기능을 위해 만든 캘린더이다. 기능은 다음과 같다.

 

1. 오늘이 포함되어 있는 년도(2022)만의 1월부터 12월까지의 달력을 스크롤로 보여줌
2. 맨 위의 오늘 날짜를 클릭할 경우, 해당 월의 달력으로 이동함
3. 맨 처음 보여줄 때에는 오늘 날짜의 달력이 가운데에 보여짐(1월부터 X)

 

전체 구조

 

시작!

 

이전 포스트와 비슷한 부분이 많으니 먼저 보고 오는 것이 좋을 것 같다.

2022.06.01 - [프로젝트 일지] - [🐾 일지] 리액트 달력 만들기

 

[🐾 일지] 리액트 달력 만들기

프로젝트에서 그림 일기를 달력으로 보여주는 기능이 필요해서 달력을 제작하게 되었다. 라이브러리를 사용해도 되지만 달력의 셀 안에 이미지를 넣어야 하기 때문에 직접 그리기로 했다. 그럼

sennieworld.tistory.com

또 하나 더! 나는 TS로 작성했는데 JS코드가 필요하다면 자료 선언부분을 지우고 사용하면 된다.

 

 

 

 

 

1. 전체 뼈대

 

 

이후에 변경이 일어나는 값은 let, 그렇지 않은 값은 const 로 선언하였다. 

currentDate, selectedDate → 오늘 날짜의 정보

currentMonth → 오늘 날짜가 포함되어 있는 년도의 첫 번째 달의 정보 (2022년 1월)

months → 올해의 달들의 정보를 가지고 있는 배열 (1월 ~ 12월)

 

렌더링 값에는 schedule-calendar 아래에 [오늘 날짜, 요일들, 월 달력]이 column 배치로 온다. (→스타일링 코드)

 

{currentDate.toLocaleString("en-US", { month: "long" })}

 

위의 부분은 달 이름을 영어로 변환해 가져오는 코드이다. (6월 → June)

 

 

 

 

 

 

 

 

 

 

2. RenderDays

 

이전 포스트에 있었던 부분

 

 

 

 

 

 

 

3. {months} 에 들어갈 코드를 작성하고 렌더링

 

 

12번의 for문을 돌려 months 배열에 값을 추가해준다.

key는 react-uuid 라이브러리의 uuid 함수를 사용해 생성해주었다. 

반복문을 돌 때마다 currentMonth 값을 addMonths 내장 함수를 사용해 하나씩 더해준다.

 

 

 

RenderHeader은 달의 이름을 나타내 주는 헤더이다.

 

 

 

 

 

RenderCells 컴포넌트에 대한 자세한 설명은 이전 포스트에 나와있다. 그냥 넘어가도록 하겠다.

 

 

 

 

 

 

 

 

 

 

4. 화면을 띄었을 때(최초 렌더링 시), calendar-list의 calendar__item 중 오늘의 날짜가 속한 달이 맨 처음에 보이게 하기.

여기까지 구현을 하면 1월부터 달력이 나오게 된다. 오늘은 6월 27일이니 6월달 달력이 가장 위에 보이는 것이 더욱 접근하기 좋을 것이다.

이를 구현하기 위해 useRef를 사용한다.

 

🤗 useRef는 다양한 기능으로 사용이 된다.
지금처럼 특정 컴포넌트에 포커스를 줄 경우에도 사용된다. 보통 여러 개의 Input이 있을 경우 이름 input -> 비밀번호 input 이렇게 포커스를 이동해 줄 때 사용된다.

 

Calender 컴포넌트에 monthRef를 선언해준다.

 

const monthRef = useRef<HTMLDivElement>(null);  //타입스크립트
const monthRef = useRef(null); //자바스크립트

 

 

이전에 그려낸 months에 오늘이 속한 달이 있는 컴포넌트[7번째 줄 조건문]에 ref를 추가한다.

 

6~10 줄

 

 

 

화면을 띄었을 때(렌더링 시), 포커스를 넣어주는 함수를 작성한다.

 

 

 

 

맨 위 글자(오늘 날짜)를 눌렀을 때, 해당 월로 이동하는 함수를 작성한다.

 

1~5줄(추가), 10줄 onClick 추가

 

scrollIntoView에 behavior가 auto면 애니메이션 없이 바로 이동하는 것이고, smooth면 애니메이션과 함께 이동하는 것이다.

 

 

 

 

 

 

 

아름다운 전체 코드는 다음과 같이 된다.

 

 

 

역시나 시간이 소중한 사람들이 있으니, 긁을 수 있는 코드와 스타일링 코드도 함께 남겨놓겠다.

더보기

↓ 메인 코드

// Calender.tsx

import { useEffect, useRef } from "react";
import uuid from "react-uuid";
import { format, addMonths, startOfWeek, addDays } from "date-fns";
import { endOfWeek, isSameDay, isSameMonth } from "date-fns";
import { startOfMonth, endOfMonth } from "date-fns";
import "../../styles/components-schedule.style.scss";

const RenderHeader = ({ currentMonth }) => {
    return (
        <div className="header row">
            {currentMonth.toLocaleString("en-US", { month: "long" })}
        </div>
    );
};

const RenderDays = () => {
    const days: any[] = [];
    const date = ["Sun", "Mon", "Thu", "Wed", "Thrs", "Fri", "Sat"];
    for (let i = 0; i < 7; i++) {
        days.push(
            <div className="col" key={i}>
                {date[i]}
            </div>,
        );
    }
    return <div className="days row">{days}</div>;
};

const RenderCells = ({ currentMonth, selectedDate }) => {
    const monthStart = startOfMonth(currentMonth);
    const monthEnd = endOfMonth(monthStart);
    const startDate = startOfWeek(monthStart);
    const endDate = endOfWeek(monthEnd);

    const rows: any[] = [];
    let days: any[] = [];
    let day = startDate;
    let formattedDate = "";

    while (day <= endDate) {
        for (let i = 0; i < 7; i++) {
            formattedDate = format(day, "d");
            days.push(
                <div
                    className={`col cell ${
                        !isSameMonth(day, monthStart)
                            ? "disabled"
                            : isSameDay(day, selectedDate)
                            ? "selected"
                            : "not-valid"
                    }`}
                    key={uuid()}
                >
                    <span
                        className={
                            format(currentMonth, "M") !== format(day, "M")
                                ? "text not-valid"
                                : isSameMonth(day, monthStart) &&
                                  isSameDay(day, selectedDate)
                                ? "text today"
                                : ""
                        }
                    >
                        {formattedDate}
                    </span>
                </div>,
            );
            day = addDays(day, 1);
        }
        rows.push(
            <div className="row" key={uuid()}>
                {days}
            </div>,
        );
        days = [];
    }
    return <div className="body">{rows}</div>;
};

export const Calender = () => {
    const currentDate = new Date();
    const selectedDate = new Date();

    let currentMonth = new Date(format(currentDate, "yyyy"));
    let months: any[] = [];

    const monthRef = useRef<HTMLDivElement>(null);

    for (let i = 0; i < 12; i++) {
        months.push(
            <div
                className="calendar__item"
                key={uuid()}
                ref={
                    format(currentMonth, "MM") === format(selectedDate, "MM")
                        ? monthRef
                        : null
                }
            >
                <RenderHeader currentMonth={currentMonth} />
                <RenderCells
                    currentMonth={currentMonth}
                    selectedDate={selectedDate}
                />
            </div>,
        );
        currentMonth = addMonths(currentMonth, 1);
    }

    useEffect(() => {
        if (monthRef.current !== null) {
            monthRef.current.scrollIntoView({ behavior: "auto" });
        }
    }, []);

    function scrollCurrentMonth() {
        if (monthRef.current !== null) {
            monthRef.current.scrollIntoView({ behavior: "smooth" });
        }
    }

    return (
        <div className="schedule-calendar">
            <div className="text-today">
                <p className="text-current" onClick={scrollCurrentMonth}>
                    {currentDate.toLocaleString("en-US", { month: "long" })}
                    {format(currentDate, " dd")}
                </p>
                <p className="text-year">{format(currentDate, " yyyy")}</p>
            </div>
            <RenderDays />
            <div className="calendar-list">{months}</div>
        </div>
    );
};

 

 

↓ 스타일링 코드

// components-schedule.style.scss
@use "assets/styles/_theme.scss";
@use "assets/styles/_common.scss" as common;

.schedule-calendar {
    @include common.size(35%, 94%);
    @include common.flex-column(flex-start, center);
    font-family: theme.$english-font;
    .text-today {
        @include common.size(80%, fit-content);
        @include common.flex-row(space-between, baseline);
        color: theme.$main-color;
        margin-bottom: -1.2%;
        .text-year {
            @include common.font(
                rgb(99, 22, 22),
                theme.$english-font,
                0.8em,
                700
            );
        }
        .text-current:hover {
            @include common.hover-event();
            color: rgb(99, 22, 22);
            transform: scale(1.03);
        }
    }
    .days {
        @include common.size(85%, fit-content);
        @include common.flex-row(flex-start, center);
        font-size: 0.5em;
        margin-bottom: 2%;
        .col {
            @include common.size(13%, 100%);
            @include common.flex-column(flex-end, center);
            margin-right: 1%;
            background: transparentize(theme.$step-color, 0.4);
            border: 1px solid transparentize(theme.$step-color, 0.6);
            border-radius: 4px;
        }
    }
    .calendar-list {
        @include common.size(90%, 90%);
        @include common.flex();
        overflow-y: scroll;
        flex-wrap: wrap;
        .calendar__item {
            @include common.size(90%, 50%);
            padding: 2%;
            padding-top: 8%;
            margin-bottom: 5%;
            .header {
                @include common.size(100%, 10%);
                text-align: center;
                font-size: 1em;
                color: theme.$main-color;
            }
            .body {
                @include common.size(100%, 89%);
                @include common.flex-column();
                .row {
                    @include common.size(100%, 100%);
                    @include common.flex-row(space-between, center);
                    .col {
                        @include common.size(15%, 100%);
                        @include common.flex-column(flex-start, center);
                        font-size: 0.5em;
                        span {
                            z-index: 10000;
                            padding-top: 5px;
                        }
                        .not-valid {
                            color: theme.$gray-color;
                        }
                        .img {
                            @include common.size(100%, 100%);
                            margin: 0% 0 0 -17%;
                            border-radius: 2px;
                        }
                        .img-undefined {
                            @include common.size(80%, 80%);
                            margin: 10% 0 0 -4%;
                            opacity: 0.2;
                        }
                        .today {
                            color: theme.$main-color;
                            font-weight: 700;
                        }
                    }
                    .col.cell.selected {
                        @include common.shape(theme.$step-color, none, 10%);
                    }
                }
            }
        }
    }
    .calendar-list::-webkit-scrollbar {
        display: none;
    }
}

 

↓ 스타일링에 사용된 _common.scss 코드

// 넓게 통용되는 것들
@use 'assets/styles/_theme.scss';

@mixin size($width: 14px, $height: 14px) {
    width: $width;
    height: $height;
}
@mixin position-fixed($top: 10px, $left: 10px) {
    position: fixed;
    top: $top;
    left: $left;
}
@mixin shape(
    $background: transparent,
    $border: transparent,
    $border-radius: 0
) {
    background: $background;
    border: $border;
    border-radius: $border-radius;
    -webkit-border-radius: $border-radius;
}
@mixin shadow($size: 3px, $color: black, $amount: 0.5) {
    box-shadow: $size $size 0px 0px transparentize($color, $amount);
}

@mixin font(
    $color: black,
    $font-family: theme.$description-font,
    $font-size: 1em,
    $font-weight: 500
) {
    color: $color;
    font-family: $font-family;
    font-size: $font-size;
    font-weight: $font-weight;
}
@mixin flex {
    display: flex;
    justify-content: center;
    align-items: center;
}
@mixin flex-column($justify-content: center, $align-items: center) {
    display: flex;
    flex-direction: column;
    justify-content: $justify-content;
    align-items: $align-items;
}
@mixin flex-row($justify-content: center, $align-items: center) {
    display: flex;
    flex-direction: row;
    justify-content: $justify-content;
    align-items: $align-items;
}
@mixin hover-event {
    cursor: pointer;
    transition: 0.2s ease-in-out;
}

 

_theme.scss 코드는 단지 프로젝트 컬러, 폰트만 들어있다.

 

 

 

 

↓ 아래와 같이 깜찍하게 응용하여 일정 관리 달력으로 사용할 수 있다.