본문 바로가기

환영합니다. 이 블로그 번째 방문자입니다.
프로젝트 일지

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

프로젝트에서 그림 일기를 달력으로 보여주는 기능이 필요해서 달력을 제작하게 되었다.

라이브러리를 사용해도 되지만 달력의 셀 안에 이미지를 넣어야 하기 때문에 직접 그리기로 했다. 

완성본

 

동작

그럼 시작!

 

↓ 스크롤 달력은 아래에

2022.06.27 - [프로젝트 일지] - [🐾 일지] 리액트 스크롤 달력 만들기

 

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

하이... 블로그 오랜만... 한 4일 동안 인생 첫 면접(심지어 취업도 아니고 교육 면접...)에 멘탈도 털리고 누가 봐도 불합격을 치르고 와서 정신줄 놓고 지내고 있었다..ㅎ 면까몰이지만...아니 제

sennieworld.tistory.com


0️⃣ 뼈대 만들기

크게 header, days, body로 이루어져 있다.

 

new Date()를 이용하면 오늘의 날짜를 가져올 수 있다.

 

 

 

 

1️⃣ header 만들기 (오늘 날짜, 달력 이동 아이콘)

header를 div 안에 직접 작성하지 않고 RenderHeader라는 컴포넌트를 만들어 랜더링해줄 것이다.

자바스크립트에서 날짜 관련 함수의 총 집합 라이브러리인 date-fns를 설치하고 포함된 메소드들을 불러 사용할 것이다.

 

//date-fns 설치 명령어
npm install date-fns --save

 

 

RenderHeader는 currentMonth를 인자로 받는다. 

 

 

col-start에는 {오늘이 속한 월, 오늘이 속한 년도}, col-end에는 이동 아이콘 2개가 들어있다. 

아이콘을 클릭했을 때 실행되는 이전 월로 이동하는 함수 prevMonth다음 월로 이동하는 함수 nextMonth는 다음과 같다.

 

 

date-fns의 subMonths, addMonths를 이용해 이동한다.

prevMonth, nextMonth를 icon의 onClick에 추가해주고 RenderHeader을 렌더링해주면 다음과 같다.

 

빨간 박스 :  수정된 곳

 

 

 

 

2️⃣ Days 만들기 👉🏻 요일

마찬가지로 RenderDays라는 컴포넌트를 만들어 사용한다.

 

요일
컴포넌트

 

렌더링

 

 

 

 

 

 

 

 

3️⃣ Body(Cells) 만들기 👉🏻 달력 그리기

RenderCells라는 컴포넌트를 만들어 사용한다. 길어서 복잡해보이지만 아주 쉽다.

 

 

먼저, date-fns의 메소드들을 이용해 다음의 변수들을 선언한다.

 

ex) currentDate : 2022년 6월 1일 [Wed Jun 01 2022 21:18:49 GMT+0900 (Korean Standard Time)]

1. monthStart : 오늘이 속한 달의 시작일 [1 : Wed Jun 01 2022 00:00:00 GMT+0900 (Korean Standard Time)]

2. monthEnd :  오늘이 속한 달의 마지막일 [30 : Thu Jun 30 2022 23:59:59 GMT+0900 (Korean Standard Time)]

3. startDate : monthStart가 속한 주의 시작일 [5/29 : Sun May 29 2022 00:00:00 GMT+0900 (Korean Standard Time) ]

4. endDate : monthEnd가 속한 주의 마지막일 [7/2 : Sat Jul 02 2022 23:59:59 GMT+0900 (Korean Standard Time)]

그림 정리본

 

5. rows : [일월화수목금토] 한 주 * 4 또는 5주

6. days : [일월화수목금토] 한 주

7. day : startDate

 

while 반복문은 day가 endDate보다 커지면 종료된다. 

일~토 의 7일동안 표를 그려야 하므로 for 문을 돌며 day를 1씩 올려주며 날짜를 추가해준다. 

for문이 끝나면 rows에 추가를 해주고 초기화를 해준다.

 

className에 길게 들어가는 조건문은 오늘의 날짜를 표시해주는(selected) 것과 현재 달의 날짜가 아닌 날들은 색상을 다르게 표현을 해주었다.

 

 

 

 

 

 

 

 

4️⃣ 총 코드

아주 아름다운 코드가 탄생했다.

 

복붙을 원하는 사람이 있을수도 있으니 아래에 남겨놓겠다.(+styling 코드)

더보기

# Calender.js

import React, { useState } from 'react';
import { Icon } from '@iconify/react';
import { format, addMonths, subMonths } from 'date-fns';
import { startOfMonth, endOfMonth, startOfWeek, endOfWeek } from 'date-fns';
import { isSameMonth, isSameDay, addDays, parse } from 'date-fns';

