React로 UI 라이브러리 사용없이 Moment.js만으로 달력 컴포넌트 만들기 📅
여지껏 기획된 화면 위에 네모를 그리는 방식으로 으레 컴포넌트 설계를 해왔었는데 최근에 피드백을 받으며 크게 깨닫게 된 사실이 있다: UI를 기준으로 컴포넌트를 쪼개는 건 좋은 방법이 아니다!!
그래서 앞으로 컴포넌트를 설계할 때 고려할 기준 2가지를 정했다:
- 행위가 들어가면 컴포넌트화 하자.
- 각 컴포넌트가 어떤 상태를 가져야 하는지를 계획하자.
상태
란? 꼭 변해야만 상태는 아니다. 요일(월, 화, 수...)과 같은 데이터는 "정적상태"를 가진 컴포넌트가 될 수 있다.
공유하는 상태를 기준으로 만든 첫 설계도
위에서 보여지는 노란색 글씨는 공유될 것이라고 예상되었던 상태들의 모양이다.
결국 구현하는 과정에서 상태에 프로퍼티들이 계속 추가되어서 상태는 설계된 모습과는 많이 달라졌다. 하지만 이렇게 기준을 세우고 나니 기본적으로 컴포넌트 계층 구조는 확립이 되어서 첫 설계도와 같은 구조를 갖게 되었다.
최종 컴포넌트 설계도
방법은 여러가지인 것 같아 나만의 가설을 세워보기로 했다.
배열로 관리되는 날짜부분을 분석해보니 지난 달의 마지막 날짜들
+ 이번 달의 모든 날짜들
+ 다음 달의 처음 날짜들
의 조합이라는 결론이 난다.
각각의 배열을 만들어서 하나의 배열로 합쳐준 뒤 map을 돌려 하나의 컴포넌트에 전부 보여지게 만들자.
그렇다면 각 날짜들을 구하기 위해 필요한 정보는 무엇일까?
요일에 알맞게 차례대로 날짜를 채워넣으려면 첫 날(매월 1일)이 무슨 요일인지만 알면된다.
· 필요한 정보: firstDayOfThisMonth
이번 달의 1일이 무슨 요일인지에 따라 채워줄 지난 달의 날짜 갯수가 달라진다.
일주일은 7일이기 때문에 필요한 지난 달의 날짜 갯수는 7 - 이번달 1일의 요일(인덱스 정보)이 된다.
그리고 지난 달의 마지막 날로부터 필요한 날짜 갯수만큼 countdown 해야하기 때문에 지난 달의 마지막 날짜정보가 필요하다.
두 정보가 구해지면 배열에 들어갈 가장 첫 날짜(지난 달의 마지막 날짜 - 이번달 1일의 요일 인덱스 +1)를 찾아서 마지막 날짜에 도달할 때까지 하나씩 더해주며 배열에 push한다.
· 필요한 정보: lastDateOfLastMonth
, firstDayOfThisMonth
다음 달의 날짜들은 무조건 1부터 채워지기 때문에 필요한 날짜 갯수만 알면 된다.
이 또한 이번 달의 마지막 날이 무슨 요일(인덱스)인지 알면 채워줘야 할 다음 달의 날짜 갯수, 즉, 배열의 길이가 계산된다(6 - 이번 달 마지막 날의 요일 인덱스).
· 필요한 정보: lastDayOfThisMonth
Date객체와 Moment.js사용을 두고 고민을 많이 했지만 결국 Moment를 사용하기로 결정한 데에는 이유가 있었다.
Moment는 내가 원하는 형식으로 값을 반환해줘서 가공없이 바로 사용이 가능하기 때문!
JavaScript의 Date객체를 사용할 때는 사람이 읽을 수 있는 문자열의 형식으로 바꾸기 위해 toDateString()
나 toTimeString()
메서드로 한 번 더 데이터를 바꿔줘야 하는 번거로움이 있다.
그런데 Moment의 경우 format()
으로 원하는 데이터의 형태(ex.YYYY-MM-DD
, DD
등)만 직접 받을 수가 있어서 추가적인 데이터 가공이 필요하지 않다.
하지만 Moment.js도 처음 사용해보는 지라 사용법이 익숙치가 않아서 공식문서의 Docs를 많이 참고했다.
설계 단계에서 필요한 데이터가 무엇인지 전부 뽑아놓았던 터라 원하는 데이터가 제공이 되는지만 확인하면 되었다.
import * as moment from "moment";
- 마지막 날짜를 구하는 방법:
moment([2021, 0, 31]).month(1).format("YYYY-MM-DD"); // 2021-02-28
const lastDateOfCurrMonth = moment([currYear, 0, 31])
.month(currMonth - 1)
.format("DD"); //"DD"는 일자에 해당하는 정보만 반환해준다.
const lastDateOfLastMonth = moment([currYear, 0, 31])
.month(currMonth - 2)
.format("DD");
const firstDayOfThisMonth = moment([currYear, currMonth - 1, 1]).day();
const lastDayOfThisMonth = moment([
currYear,
currMonth - 1,
lastDateOfCurrMonth,
]).day();
Redux로 전역상태를 만들기 전, 로컬에서 필요한 상태를 정의해서 내려주는 방식을 사용했다.
Calendar
가 캘린더를 구성하는 가장 상위 컴포넌트이고, CalendarButtons
와 DatesOfMonth
는 공유하는 상태가 있어서 달력에 관한 로직을 전부 부모 컴포넌트인Calendar
에서 관리했었다.
이 때 마주했던 문제:
1. 날짜관련 로직이 하나둘 추가되다 보니 Calendar 컴포넌트가 상당히 커졌다.
2. 하위 컴포넌트로 전달하는 props가 너무 많아졌다.
그래서 실제로 쓰이는 곳에서만 관련 함수를 import 해서 쓸 수 있는 useCalendar.js
custom hook을 만들어 캘린더 로직과 관련된 부분을 전부 migrate했다.
동시에 Redux를 설치해 공통으로 쓰이는 상태값에 대해서는 전역(global)으로 관리될 수 있도록 했는데, useCalendar
내부에서 useSelector()
로 필요한 상태만 가져오는 것이 가능해졌다.
기존에 props로 전달했었던 로컬 상태와 계산 로직이 custom hook으로 옮겨가면서 컴포넌트가 심플해졌고, props의 갯수가 상당부분 줄어들었다.
Calendar.jsx
비교 이번 캘린더 컴포넌트에는 (개별 날짜를 감싸는 버튼을 제외하고)3개의 버튼이 필요한데, 스타일으로 variation을 준다면 버튼자체를 컴포넌트화해서 재사용할 수 있을 것이란 생각이 들었다.
다음은 구현한 Button컴포넌트의 모습이다:
import React from "react";
import { css } from "styled-components";
import { style } from "./ButtonStyles";
const { PrevArrow, NextArrow, StyledButton } = style;
const ICON = {
prev: PrevArrow,
next: NextArrow,
};
const VARIANT = {
previous: css`
margin: 0.5rem 0 0.5rem 0.5rem;
`,
next: css`
margin: 0.5rem;
`,
thisMonth: css`
width: 70px;
margin: 0.5rem;
font-size: 0.8rem;
`,
};
const Button = ({ icon, name, handleClickFunc, children }) => {
const varientStyle = VARIANT[name];
const contentsSelector = (icon) => {
if (icon) return React.createElement(ICON[icon]);
else return children;
};
return (
<StyledButton varientStyle={varientStyle} onClick={handleClickFunc}>
{contentsSelector(icon)}
</StyledButton>
);
};
export default Button;
세 버튼의 차이점은 버튼의 컨텐츠이기 때문에 contentsSelector
라는 함수를 정의해 버튼 이름을 기준으로 컨텐츠를 다르게 반환할 수 있게 했다. 그런데 화살표 버튼의 경우 꺽쇠모양의 SVG파일이 컨텐츠로 들어가야 했고,return ICON[icon]
은 계속 에러를 반환했다.
알고보니 {contentsSelector(icon)}
이 부분에서는 컴포넌트를 반환해주기를 기대하는데, SVG 파일 자체만으로는 컴포넌트가 아니라 그저 SVG파일 그 자체에 불과했기 때문이었다.
따라서 React.createElement()
를 사용해 컴포넌트화 시켜 반환해줌으로써 에러를 해결했다.
각 JSX 엘리먼트는 단지 React.createElement()를 호출하는 편리한 문법에 불과합니다.
-from. React 공식홈
release
(배포용) 브랜치 생성 후 build 파일 생성//build 파일 생성
yarn build
github repository > Settings > Pages에서 release브랜치로 주소 발급받기
gh-pages 패키지를 설치
//글로벌로 gh-pages 패키지 설치
npm install -g gh-pages
//package.json
{...,
"scripts": {
...
"predeploy": "yarn build", //로컬에서 build 진행 => build 디렉토리가 생성됨
"deploy": "gh-pages -d build" //build 디렉토리의 html을 배포하겠다는 뜻 (d === directory)
},...
"homepage": "https://ha3158987.github.io/Paywork_Calendar/"
}
Pages에서 배포 Source 브랜치를 gh-pages로 설정
yarn build로 predeploy 실행
yarn build
yarn으로 모든 패키지를 관리해왔기 때문에 gh-pages 패키지 또한 yarn add gh-pages --dev
로 설치를 시도했었는데, yarn build
스크립트를 입력해 빌드를 시도하자 gh-pages가 없다는 에러메세지가 나왔다.
검색결과 gh-pages는 글로벌로 설치해야 하는 것으로 확인해 npm install -g gh-pages
로 재설치한 후 실행했더니 정상적으로 빌드가 진행됐다.
또 하나, 처음 repository에 Source 브랜치를 설정하면서 디렉토리 설정이 잘못되는 바람에 배포된 주소에서 프로젝트가 아닌 readme가 보여지는 문제가 있었다.
디렉토리 설정을 다시 한 다음에 배포를 다시 시도해봤지만 브라우저 상에는 여전히 리드미만 보여져서 개발자도구
> Application
> Storage
> Clear site data
로 스토리지에 저장된 브라우징 데이터를 초기화 한 후 다시 실행해 보았다.
정상적으로 배포 성공! 🌈
https://www.daleseo.com/react-button-component/
https://dev-yakuza.posstree.com/ko/react/github-pages/
https://velog.io/@byjihye/react-github-pages
https://ko.reactjs.org/docs/react-api.html
https://medium.com/react-native-seoul/react-%EB%A6%AC%EC%95%A1%ED%8A%B8%EB%A5%BC-%EC%B2%98%EC%9D%8C%EB%B6%80%ED%84%B0-%EB%B0%B0%EC%9B%8C%EB%B3%B4%EC%9E%90-02-react-createelement%EC%99%80-react-component-%EA%B7%B8%EB%A6%AC%EA%B3%A0-reactdom-render%EC%9D%98-%EB%8F%99%EC%9E%91-%EC%9B%90%EB%A6%AC-41bf8c6d3764
https://www.howtogeek.com/664912/how-to-clear-storage-and-site-data-for-a-single-site-on-google-chrome/
Wooooooooooooooooow!!!!!