아래는 네이버 앱 메인화면 입니다.
모든 언어의 공부는 클론코딩부터 시작이다. 대학생 시절 노마드 코더나 유튜브에 돌아다니는 클론코딩을 했던 경험이 많았습니다. ReactNative 를 첫 시작하는 초보개발자분들이 있다면 오픈소스 라이브러리의 사용법이나 ReactNative가 이런것이구나 하고 익히는데 캘린더만한 앱이 없을 것 같고, 저도 집에서 천천히 복습할겸 시작하게된 프로젝트입니다.
환경
- react-native 0.72.3
- react-native-calendars
- react-redux
calendarUtils.js
/**
* @title Date 객체를 스트링 형태의 날짜로 변환해줍니다.
* @param date
* @returns {string}
*/
export function getFormattedDate(date) {
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
return `${year}-${month}-${day}`;
}
/**
* 스트링 형태의 날짜를 주면 분리해서 json으로 만들어줌.
* @param dateString
* @returns {{month: *, year: *, day: *}}
*/
export function parseDateString(dateString) {
const [year, month, day] = dateString.split('-');
return {
year,
month: String(parseInt(month)), // 단일 자리 월에 0을 붙이지 않도록 변환
day: String(parseInt(day)) // 단일 자리 일에 0을 붙이지 않도록 변환
};
}
가장 많이 사용하는 오픈소스 라이브러리를 사용해서 최대한 네이버 캘린더와 비슷하게 한번 만들어보려 합니다.
무려 8.6K의 깃허브 Stars!!
https://wix.github.io/react-native-calendars/docs/
사용자가 선택한 날짜는 앱 자체에서 전역적으로 관리하면서 다른 기능에도 부가적으로 사용해야될것 같아 ReduxStore에서 관리하도록 처리했습니다.
MainCalendar.js
export default function MainCalendar() {
const dispatch = useDispatch();
const windowWidth = Dimensions.get('window').width;
let currentMonth;
return (
<>
<CalendarHeader />
<CalendarList
onDayPress={day => {
dispatch(selectedDate(day.dateString))
}}
customHeader={({current})=>{
//현재 헤더의 년월 정보를 변수에 저장해놓고 Day컴포넌트르 만들때 사용할게요.
currentMonth = parseDateString(current);
}}
headerStyle={{height: 200}}
dayComponent={(data) => {
return (
<Day data={data} currentMonth ={currentMonth}/>
)
}}
//월이 바뀔경우 바뀐 월의 1일을 선택합니다.
onMonthChange={month => {
// console.log("바뀜")
const parseString = parseDateString(month.dateString);
dispatch(selectedDate(`${parseString.year}-${parseString.month < 10 ? `0${parseString.month}` : `${parseString.month}`}-01`))
}}
hideExtraDays={false}
horizontal={true}
pagingEnabled={true}
disabledByDefault={true}
calendarWidth={windowWidth}
/>
</>
);
}
handleOnPressDay() 함수는 각 컴포넌트별로 이벤트가 모두 생성되기때문에 UseCallBack을 통해 메모이제이션 해두고 캐싱하도록 처리해 컴포넌트 성능을 개선했습니다.
currentMonth는 캘린더가 첫 화면을 렌더링할때 렌더링하는 년,달 입니다.
해당 매게변수에 저장되있는 값으로 이전달인지 다음달인지 확인해서 컴포넌트 Text Color를 결정합니다. (아래 코드 부분을 보시면됩니다.)
customHeader={({current})=>{
//현재 헤더의 년월 정보를 변수에 저장해놓고 Day컴포넌트르 만들때 사용할게요.
//캘린더가 생성될때마다 생성되는 날짜 정보를 변수에 저장해서 사용함.
currentMonth = parseDateString(current);
}}
MainCalendar.js
const Day = ({ data ,currentMonth}) => {
const todayDate = getFormattedDate(new Date());
const dayDate = {
year: data.date.year.toString(),
month: data.date.month.toString(),
day: data.date.day.toString(),
};
const dispatch = useDispatch();
const selected = useSelector(state => state.selectedDate);
//현재 선택되어있는 날짜 flag
const isSelected = selected === data.date.dateString;
//현재 생성하는 Day 컴포넌트의 월과 헤더 타이틀에 표시되는 월이 동일한가? 동일하지 않으면 Gray 처리 해야함.
const isSameMonth = currentMonth.month === dayDate.month;
//현재 생성하는 Day컴포넌트가 오늘인가? 오늘이라면 검은색으로 표시해줘야함
const isToday = todayDate === data.date.dateString;
//컴포넌트 Text Color
const textColor = isSameMonth ? (isToday ? 'white' : 'black') : 'gray';
//해당 함수는 각 컴포넌트마다 모두 똑같기 때문에 UseCallBack에 넣어놓고 사용하자.
const handleOnPressDay = useCallback(({ date }) => {
dispatch(selectedDate(date.dateString));
}, [dispatch]);
return (
<TouchableOpacity
activeOpacity={1}
style={[{ height: 40, paddingLeft: 8, paddingRight: 8 }]}
onPress={() => handleOnPressDay(data)}>
<View
style={[
styles.day,
isSelected && styles.selectedDay,
isToday && styles.today,
]}>
<Text
style={[
globalFonts.fontSemiBold,
{
fontSize: 13,
color: textColor,
},
]}>
{data.date.day}
</Text>
</View>
</TouchableOpacity>
);
};
action.js
// Action Types
export const SELECTED_DATE = 'SELECTED_DATE';
// Action Creators
export const selectedDate = (state) => {
return {
type: SELECTED_DATE,
state : state
};
};
1.헤더에서도 해당 년월을 보여 줘야하는 요구사항이 있어 store에 저장되어있는 선택된 날짜를 가져와 관리합니다.
const CalendarHeader = React.memo(({}) => {
const selected = useSelector(state => state.selectedDate);
const parts = selected.split('-');
const year = parts[0];
const month = parseInt(parts[1]);
return(
<View style = {[styles.calendarHeaderView,{height:70}]}>
<View style = {{flex:1}}>
<Text style = {[globalFonts.fontBold,{fontSize:20}]}>{`${year}. ${month}`}</Text>
</View>
<View style = {{flex:1,flexDirection:'row',alignItems:'flex-end',marginBottom:4}}>
<Text style = {[styles.dayOfWeek,{color:'red',marginRight:1}]}>일</Text>
<Text style = {[styles.dayOfWeek]}>월</Text>
<Text style = {[styles.dayOfWeek]}>화</Text>
<Text style = {[styles.dayOfWeek]}>수</Text>
<Text style = {[styles.dayOfWeek]}>목</Text>
<Text style = {[styles.dayOfWeek]}>금</Text>
<Text style = {[styles.dayOfWeek,{color:'blue'}]}>토</Text>
</View>
</View>
)
})
import {StyleSheet, Text, TouchableOpacity, View} from 'react-native';
import React, {useState, useMemo, useEffect, useCallback} from 'react';
import {Dimensions} from 'react-native';
import {CalendarList} from "react-native-calendars";
import {globalFonts} from "../styles/globalStyles";
import {getFormattedDate, parseDateString} from '../utils/calendarUtils'
import {selectedDate, selectedPrevDate} from "../modules/actions/actions";
import {Provider, useDispatch, useSelector} from 'react-redux'
const todayDate = getFormattedDate(new Date());
export default function MainCalendar() {
const dispatch = useDispatch();
const windowWidth = Dimensions.get('window').width;
let currentMonth;
return (
<>
<CalendarHeader />
<CalendarList
onDayPress={day => {
dispatch(selectedDate(day.dateString))
}}
customHeader={({current})=>{
//현재 헤더의 년월 정보를 변수에 저장해놓고 Day컴포넌트르 만들때 사용할게요.
currentMonth = parseDateString(current);
}}
dayComponent={(data) => {
return (
<View style = {{height:40}}>
<Day data={data} currentMonth ={currentMonth}/>
</View>
)
}}
//월이 바뀔경우 바뀐 월의 1일을 선택합니다.
onMonthChange={month => {
//오늘 날짜가 포함된 달이 아닌경우에만 1을 선택함
if (month.dateString !== todayDate) {
const parseString = parseDateString(month.dateString);
dispatch(selectedDate(`${parseString.year}-${parseString.month < 10 ? `0${parseString.month}` : `${parseString.month}`}-01`))
}
}}
hideExtraDays={false}
horizontal={true}
pagingEnabled={true}
// disabledByDefault={true}
calendarWidth={windowWidth}
/>
</>
);
}
const Day = ({ data ,currentMonth}) => {
const dayDate = {
year: data.date.year.toString(),
month: data.date.month.toString(),
day: data.date.day.toString(),
};
const dispatch = useDispatch();
const selected = useSelector(state => state.selectedDate);
//현재 선택되어있는 날짜 flag
const isSelected = selected === data.date.dateString;
//현재 생성하는 Day 컴포넌트의 월과 헤더 타이틀에 표시되는 월이 동일한가? 동일하지 않으면 Gray 처리 해야함.
const isSameMonth = currentMonth.month === dayDate.month;
//현재 생성하는 Day컴포넌트가 오늘인가? 오늘이라면 검은색으로 표시해줘야함
const isToday = todayDate === data.date.dateString;
//컴포넌트 Text Color
const textColor = isSameMonth ? (isToday ? 'white' : 'black') : 'gray';
//해당 함수는 각 컴포넌트마다 모두 똑같기 때문에 UseCallBack에 넣어놓고 사용하자.
const handleOnPressDay = useCallback(({ date }) => {
dispatch(selectedDate(date.dateString));
}, [dispatch]);
return (
<TouchableOpacity
activeOpacity={1}
style={[{flex:1, minWidth:50,alignItems:'center'}]}
onPress={() => handleOnPressDay(data)}>
<View
style={[
styles.day,
isSelected && styles.selectedDay,
isToday && styles.today,
]}>
<Text
style={[
globalFonts.fontSemiBold,
{
fontSize: 13,
color: textColor,
textAlign: 'center',
},
]}>
{data.date.day}
</Text>
</View>
</TouchableOpacity>
);
};
const CalendarHeader = React.memo(({}) => {
const selected = useSelector(state => state.selectedDate);
const parts = selected.split('-');
const year = parts[0];
const month = parseInt(parts[1]);
return(
<View style = {[styles.calendarHeaderView,{height:70}]}>
<View style = {{flex:1}}>
<Text style = {[globalFonts.fontBold,{fontSize:20}]}>{`${year}. ${month}`}</Text>
</View>
<View style = {{flex:1,flexDirection:'row',alignItems:'flex-end',marginBottom:4}}>
<Text style = {[styles.dayOfWeek,{color:'red',marginRight:1}]}>일</Text>
<Text style = {[styles.dayOfWeek]}>월</Text>
<Text style = {[styles.dayOfWeek]}>화</Text>
<Text style = {[styles.dayOfWeek]}>수</Text>
<Text style = {[styles.dayOfWeek]}>목</Text>
<Text style = {[styles.dayOfWeek]}>금</Text>
<Text style = {[styles.dayOfWeek,{color:'blue'}]}>토</Text>
</View>
</View>
)
})
const styles = StyleSheet.create({
calendarHeaderView: {
paddingLeft: 16,
paddingRight: 16,
paddingTop: 12,
// paddingBottom: 12,
flexDirection:'column',
// flex:1,
},
dayOfWeek : {
flex:1,
minWidth:50,
fontSize:12,
textAlign: 'center',
},
day: {
alignItems: 'center',
justifyContent: 'center',
width: 22,
height: 22,
},
selectedDay: {
borderRadius: 15,
backgroundColor: '#C9C9C9'
},
today: {
borderRadius: 15,
backgroundColor: 'black'
}
});
좀 비슷한가요? 시간이 된다면 하단 상세 일정 정보와 메뉴 네비게이터도 만들어서 올려보겠습니다!
좋은 글이네요. 공유해주셔서 감사합니다.