프로젝트에서 화면에 달력을 보여주고, 선택하는 날짜에 맞는 데이터를 불러와 내용을 띄워줘야 하는 기획이 있어 캘린더를 구현하게 되었습니다. 직접 캘린더를 구현하는 것과 라이브러리를 사용하는 방법 중 고민 끝에 개발 시간을 단축 시킬 수 있고 비교적 커스텀이 유연한 react-calendar 라이브러리를 사용하여 구현하게 되었습니다.
우선 프로젝트에 라이브러리를 설치해 줍니다.
npm i react-calendar
저는 styled-component를 사용했기 때문에 style 파일에서 Calendar를 불러와 생성해주었습니다. 캘린더를 커스텀 하기 위해서는 react-calendar/dist/Calendar.css
파일도 불러와 주어야 합니다.
- styles.ts
import Calendar from "react-calendar";
import "react-calendar/dist/Calendar.css";
// 캘린더를 감싸주는 스타일
export const StyledCalendarWrapper = styled.div`
width: 100%;
display: flex;
justify-content: center;
position: relative;
`
// 캘린더를 불러옴
export const StyledCalendar = styled(Calendar)``;
다음으로 index.tsx 파일에서 작성해준 스타일을 불러옵니다. StyledCalendar
안에서 원하는 조건에 맞는 값들을 설정해 줄 수 있습니다. 더 다양한 옵션을 설정하고 싶으시다면 react-calendar 사이트에서 확인해 보실 수 있습니다.
- index.tsx
import { StyledCalendarWrapper, StyledCalendar } from "./styles";
type ValuePiece = Date | null;
type Value = ValuePiece | [ValuePiece, ValuePiece];
const DogInfo = () => {
const today = new Date();
const [date, setDate] = useState<Value>(today);
const handleDateChange = (newDate: Value) => {
setDate(newDate);
};
return (
<StyledCalendarWrapper>
<StyledCalendar
value={date}
onChange={handleDateChange}
formatDay={(locale, date) => moment(date).format("D")} // 일 제거 숫자만 보이게
formatYear={(locale, date) => moment(date).format("YYYY")} // 네비게이션 눌렀을때 숫자 년도만 보이게
formatMonthYear={(locale, date) => moment(date).format("YYYY. MM")} // 네비게이션에서 2023. 12 이렇게 보이도록 설정
calendarType="gregory" // 일요일 부터 시작
showNeighboringMonth={false} // 전달, 다음달 날짜 숨기기
next2Label={null} // +1년 & +10년 이동 버튼 숨기기
prev2Label={null} // -1년 & -10년 이동 버튼 숨기기
minDetail="year" // 10년단위 년도 숨기기
/>
</StyledCalendarWrapper>
);
}
달력의 스타일과 추가 기능들을 위해 커스텀을 진행했습니다. 강아지가 출석한 날은 점으로 표시해주고, 다른 년도 다른 월 위치에 있다가 상단 우측의 오늘 버튼을 누르면 오늘 날짜 캘린더로 돌아오는 기능을 추가해 주었습니다.
아래는 스타일 커스텀 내용입니다. 스타일은 캘린더를 감싸주고 있는 StyledCalendarWrapper
안에서 설정해 주어야 적용됩니다. 세부적인 스타일 커스텀을 위해 개발자도구에서 리액트 캘린더 라이브러리의 내부 구조를 자세히 살펴보고 원하는 요소의 클래스명을 찾아 설정해주면 원하는 스타일을 조금 더 수월하게 적용해 줄 수 있습니다.
import Calendar from "react-calendar";
import styled from "styled-components";
import "react-calendar/dist/Calendar.css";
export const StyledCalendarWrapper = styled.div`
width: 100%;
display: flex;
justify-content: center;
position: relative;
.react-calendar {
width: 100%;
border: none;
border-radius: 0.5rem;
box-shadow: 4px 2px 10px 0px rgba(0, 0, 0, 0.13);
padding: 3% 5%;
background-color: white;
}
/* 전체 폰트 컬러 */
.react-calendar__month-view {
abbr {
color: ${(props) => props.theme.gray_1};
}
}
/* 네비게이션 가운데 정렬 */
.react-calendar__navigation {
justify-content: center;
}
/* 네비게이션 폰트 설정 */
.react-calendar__navigation button {
font-weight: 800;
font-size: 1rem;
}
/* 네비게이션 버튼 컬러 */
.react-calendar__navigation button:focus {
background-color: white;
}
/* 네비게이션 비활성화 됐을때 스타일 */
.react-calendar__navigation button:disabled {
background-color: white;
color: ${(props) => props.theme.darkBlack};
}
/* 년/월 상단 네비게이션 칸 크기 줄이기 */
.react-calendar__navigation__label {
flex-grow: 0 !important;
}
/* 요일 밑줄 제거 */
.react-calendar__month-view__weekdays abbr {
text-decoration: none;
font-weight: 800;
}
/* 일요일에만 빨간 폰트 */
.react-calendar__month-view__weekdays__weekday--weekend abbr[title="일요일"] {
color: ${(props) => props.theme.red_1};
}
/* 오늘 날짜 폰트 컬러 */
.react-calendar__tile--now {
background: none;
abbr {
color: ${(props) => props.theme.primary_2};
}
}
/* 네비게이션 월 스타일 적용 */
.react-calendar__year-view__months__month {
border-radius: 0.8rem;
background-color: ${(props) => props.theme.gray_5};
padding: 0;
}
/* 네비게이션 현재 월 스타일 적용 */
.react-calendar__tile--hasActive {
background-color: ${(props) => props.theme.primary_2};
abbr {
color: white;
}
}
/* 일 날짜 간격 */
.react-calendar__tile {
padding: 5px 0px 18px;
position: relative;
}
/* 네비게이션 월 스타일 적용 */
.react-calendar__year-view__months__month {
flex: 0 0 calc(33.3333% - 10px) !important;
margin-inline-start: 5px !important;
margin-inline-end: 5px !important;
margin-block-end: 10px;
padding: 20px 6.6667px;
font-size: 0.9rem;
font-weight: 600;
color: ${(props) => props.theme.gray_1};
}
/* 선택한 날짜 스타일 적용 */
.react-calendar__tile:enabled:hover,
.react-calendar__tile:enabled:focus,
.react-calendar__tile--active {
background-color: ${(props) => props.theme.yellow_2};
border-radius: 0.3rem;
}
`;
export const StyledCalendar = styled(Calendar)``;
/* 오늘 버튼 스타일 */
export const StyledDate = styled.div`
position: absolute;
right: 7%;
top: 6%;
background-color: ${(props) => props.theme.primary_3};
color: ${(props) => props.theme.yellow_2};
width: 18%;
min-width: fit-content;
height: 1.5rem;
text-align: center;
margin: 0 auto;
line-height: 1.6rem;
border-radius: 15px;
font-size: 0.8rem;
font-weight: 800;
`;
/* 오늘 날짜에 텍스트 삽입 스타일 */
export const StyledToday = styled.div`
font-size: x-small;
color: ${(props) => props.theme.br_2};
font-weight: 600;
position: absolute;
top: 50%;
left: 50%;
transform: translateX(-50%);
`;
/* 출석한 날짜에 점 표시 스타일 */
export const StyledDot = styled.div`
background-color: ${(props) => props.theme.br_2};
border-radius: 50%;
width: 0.3rem;
height: 0.3rem;
position: absolute;
top: 60%;
left: 50%;
transform: translateX(-50%);
`;
아래는 index.tsx 파일에서 위의 점 스타일과 기능을 추가해준 내용입니다. 오늘 날짜로 돌아오는 기능을 위해서 activeStartDate가 필요합니다. handleTodayClick을 클릭했을때 오늘 날짜 캘린더로 돌아옵니다. attendDay 값에 맞는 날짜에 점으로 출석한 날짜가 표시됩니다.
import {
StyledCalendarWrapper,
StyledCalendar,
StyledDate,
StyledToday,
StyledDot,
} from "./styles";
import moment from "moment";
type ValuePiece = Date | null;
type Value = ValuePiece | [ValuePiece, ValuePiece];
const DogInfo = () => {
const today = new Date();
const [date, setDate] = useState<Value>(today);
const [activeStartDate, setActiveStartDate] = useState<Date | null>(
new Date()
);
const attendDay = ["2023-12-03", "2023-12-13"]; // 출석한 날짜 예시
const handleDateChange = (newDate: Value) => {
setDate(newDate);
};
const handleTodayClick = () => {
const today = new Date();
setActiveStartDate(today);
setDate(today);
};
return (
<StyledCalendarWrapper>
<StyledCalendar
value={date}
onChange={handleDateChange}
formatDay={(locale, date) => moment(date).format("D")}
formatYear={(locale, date) => moment(date).format("YYYY")}
formatMonthYear={(locale, date) => moment(date).format("YYYY. MM")}
calendarType="gregory"
showNeighboringMonth={false}
next2Label={null}
prev2Label={null}
minDetail="year"
// 오늘 날짜로 돌아오는 기능을 위해 필요한 옵션 설정
activeStartDate={
activeStartDate === null ? undefined : activeStartDate
}
onActiveStartDateChange={({ activeStartDate }) =>
setActiveStartDate(activeStartDate)
}
// 오늘 날짜에 '오늘' 텍스트 삽입하고 출석한 날짜에 점 표시를 위한 설정
tileContent={({ date, view }) => {
let html = [];
if (
view === "month" &&
date.getMonth() === today.getMonth() &&
date.getDate() === today.getDate()
) {
html.push(<StyledToday key={"today"}>오늘</StyledToday>);
}
if (
attendDay.find((x) => x === moment(date).format("YYYY-MM-DD"))
) {
html.push(<StyledDot key={moment(date).format("YYYY-MM-DD")} />);
}
return <>{html}</>;
}}
/>
// 오늘 버튼 추가
<StyledDate onClick={handleTodayClick}>오늘</StyledDate>
</StyledCalendarWrapper>
);
};
export default DogInfo;
좋은 라이브러리네요! 친절한 설명 감사합니다 o_<