개발 중인 서비스에 캘린더 기능이 있다.
RN에서 캘린더 하면 다들 아는 유명한 라이브러리가 있지만, 해당 라이브러리가 원하는 기능을 다 포함하지 못해서 캘린더를 직접 구현했다.
해당 캘린더는 date-fns만 있다면 구현할 수 있다!
내가 생각한 전체적인 구상은
1. 선택한 년, 월에 해당하는 날짜(일) 정보를 이차원 배열 형태로 구한다.
2. 해당 2차원 배열을 돌면서 화면에 출력한다.
이다.
먼저 1번, 날짜 정보를 구해보자
일단 내가 만들고 있는 캘린더는 년도와 월을 picker 형태로 선택하기 때문에 함수의 파라미터로 year과 month를 넘겼다.
만약 캘린더를 옆으로 넘기는 형식이라면 대신 date 전체를 넘겨서 사용하면 된다.
일단 보여줄 캘린더의 시작과 끝 날짜를 구해보자.
여기서 시작과 끝이란 해당 월의 시작과 끝인 1일과 28,29,30,31과 같은 날짜를 의미하는 것이 아니라 아래 사진에서 회색으로 처리된 26과 1을 의미한다.
시작과 끝을 구하는 이유는 이번 달이 아니더라도 화면에는 그 날짜가 가지는 '칸'이 해당 위치에 보여져야 하기 때문이다.
(그게 빈칸이건, 아래와 같이 연한 숫자를 가진 칸이건 상관없이)
그러니까 위의 사진의 경우 5*7 배열을 구하기 위해서라고 생각하면 쉽다.
아래는 코드이다.
const monthStart = startOfWeek(new Date(year, month - 1)); // 월 시작 주의 첫번째 날짜 (이전 달 일 수 있음)
const monthEnd = endOfWeek(endOfMonth(new Date(year, month - 1))); // 월 마지막 주의 7번째 날짜 (이전 달 일 수 있음)
startOfWeek()라는 date-fns에서 제공하는 함수를 사용했다. 이 함수는 Date() 형식의 값을 넣으면 날짜에 해당하는 week의 첫번째 날짜를 구해준다.
new Date()에 년도와 월만 넣어주면 자동으로 해당 년, 월의 1일이 구해지므로 시작날짜를 구할 수 있다.
끝날짜도 마찬가지의 이유로 구한다. 다만 차이점은 endOfMonth()를 사용하여 달의 끝 날짜를 먼저 구한 후 endOfWeek()를 사용했다.
이번에는 위에서 구한 monthStart와 monthEnd로 전체 날짜 정보를 구하는 코드이다.
const monthList = [];
let weekList = [];
let date = monthStart;
while (date <= monthEnd) {
for (let i = 0; i < 7; i++) {
// 한 주에 하루씩 추가
weekList.push({
date: date,
isCurMonth: getMonth(date) === month - 1,
// 이 외에 필요한 정보들
});
date = addDays(date, 1); // 다음날로 변경
}
monthList.push(weekList); // month에 한 주 추가
weekList = []; // 한 주 초기화
}
7번 반복해서 해당 날짜의 정보를 weekList 배열에 넣는다. 그리고 이 weekList를 monthList에 쌓아주는 식으로 구현했다.
여기서 isCurMonth는 화면에 렌더링 할 때 해당 month가 아닌 날짜는 다른 처리를 하기 위해 넣었다.
이 밖에도 필요한 정보들이 있다면 여기서 구해서 넣으면 된다. 필자도 실제로는 해당 날짜의 기념일 정보 등을 추가로 넣어서 구현했다.
이렇게 하면 목표였던 날짜 정보인 monthList를 구했다.
데이터를 다 구했으므로 이제 monthList의 이차원 배열을 돌면서 화면에 보여주기만 하면 된다.
아래는 구현 코드이다.
const Week = styled.View`
flex: 1;
flex-direction: row;
`;
const DateBox = styled.TouchableOpacity`
flex: 1;
padding: 1px;
align-items: center;
overflow: hidden;
`;
monthDays.map((week, idx) => (
<Week key={idx}>
{week.map(date => {
const formatDate = format(date.date, 'yyyy-MM-dd');
return (
<DateBox
key={date.date}
onPress={() => {
navigation.navigate('Post', {
date: date.date
});
}}>
{date.isCurMonth && (
<>
{/* 오늘인 경우 원으로 표시하기 */}
<AppComponents.Circle
width={23}
height={20}
color={
formatDate == format(today, 'yyyy-MM-dd') &&
date.isCurMonth
? AppColors.Primary
: null
}>
<AppFonts.Body2>{getDate(date.date)}</AppFonts.Body2>
</AppComponents.Circle>
{/* 기념일은 최대 3개까지만 들어감 */}
{date.annData &&
date.annData[formatDate] &&
date.annData[formatDate].map(plan => (
<MiniText
type={plan.anniversaryType}
key={plan.anniversaryId}>
<AppFonts.Caption
numberOfLines={1}
ellipsizeMode="tail">
{plan.anniversaryContent}
</AppFonts.Caption>
</MiniText>
))}
</>
)}
</DateBox>
);
})}
</Week>
))
오늘 날짜를 따로 표시하고 싶어서 날짜를 비교하여 오늘인 경우 동그라미로 표시했고, 해당 날짜를 클릭하면 post 페이지로 이동 시키게 했다.
css는 7개의 주를 정해진 너비안에서 같은 폭으로 만들기 위해 flex를 1로 주었다.
부가적인 기능들을 넣어서 코드가 길어졌지만 실제로는 아래와 같이 아주 간단한 코드이다.
monthDays.map((week, idx) => (
<Week key={idx}>
{week.map(date => (
<DateBox key={date.date}>
{date.isCurMonth && (
<AppFonts.Body2>{getDate(date.date)}</AppFonts.Body2>
)}
</DateBox>
))}
</Week>
));
참고 블로그
2. RN 달력만들기(직접구현하기) (tistory.com)
'react-native' 카테고리의 다른 글
[react-native] 입력 창 연속해서 이동하며 focus 하기(2) - useRef 여러개를 배열로 관리하기 (0) | 2023.04.05 |
---|---|
[react-native] 입력 창 연속해서 이동하며 focus 하기(1), 컴포넌트로 ref 넘기기, forwardRef (0) | 2023.04.02 |
[react-native/recoil] recoil selector 사용하기 (0) | 2023.03.12 |
[react-native/recoil] atom을 서버와 연동하기 (0) | 2023.01.06 |
[react-native] 로딩 바 만들기 (0) | 2023.01.02 |