썸네일 어그로에 끌리셨다면...ㅋ

1. 프로젝트 생성

https://github.com/zynkn/component-calendar 에서 프로젝트를 fork 하거나 clone 해주세요.

그리고 init branch로 이동 합니다.

git checkout init
...
yarn 
...
yarn start

폴더트리.PNG

이닛.PNG

프로젝트 실행 후 위의 결과가 나오면 준비 완료입니다.

2. 디자인 확인

ideal.jpg

우리가 만들 캘린더 디자인입니다.

헤더와 하단의 이벤트 입력 컴포넌트는 만들지 않을겁니다.

3. UI 코딩

components/Calendar/Calendar.tsx

import React from 'react';
import './Calendar.scss';
import { MdChevronLeft, MdChevronRight } from 'react-icons/md';

function Calendar() {
  return (
    <div className="Calendar">
      <div className="head">
        <button><MdChevronLeft /></button>
        <span className="title">December 2016</span>
        <button><MdChevronRight /></button>
      </div>
      <div className="body">
        Body
      </div>
    </div>
  )
}
export default Calendar;

components/Calendar/Calendar.scss

.Calendar{
  user-select: none;
  .head{
    display: flex;
    justify-content: space-between;
    align-items: center;
    padding: 12px 8px;
    button{
      cursor: pointer;
      outline: none;
      display: inline-flex;
      background: transparent;
      border: none;
      font-size: 20pt;
      padding: 4px;
      border-radius: 4px;
      &:hover{
        background-color: rgba(gray,0.1);
      }
      &:active{
        background-color: rgba(gray,0.2);
      }
    }
    span.title{
      cursor:pointer;
      border-radius: 5px;
      padding: 4px 12px;
      &:hover{
        background-color: rgba(gray,0.1);
      }
      &:active{
        background-color: rgba(gray,0.2);
      }
    }
  }
}

스텝1.PNG

이제 캘린더 Body 부분을 코딩하겠습니다.

components/Calendar/Calendar.tsx

import React from 'react';
import './Calendar.scss';
import { MdChevronLeft, MdChevronRight } from 'react-icons/md';

function Calendar() {
  return (
    <div className="Calendar">
      <div className="head">
        <button><MdChevronLeft /></button>
        <span className="title">December 2016</span>
        <button><MdChevronRight /></button>
      </div>
      <div className="body">
        <div className="row">
          <div className="box">
            <span className="text">SUN</span>
          </div>
          <div className="box">
            <span className="text">MON</span>
          </div>
          <div className="box">
            <span className="text">TUE</span>
          </div>
          <div className="box">
            <span className="text">WED</span>
          </div>
          <div className="box">
            <span className="text">THU</span>
          </div>
          <div className="box">
            <span className="text">FRI</span>
          </div>
          <div className="box">
            <span className="text">SAT</span>
          </div>
        </div>

        <div className="row">
          <div className="box grayed">
            <span className="text">28</span>
          </div>
          <div className="box grayed">
            <span className="text">29</span>
          </div>
          <div className="box grayed">
            <span className="text">30</span>
          </div>
          <div className="box selected">
            <span className="text">1</span>
          </div>
          <div className="box">
            <span className="text">2</span>
          </div>
          <div className="box">
            <span className="text">3</span>
          </div>
          <div className="box">
            <span className="text">4</span>
          </div>
        </div>
      </div>
    </div>
  )
}
export default Calendar;

스타일링을 위해서 직접 하드 코딩합니당.

components/Calendar/Calendar.scss