const RenderHeader = ({ currentMonth, prevMonth, nextMonth }) => {
    return (
        <div className="header row">
            <div className="col col-start">
                <span className="text">
                    <span className="text month">
                        {format(currentMonth, 'M')}월
                    </span>
                    {format(currentMonth, 'yyyy')}
                </span>
            </div>
            <div className="col col-end">
                <Icon icon="bi:arrow-left-circle-fill" onClick={prevMonth} />
                <Icon icon="bi:arrow-right-circle-fill" onClick={nextMonth} />
            </div>
        </div>
    );
};

const RenderDays = () => {
    const days = [];
    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, onDateClick }) => {
    const monthStart = startOfMonth(currentMonth);
    const monthEnd = endOfMonth(monthStart);
    const startDate = startOfWeek(monthStart);
    const endDate = endOfWeek(monthEnd);

    const rows = [];
    let days = [];
    let day = startDate;
    let formattedDate = '';

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

export const Calender = () => {
    const [currentMonth, setCurrentMonth] = useState(new Date());
    const [selectedDate, setSelectedDate] = useState(new Date());

    const prevMonth = () => {
        setCurrentMonth(subMonths(currentMonth, 1));
    };
    const nextMonth = () => {
        setCurrentMonth(addMonths(currentMonth, 1));
    };
    const onDateClick = (day) => {
        setSelectedDate(day);
    };
    return (
        <div className="calendar">
            <RenderHeader
                currentMonth={currentMonth}
                prevMonth={prevMonth}
                nextMonth={nextMonth}
            />
            <RenderDays />
            <RenderCells
                currentMonth={currentMonth}
                selectedDate={selectedDate}
                onDateClick={onDateClick}
            />
        </div>
    );
};

 

# _style.scss

@use 'assets/styles/_theme.scss';
@use 'assets/styles/common/_common.scss' as common;

.calendar {
    @include common.size(60%, 90%);
    .header {
        @include common.size(100%, 7%);
        @include common.flex-row(space-between, baseline);
        .col.col-first {
            @include common.size(80%, 100%);
            @include common.flex-column(center, flex-start);
            margin-left: 1%;
            .text {
                font-size: 0.8em;
            }
            .text.month {
                margin-right: 5px;
                font-size: 1.6em;
                font-weight: 600;
            }
        }
        .col.col-end {
            @include common.size(20%, 100%);
            @include common.flex-row(flex-end, baseline);
            svg {
                @include common.size(11%, fit-content);
                margin-left: 5%;
                color: transparentize(gray, 0.2);
                &:hover {
                    @include common.hover-event();
                    transform: scale(1.15);
                    color: theme.$dark-gray-color;
                }
            }
        }
    }
    .days {
        @include common.size(100%, fit-content);
        @include common.flex-row(space-between, center);
        font-weight: 600;
        font-size: 0.65em;
        padding: 2px;
        color: theme.$dark-gray-color;
        .col {
            @include common.size(12.9%, 100%);
            @include common.flex-column(flex-end, flex-start);
            padding-left: 1%;
            background: transparentize(theme.$step-color, 0.6);
            border-radius: 10px;
        }
    }
    .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(13.5%, 93%);
                @include common.flex-row(flex-start, flex-start);

                border: 0.4px solid transparentize(gray, 0.4);
                border-radius: 3px;
                font-size: 0.8em;
                span {
                    margin: 4px 0 0 4px;
                }
                .not-valid {
                    color: theme.$gray-color;
                }
                img {
                    opacity: 0.1;
                }
            }
            .col.cell.valid {
                &:hover {
                    @include common.hover-event();
                    @include common.shadow(1.5px, theme.$dark-gray-color, 0.1);
                    transform: scale(1.01);
                    border: none;
                    background: transparentize(theme.$gray-color, 0.5);
                }
            }
            .col.cell.selected {
                @include common.shadow(1.5px, theme.$main-color, 0.1);
                transform: scale(1.02);
                border: none;
                background: theme.$sub-color;
                color: theme.$main-color;
                font-weight: 600;
            }
        }
    }
}

 

# _common.scss : 이건 그냥 내가 편하게 코드 짤려고 만들어놓고 불러서 사용하는 코드이다.

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

@mixin size($width: 14px, $height: 14px) {
    width: $width;
    height: $height;
}

@mixin shape(
    $background: transparent,
    $border: transparent,
    $border-radius: 0
) {
    background: $background;
    border: $border;
    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 hover-event {
    cursor: pointer;
    transition: 0.2s ease-in-out;
}

@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;
}

 

# _theme.scss : 이것도 마찬가지고 자주 쓰는 색상들을 모아논 것이다 (프로젝트 메인 색상)

// 색상
$main-color: #aa5b42;
$sub-color: #f3c5b6;
$login-color: #f4e7e3;
$dashboard-color: #fffcfb;
$dashboard-icon-color: #d6a291;
$step-color: #ebcfc6;
$dark-step-color: #cea193;
$gray-color: #c4c4c4;
$dark-gray-color: #686868;