이전 글에서 React Native Calendars 라이브러리를 활용한 캘린더를 구현하는 글을 작성하였습니다.
이번에는 제가 직접 순수 React Native 코드로 활용하여 캘린더 구현 방법에 대해 작성할려고 합니다.
이렇게 한번 작성하는 이유는 이전글을 보고 오신 분들을 아시겠지만,
우선적으로 커스텀에 대한 불편함과 공휴일을 표기하기 위해 결국 해당 라이브러리 또한 직접 코드를 작성해야한다는 점 때문에
문득 생각이 들었습니다.
그렇다면?
결국 해당 라이브러리 분석할 시간에 차라리 직접 만들어서 사용하는게 더 효율이 있지 않을까?
그래서 이번 글에서는 React Native를 사용하여 캘린더를 직접 구현하는 방법에 대해 설명해볼려고 합니다.
초기 상태 설정
우선 초기 상태를 위해 useState와 useEffect를 활용해야합니다.
useState를 사용하는 이유는 특정 데이터를 저장하여 사용하기 위해서지만, useEffect는 특정 공휴일들을 다 외울 수도 없으며
또한, 양력이 아닌 음력인 공휴일도 존재하기 때문에 공공포털에서 제공하는 API를 사용하기 위해 useEffect를 사용하게 됩니다.
공공포털 API 사용 방법에 대해서는 혹시나 활용법에 대해 모르시는 분들을 위해 추후에 따로 글을 작성하도록 하며, 이번 내용에서는 사용할 수 있다는 가정하에 진행하도록 하겠습니다.
저는 4개의 값을 만들었습니다.
1. currentMonth
2. selectedDay
3. specificDates
4. checkDate
const [currentMonth, setCurrentMonth] = useState(new Date()); //현재 월
const [selectedDay, setSelectedDay] = useState(null); // 선택한 날짜
const [specificDates, setSpecificDates] = useState([]); // 특정 날짜
const [checkDate, setCheckDate] = useState(''); // 선택한 날짜 포맷 'YYYY-MM-DD'
API호출을 통한 공휴일 데이터 가져오기
useEffect를 사용하여 컴포넌트가 마운트될 때 공공 API에서 공휴일 데이터를 가져옵니다.
const GetDate = async () => {
try {
// API 호출 및 데이터 처리
const response = await axios.get(encodedUrl);
const items = response.data?.response.body.items.item;
const locdateArray = items.map(item => formatDate(item.locdate));
setSpecificDates(locdateArray);
} catch (error) {
console.error('Error during API call', error);
}
};
useEffect(() => {
GetDate();
}, []);
해당 API를 호출하여 axios로 호출 한 뒤 찾고자 하는 데이터를 가져와 처음에 설정 해둔 specificDates에 상태값을 저장해줍니다.
( 해당 API를 사용하기 위해 키값이 필요하여 해당 부분은 삭제하였습니다. 참고하신 뒤 꼭 공공포털 사이트에 접속하셔서 API 주소를 받아 사용해주시길 바라겠습니다.)
공공데이터 포털
국가에서 보유하고 있는 다양한 데이터를『공공데이터의 제공 및 이용 활성화에 관한 법률(제11956호)』에 따라 개방하여 국민들이 보다 쉽고 용이하게 공유•활용할 수 있도록 공공데이터(Datase
www.data.go.kr
캘린더 핵심 기능 구현
캘린더에 꼭 필요한 기능들은 무엇이 있을까요?
- 이전달과 다음달 생성
- 캘린더 데이터(날짜) 구조 생성
- 캘린더 날짜에 적용될 텍스트 스타일
- 캘린더 UI 렌더링
- 날짜 선택
제가 생각하였을 땐 일반적으로 사용하게 된다면 해당 4가지 기능은 기본적으로 있어야한다고 생각합니다.
그럼 이 4가지 기능들을 하나하나 알아보도록 하겠습니다.
1. 이전달과 다음달 생성
사용자가 캘린더에서 월을 이동할 수 있도록, goToNextMonth와 goToPreviousMonth 함수를 구현했습니다. 이 함수들은 현재 월의 상태를 업데이트하여 사용자가 이전 또는 다음 월로 쉽게 탐색할 수 있게 합니다.
const goToNextMonth = () => {
setCurrentMonth(
new Date(currentMonth.getFullYear(), currentMonth.getMonth() + 1, 1),
);
};
const goToPreviousMonth = () => {
setCurrentMonth(
new Date(currentMonth.getFullYear(), currentMonth.getMonth() - 1, 1),
);
};
해당 함수는 이전 달과 다음 달을 불러오기 위한 함수이며, 위에서 선언하였던 현재 월 currentMonth에 함수가 실행될 때마다 상태값을 변경시켜줍니다.
2. 캘린더 데이터(날짜) 구조 생성
이 함수는 현재 월에 대한 날짜와 요일 정보를 포함하는 매트릭스(2차원 배열)를 생성합니다.
const generateMatrix = () => {
var matrix = [];
matrix[0] = days; // 요일 헤더 초기화
var year = currentMonth.getFullYear();
var month = currentMonth.getMonth();
var firstDay = new Date(year, month, 1).getDay();
var maxDays = new Date(year, month + 1, 0).getDate();
var counter = -firstDay + 1; // 첫 주의 첫 날짜를 계산
for (var row = 1; row < 7; row++) {
matrix[row] = [];
for (var col = 0; col < 7; col++) {
let cellValue = counter > 0 && counter <= maxDays ? counter : '';
matrix[row][col] = { day: cellValue, isInCurrentMonth: counter > 0 && counter <= maxDays };
counter++;
}
}
return matrix;
};
- matrix의 첫번째 행은 요일을 나타내며, 이후 행들은 각 주의 날짜를 나타냅니다.
- firstDay는 해당 월의 첫번째 날짜가 무슨 요일인지를 나타내며, 이를 통해 캘린더의 시작 위치를 결정해줍니다.
- maxDays는 해당 월의 총 일수를 나타냅니다.
- matrix에 현재 월의 날짜만 보여주는 것이 아닌, 이전과 다음달의 일부 날짜도 포함되어 나타냅니다.
3. 캘린더 날짜 텍스트 스타일
캘린더에 표시할 일요일, 토요일 색상 비교와 공휴일 색상 비교를 위해 예외처리로 작성합니다.
const getTextStyle = (rowIndex, colIndex, item) => {
if (rowIndex !== 0) {
const year = currentMonth.getFullYear();
const month = currentMonth.getMonth() + 1; // 월은 0부터 인덱스가 지정됩니다. 1을 추가하세요
const formattedMonth = month < 10 ? `0${month}` : month;
const formattedDay = item.day < 10 ? `0${item.day}` : item.day;
const fullDate = `${year}-${formattedMonth}-${formattedDay}`; // 'YYYY-MM-DD'와 일치하도록 조정
let textStyle = item.isInCurrentMonth
? colIndex === 0
? styles.cellTextRed
: colIndex === 6
? styles.cellTextBlue
: styles.cellText
: colIndex === 0
? {...styles.cellTextRed, ...styles.cellTextGrayOpacity}
: colIndex === 6
? {...styles.cellTextBlue, ...styles.cellTextGrayOpacity}
: {...styles.cellTextGray, ...styles.cellTextGrayOpacity};
if (item.isInCurrentMonth && specificDates.includes(fullDate)) {
textStyle = {...textStyle, ...styles.specificDate};
}
if (item.day === selectedDay && item.isInCurrentMonth) {
textStyle = styles.selectedDay;
}
return textStyle;
} else {
return colIndex === 0
? styles.headerTextRed
: colIndex === 6
? styles.headerTextBlue
: styles.headerText;
}
};
getTextStyle 함수는 세 개의 매개변수를 받습니다.
rowIndex (행 인덱스), colIndex (열 인덱스), 그리고 item (해당 셀의 날짜 정보를 담은 객체)
- rowIndex 행의 0번째가 요일을 표시하기 때문에 if문을 활용하여 요일 헤더 스타일링를 해줍니다.
예를 들어 일요일과 토요일은 빨간색과 파란색으로 표시합니다. - 헤더 스타일링으로 일요일과 토요일의 색상을 변경되었다면, 동일하게 날짜 또한 빨간색과 파란색으로 보여주기 위해 날짜도 스타일링을 해줍니다. 여기서 colIndex를 활용하여 일요일인 0번과 토요일인 6번에 색상을 줍니다.
하나의 행에 총 7개의 열이 존재하며, 컴퓨터 언어의 시작은 0부터 임으로 0~6으로 확인할 수 있습니다. - 특정 날짜 또는 공휴일 또한 스타일링을 해주기 위해 공공포털 API의 데이터를 담아두었던 specificDates 배열 가져와
캘린더에 생성된 날짜와 해당 배열에 존재하는 날짜를 비교하여 스타일링을 해줍니다. - 생성된 날짜에서 선택한 날짜를 표시하기 위해 selectedDay에 담은 데이터를 비교하여 스타일링을 해줍니다.
4. 캘린더 UI 렌더링
위에 작성된 생성된 구조 generateMatrix() 함수와 getTextStyle() 함수를 활용하여 캘린더 UI를 구성합니다.
const renderCalendar = () => {
var matrix = generateMatrix();
var rows = matrix.map((row, rowIndex) => {
var rowItems = row.map((item, colIndex) => {
const textStyle = getTextStyle(rowIndex, colIndex, item); // 날짜 스타일 결정
return (
<TouchableOpacity
style={styles.cell}
key={colIndex}
onPress={() => handleDayPress(item.day, item.isInCurrentMonth)}>
<Text style={textStyle}>{item.day}</Text>
</TouchableOpacity>
);
});
return <View style={styles.row} key={rowIndex}>{rowItems}</View>;
});
return <View style={styles.calendar}>{rows}</View>;
};
- matrix.map()을 사용하여 각 행을 순회하고, 각 행의 날짜에 대해 row.map()을 사용하여 순회합니다.
- 각 날짜는 TouchableOpacity로 래핑하여 날짜 선택과 선택한 날짜의 데이터를 가져오기 위해 클릭이벤트 처리를 해줍니다.
- getTextStyle 함수는 날짜의 스타일을 결정하는데 사용이되며, 여기서는 날짜의 현재 월 소속 여부, 특정 날짜 스타일, 선택된 날짜 스타일 등을 처리하기 위해 사용되었습니다.
5. 날짜 선택
캘린더에서 특정 날짜를 선택할때 호출되며, 선택 로직은 사용자가 현재 월의 날짜를 선택 했는지, 아니면 이전/다음 월의 날짜를 선택했는지에 따라 데이터가 달라지게 세팅해줍니다.
예를 들어 24년2월달을 기준으로 29일이 목요일일 경우 금요일과 토요일은 3월 1일과 2일이 표시가 됩니다.
여기서 2월달 캘린더 화면에서 3월 2일을 선택 하였을 때 2일을 선택 했다가 아닌 3월 2일을 선택 했다는걸 인식을 시켜 3월달로 이동시켜줍니다.
const handleDayPress = (day, isInCurrentMonth) => {
const year = currentMonth.getFullYear();
const month = currentMonth.getMonth();
if (!isInCurrentMonth) {
const isNextMonth = day < 15;
const newMonth = isNextMonth ? month + 1 : month - 1;
const newYear = newMonth < 0 ? year - 1 : newMonth > 11 ? year + 1 : year;
const adjustedMonth = (newMonth + 12) % 12;
const newCurrentMonth = new Date(newYear, adjustedMonth, 1);
setCurrentMonth(newCurrentMonth);
const formattedMonth =
adjustedMonth < 9 ? `0${adjustedMonth + 1}` : adjustedMonth + 1;
const formattedDay = day < 10 ? `0${day}` : day;
const formattedDate = `${newYear}-${formattedMonth}-${formattedDay}`;
setSelectedDay(day);
setCheckDate(formattedDate);
} else {
const formattedMonth = month < 9 ? `0${month + 1}` : month + 1;
const formattedDay = day < 10 ? `0${day}` : day;
const formattedDate = `${year}-${formattedMonth}-${formattedDay}`;
setSelectedDay(day);
setCheckDate(formattedDate);
}
};
- isInCurrentMonth 변수는 선택된 날짜가 현재 표시되는 월에 속하는지 여부판단을 해줍니다.
- 현재 월에서 선택된 경우, 단순하게 setSelectedDay와 setCheckDate에 상태값을 변경시켜줍니다.
selectedDay와 checkDate의 차이점은 selectedDay는 선택한 날짜값만 저장시켜주며, checkDate는 한번 포맷을 시켜
'YYYY-MM-DD' 형식으로 저장시킵니다. - 만약, 다른 월에 존재하는 날짜를 선택할 경우, 새로운 currentMonth 상태를 설정한 뒤 선택된 날짜를 해당 월에 맞게 조정시켜줍니다.
최종 코드
import React, {useState, useEffect} from 'react';
import {
View,
Text,
TouchableOpacity,
StyleSheet,
SafeAreaView,
} from 'react-native';
// 캘린더 헤더
const days = ['일', '월', '화', '수', '목', '금', '토'];
// 월 표기
const months = [
'01',
'02',
'03',
'04',
'05',
'06',
'07',
'08',
'09',
'10',
'11',
'12',
];
const CalendarModal = props => {
// 상태 값 useState
// 공공포털 Api GetDate()
const goToNextMonth = () => {
// goToNextMonth 코드
};
const goToPreviousMonth = () => {
// goToPreviousMonth 코드
};
const handleDayPress = (day, isInCurrentMonth) => {
// handleDayPress 코드
};
const generateMatrix = () => {
// generateMatrix 코드
};
const renderCalendar = () => {
// renderCalendar 코드
};
const getTextStyle = (rowIndex, colIndex, item) => {
// getTextStyle 코드
};
return (
<SafeAreaView style={styles.bg}>
<View style={styles.container}>
<View style={styles.header}>
<TouchableOpacity onPress={goToPreviousMonth}>
<Text style={styles.monthLabel}>< Prev</Tes>
</TouchableOpacity>
<Text style={styles.monthLabel}>
{currentMonth.getFullYear()}.
{months[currentMonth.getMonth()]}
</Text>
<TouchableOpacity onPress={goToNextMonth}>
<Text style={styles.monthLabel}>Next ></Text>
</TouchableOpacity>
</View>
<View style={styles.calendar}>{renderCalendar()}</View>
</View>
</SafeAreaView>
);
};
const styles = StyleSheet.create({
bg: {
backgroundColor: 'rgba(0,0,0,0.3)',
position: 'absolute',
height: '100%',
flex: 1,
width: '100%',
display: 'flex',
justifyContent: 'center',
},
container: {
backgroundColor: '#FFF',
borderRadius: 8,
width: '90%',
marginLeft: '5%',
marginRight: '5%',
},
header: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
width: '90%',
marginLeft: '5%',
marginRight: '5%',
padding: 20,
paddingBottom: 10,
},
calendar: {
width: '90%',
marginLeft: '5%',
marginRight: '5%',
height: 280,
justifyContent: 'space-between',
},
monthLabel: {
fontSize: 18,
color: '#000',
},
row: {
flexDirection: 'row',
},
cell: {
flex: 1,
height: 40,
justifyContent: 'center',
alignItems: 'center',
},
headerText: {
color: '#000',
},
headerTextRed: {
color: '#FF0000',
},
headerTextBlue: {
color: '#007BA4',
},
cellText: {
color: '#000',
},
cellTextRed: {
color: '#FF0000',
},
cellTextBlue: {
color: '#007BA4',
},
cellTextGray: {
color: '#0000004D',
},
selectedDay: {
backgroundColor: '#E6EEF5',
textAlign: 'center',
lineHeight: 40,
color: '#000',
height: 40,
width: 40,
borderRadius: 20,
overflow: 'hidden',
},
cellTextGrayOpacity: {
opacity: 0.3,
},
specificDate: {
color: '#FF0000',
},
});
최종 코드 관련하여 주석처리를 한 이유는 열심히 만든 코드를 한번이라도 읽어줬으면 좋겠다는 의미로 표시하였으며,
그래도 열심히 따라하신 분들을 위해 최종적으로 캘린더는 확인할 수 있도록 스타일 코드는 표기해두었습니다.
결과
결론
이전에 작성하였던 React Native Calendars 라이브러리와 다르게 직접 작성한 캘린더라 편하게 커스텀이 가능하며,
추가적인 기능을 손 쉽게 제작과 접근이 가능하다는 점에서 좋은 것 같습니다.
다만, 기능을 추가 하기 위한 과정이 번거롭고 어려운 점이 있지만, 다양한 기능을 접목 시킬 수 있다는 점이 장점일 것 같습니다.
또한, 해당 작업을 하게 된 계기는 React Native에서 캘린더를 제작한 사람들 중 공휴일과 일요일, 토요일 같은 특정 일자를 표시하는 방법에 대한 내용을 찾기가 힘들다보니 직접 만들어야겠다 싶어 이번 계기에 제작하게 되어 코드 상 이해가 안되는 부분들도 존재할 수 있습니다.
궁금한 부분이 있을 경우 댓글을 달아주세요.
React-Native-Calendars 라이브러리를 활용하는 방법에 대해 궁금하다면 해당 페이지로 이동해주세요.
RN에서 캘린더 기능 쉽게 추가하기 - React-Native-Calendars 라이브러리 활용 가이드
React Native Calendars란? React Native Calendars는 React Native 애플리케이션을 위한 다양한 캘린더 컴포넌트를 제공하는 오픈 소스 라이브러리입니다. 월간 뷰, 주간 뷰, 날짜 마킹, 사용자 정의 일 컴포넌트
ha-jenong.tistory.com