.Calendar{
  user-select: none;
  .head{
    ...
  }
  .body{
    .row{
      display: flex;
      cursor: pointer;
      &:first-child{
        cursor: initial;
        .box{
          font-weight: bold;
        }
        .box:hover > span.text{
          background-color: white;
        }
      }
      .box{
        position: relative;
        display: inline-flex;
        width: calc(100%/7);
        height: 0;
        padding-bottom: calc(100%/7);
        font-size: 12pt;
        &:first-child{
          color: red;
        }
        &:last-child{
          color: #588dff;
        }
        &.grayed{
          color: gray;
        }
        &:hover{
          span.text{
            background-color: rgba(#588dff, 0.1);
          }
        }
        &.selected{
          span.text{
            background-color: #588dff;
            color: white;
          }
        }
        span.text{
          border-radius: 100%;
          display: inline-flex;
          justify-content: center;
          align-items: center;
          width: 60%;
          height: 60%;
          position: absolute;
          top: 50%;
          left: 50%;
          transform: translate(-50%,-50%);
        }
      }
    }
  }
}

스텝2.PNG

UI 코딩은 끝이 났습니다. 이제 momentjs를 이용해서 캘린더를 출력해보겠습니다.

4. 캘린더 출력하기

캘린더 출력하는 알고리즘(?)은 구글링하면 다양하게 나옵니다. 정답은 없지 않을까요?

components/Calendar/Calendar.tsx

import React from 'react';
import './Calendar.scss';
import { MdChevronLeft, MdChevronRight } from 'react-icons/md';
import moment, { Moment as MomentTypes } from 'moment';

function Calendar() {
  function generate() {
    const today = moment();
    const startWeek = today.clone().startOf('month').week();
    const endWeek = today.clone().endOf('month').week() === 1 ? 53 : today.clone().endOf('month').week();
    let calendar = [];
    for (let week = startWeek; week <= endWeek; week++) {
      calendar.push(
        <div className="row" key={week}>
          {
            Array(7).fill(0).map((n, i) => {
              let current = today.clone().week(week).startOf('week').add(n + i, 'day')
              let isSelected = today.format('YYYYMMDD') === current.format('YYYYMMDD') ? 'selected' : '';
              let isGrayed = current.format('MM') === today.format('MM') ? '' : 'grayed';
              return (
                <div className={`box  ${isSelected} ${isGrayed}`} key={i}>
                  <span className={`text`}>{current.format('D')}</span>
                </div>
              )
            })
          }
        </div>
      )
    }
    return calendar;
  }
  return (
    <div className="Calendar">
      <div className="head">
        <button><MdChevronLeft /></button>
        <span className="title">{moment().format('MMMM YYYY')}</span>
        <button><MdChevronRight /></button>
      </div>
      <div className="body">
        <div className="row">
          <div className="box">
            <span className="text">SUN</span>
          </div>
          <div className="box">
            <span className="text">MON</span>
          </div>
          <div className="box">
            <span className="text">TUE</span>
          </div>
          <div className="box">
            <span className="text">WED</span>
          </div>
          <div className="box">
            <span className="text">THU</span>
          </div>
          <div className="box">
            <span className="text">FRI</span>
          </div>
          <div className="box">
            <span className="text">SAT</span>
          </div>
        </div>
        {generate()}
      </div>
    </div>
  )
}
export default Calendar;

스텝3.PNG

오늘 날짜 기준으로 캘린더가 생성되었습니다.

function generate() {
    const today = moment();
    const startWeek = today.clone().startOf('month').week();
    const endWeek = today.clone().endOf('month').week() === 1 ? 53 : today.clone().endOf('month').week();
    let calendar = [];
    for (let week = startWeek; week <= endWeek; week++) {
      calendar.push(
        <div className="row" key={week}>
          {
            Array(7).fill(0).map((n, i) => {
              let current = today.clone().week(week).startOf('week').add(n + i, 'day')
              return (
                <div className={`box`} key={i}>
                  <span className={`text`}>{current.format('D')}</span>
                </div>
              )
            })
          }
        </div>
      )
    }
    return calendar;
  }

위의 코드가 캘린더를 생성하는 코드입니다.

스스로 분석해 보는 것도 좋은 공부가 될 것 같습니다.

5. Redux 스토어 만들기

React-hooks를 이용해도 됩니다.

yarn add react-redux redux-actions

리덕스 관련 라이브러리를 먼저 설치합니다.

stores/calendar.ts

import { createAction, handleActions } from 'redux-actions';
import moment, { Moment as MomentTypes } from 'moment';
import produce from "immer"

const DATE_CHANGE = 'calendar/DATE_CHANGE';
export const changeDate = createAction(DATE_CHANGE);

export interface CalendarState {
  date: MomentTypes
}
const initialState: CalendarState = {
  date: moment(),
}
export default handleActions({
  [DATE_CHANGE]: (state, action: any) => {
    return produce(state, draft => {
      draft.date = action.payload
    })
  }
}, initialState)

stores/index.ts

import { createStore, combineReducers } from "redux";
import calendar from "./calendar";

export default createStore(combineReducers({ calendar }));

./index.tsx

import React from 'react';
import ReactDOM from 'react-dom';
import { Provider } from 'react-redux';
import './index.css';
import App from './App';
import * as serviceWorker from './serviceWorker';
import store from 'stores';

ReactDOM.render(
  <Provider store={store}>
    <App />
  </Provider>, document.getElementById('root'));

containers/CalendarContainer.tsx

import React from 'react';
import Calendar from 'components/Calendar';
import { Moment as MomentTypes } from "moment";
import { connect } from 'react-redux';
import { changeDate } from 'stores/calendar';
import { bindActionCreators } from 'redux';

interface Props {
  date: MomentTypes
  changeDate: typeof changeDate
}
class CalendarContainer extends React.Component<Props> {
  render() {
    const { date, changeDate } = this.props;
    return (
      <Calendar date={date} changeDate={changeDate} />
    )
  }
}
export default connect(
  ({ calendar }: any) => ({
    date: calendar.date
  }),
  (dispatch) => ({
    changeDate: bindActionCreators(changeDate, dispatch)
  })
)(CalendarContainer);

components/Calendar/Calendar.tsx

(...)
interface Props {
  date: MomentTypes
  changeDate: Function
}
function Calendar(props: Props) {
 (...)
 function generate() {

    // today를 props.date로 변경합니다.
    const today = moment();
    const startWeek = props.date.clone().startOf('month').week();
    const endWeek = props.date.clone().endOf('month').week() === 1 ? 53 : props.date.clone().endOf('month').week();
    let calendar = [];
    for (let week = startWeek; week <= endWeek; week++) {
      calendar.push(
        <div className="row" key={week}>
          {
            Array(7).fill(0).map((n, i) => {
              let current = props.date.clone().week(week).startOf('week').add(n + i, 'day');
              let isToday = today.format('YYYYMMDD') === current.format('YYYYMMDD') ? 'today' : '';
              let isSelected = props.date.format('YYYYMMDD') === current.format('YYYYMMDD') ? 'selected' : '';
              let isGrayed = current.format('MM') === props.date.format('MM') ? '' : 'grayed';

              // .box에 changeDate 이벤트를 달아줍니다.
              return (
                <div className={`box ${isSelected} ${isGrayed} ${isToday}`} key={i} onClick={() => props.changeDate(current)}>
                  <span className={`text`}>{current.format('D')}</span>
                </div>
              )
            })
          }
        </div>
      )
    }
    return calendar;
  }
  return (
     <div className="Calendar">
      <div className="head">
        <button onClick={() => props.changeDate(props.date.clone().subtract(1, 'month'))}><MdChevronLeft /></button>
        <span className="title">{props.date.format('MMMM YYYY')}</span>
        <button onClick={() => props.changeDate(props.date.clone().add(1, 'month'))}><MdChevronRight /></button>
      </div>
     (...)
  )
}

components/Calendar/Calendar.scss

(...)
       &:hover{
          span.text{
            background-color: rgba(#588dff, 0.1);
          }
        }
        &.today{
          span.text{
            background-color: rgba(red, 0.1);
          }
        }
        &.selected{
          span.text{
            background-color: #588dff;
            color: white;
          }
        }
(...)

스텝4.PNG

캘린더가 잘 작동하는지 확인해주세요.

아직 기능 하나가 남았습니다.

캘린더 헤더에 있는 월 년 텍스트를 클릭하면 오늘 날짜로 돌아오는 기능을 구현해야합니다.

이건 혼자서 구현해보시길 바랍니다.

6. 끝

문제가 있다면 댓글로 알려주세요.