@ List
* 7.5 달력 기능 구현하기
** 7.5.1 달력에 표시하기
** 7.5.2 데이터를 달력과 연동하기
** 7.5.3 달력 하단에 로그 목록 보여주기
** 7.5.4 useMemo Hook으로 최적화하기
* 7.6 날짜 및 시간 수정 기능 구현하기
** 7.6.1 WriteHeader에서 날짜 및 시간 보여주기
** 7.6.2 DateTimePickerModal 컴포넌트 사용하기
@ Note
1. 유용한 라이브러리
- react-native-calendars : 달력 생성
> reference : https://github.com/wix/react-native-calendars
- react-native-modal-datetime-picker : 날짜/시간 선택 컴포넌트를 모달 형태로 쉽게 사용할 수 있게 함
- @react-native-community/datetimepicker : iOS 및 android 플랫폼에 각각 특화된 날짜/시간 선택 컴포넌트 제공
> reference : https://github.com/react-native-datetimepicker/datetimepicker
2. 내장함수 map(), filter(), reduce() 의 차이
- map : 배열 각 요소에 대하여 주어진 함수를 수행한 결괏값를 모아 새로운 배열을 반환하는 메서드
- filter : 배열 각 요소에 대하여 주어진 함수의 결괏값이 true인 요소를 모아 새로운 배열을 반환하는 메서드
- reduce : 배열 각 요소에 대하여 주어진 함수를 실행하고, 배열이 아닌 하나의 결과값을 반환 (객체 형태로 반환 --> { })
> reference : https://ko.javascript.info/array-methods#ref-5094
3. JSON 객체 key 값 동적 할당
- map : 배열 각 요소에 대
> reference https://kyounghwan01.github.io/blog/JS/JSbasic/jsonDynamicAllocation/
const markedSelectedDates = {
...markedDates,
// key값 동적 할당
[selectedDate]: {
selected: true,
marked: markedDates[selectedDate]?.marked,
},
};
4. <FlatList /> 의 ListHeaderComponent Props 사용
- FliatList의 내용 상단부에 특정 컴포넌트를 보여줄 수 있음
> reference : https://reactnative.dev/docs/flatlist#listheadercomponent
5. 중괄호 여부에 따른 return 생략
- 화살표 함수의 유일한 문장이 'return'일 때, 'return' 과 '중괄호'를 생략할 수 있음
// 생략 전
const filteredLogs = logs.filter(log => {
return format(new Date(log.date), 'yyyy-MM-dd') === selectedDate;
});
// 생략 후
const filteredLogs = logs.filter(
log => format(new Date(log.date), 'yyyy-MM-dd') === selectedDate,
);
6. new Date(), new Date(log.date) 차이
var date1 = new Date(); // 현재 날짜 및 시간
var date2 = new Date(1991,11,25,3,50); // 1991년 12월 25일 3:50:00 (월 +1 주의)
var date3 = new Date('2014-6-4'); // 2002년 1월 1일 09:00:00
var date4 = new Date('2012-05-17 10:20:30'); // 2012년 5월 17일 10:20:30
7. useMemo Hook 의 사용
- 날짜가 변경될 때마다 컴포넌트가 리렌더링되고 markedDates를 생성하는데, markedDate는 변하지 않기 때문에 생성할 필요 없음
- 사용법 : const value = useMemo(() => compute(a, b), [a, b]);
> 아래 처럼 사용 시, logs 배열이 바뀔 때만 logs.reduce 함수가 수행됨
const markedDates = useMemo(
() =>
logs.reduce((acc, current) => {
const formattedDate = format(new Date(current.date), 'yyyy-MM-dd');
acc[formattedDate] = {marked: true};
return acc;
}, {}),
[logs],
);
8. 기타
- 내장함수 toISOString() : 주어진 날짜를 국제표준시 기준 ISO 8601 형식으로 표현
> reference : https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Global_Objects/Date/toISOString
- 스타일 속성 zIndex : 다른 컴포넌트와 위치가 중첩될 때 레이어 위치 결정
> 더 높은 값이 더 낮은 값을 가진 컴포넌트를 가림
@ Result
@ Git
- https://github.com/eunbok-bocoder/DayLog/commits/main
# Source tree
# components > CalendarView.js
import React from 'react';
import {Calendar} from 'react-native-calendars';
import {StyleSheet} from 'react-native';
import {daysInWeek} from 'date-fns';
function CalendarView({markedDates, selectedDate, onSelectDate}) {
// const markedDates = {
// '2022-01-17': {
// selected: true,
// },
// '2022-01-18': {
// marked: true,
// },
// '2022-01-19': {
// marked: true,
// },
// };
const markedSelectedDates = {
...markedDates,
// key값 동적 할당
[selectedDate]: {
selected: true,
marked: markedDates[selectedDate]?.marked,
},
};
return (
<Calendar
style={styles.calendar}
markedDates={markedSelectedDates}
onDayPress={day => {
console.log('day : ', day); // day : {"dateString": "2022-01-05", "day": 5, "month": 1, "timestamp": 1641340800000, "year": 2022}
onSelectDate(day.dateString);
}}
theme={{
selectedDayBackgroundColor: '#009688',
arrowColor: '#009688',
dotColor: '#009688',
todayTextColor: '#009688',
}}
/>
);
}
const styles = StyleSheet.create({
calendar: {
borderBottomWidth: 1,
borderBottomColor: '#e0e0e0',
},
});
export default CalendarView;
# components > FeedList.js
import React from 'react';
import {FlatList, StyleSheet, View} from 'react-native';
import FeedListItem from './FeedListItem';
function FeedList({logs, onScrolledToBottom, ListHeaderComponent}) {
const onScroll = e => {
if (!onScrolledToBottom) {
return;
}
const {contentSize, layoutMeasurement, contentOffset} = e.nativeEvent;
// console.log({contentSize, layoutMeasurement, contentOffset});
const distanceFromBottom =
contentSize.height - layoutMeasurement.height - contentOffset.y;
if (
contentSize.height > layoutMeasurement.height &&
distanceFromBottom < 72
) {
// console.log('바닥과 가까워요.');
onScrolledToBottom(true);
} else {
// console.log('바닥과 멀어졌어요.');
onScrolledToBottom(false);
}
};
return (
<FlatList
data={logs}
style={styles.block}
renderItem={({item}) => <FeedListItem log={item} />}
keyExtractor={log => log.id}
ItemSeparatorComponent={() => <View style={styles.separator} />}
onScroll={onScroll}
ListHeaderComponent={ListHeaderComponent}
/>
);
}
const styles = StyleSheet.create({
block: {flex: 1},
separator: {
backgroundColor: '#e0e0e0',
height: 1,
width: '100%',
},
});
export default FeedList;
# components > WriteHeader.js
import {useNavigation} from '@react-navigation/native';
import {format} from 'date-fns';
import {ko} from 'date-fns/locale';
import React, {useState} from 'react';
import {Pressable, StyleSheet, Text, View} from 'react-native';
import TransparentCircleButton from './TransparentCircleButton';
import DateTimePickerModal from 'react-native-modal-datetime-picker';
function WriteHeader({onSave, onAskRemove, isEditing, date, onChangeDate}) {
const navigation = useNavigation();
const onGoBack = () => {
navigation.pop();
};
const [mode, setMode] = useState('date');
const [visible, setVisible] = useState(false);
// console.log('[LOG] mode / visible : ', mode + ' / ' + visible);
const onPressDate = () => {
setMode('date');
setVisible(true);
};
const onPressTime = () => {
setMode('time');
setVisible(true);
};
const onConfirm = selectedDate => {
setVisible(false);
onChangeDate(selectedDate);
};
const onCancel = () => {
setVisible(false);
};
return (
<View style={styles.block}>
<View style={styles.iconButtonWrapper}>
<TransparentCircleButton
onPress={onGoBack}
name="arrow-back"
color="#424242"
/>
</View>
<View style={styles.buttons}>
{isEditing && (
<TransparentCircleButton
name="delete-forever"
color="#ef5350"
hasMarginRight
onPress={onAskRemove}
/>
)}
<TransparentCircleButton
name="check"
color="#009688"
onPress={onSave}
/>
</View>
<View style={styles.center}>
<Pressable onPress={onPressDate}>
<Text>{format(new Date(date), 'PPP', {locale: ko})}</Text>
</Pressable>
<View style={styles.separator} />
<Pressable onPress={onPressTime}>
<Text>{format(new Date(date), 'p', {locale: ko})}</Text>
</Pressable>
</View>
<DateTimePickerModal
isVisible={visible}
mode={mode}
onConfirm={onConfirm}
onCancel={onCancel}
date={date}
/>
</View>
);
}
const styles = StyleSheet.create({
block: {
height: 48,
paddingHorizontal: 8,
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
},
iconButtonWrapper: {
width: 32,
height: 32,
borderRadius: 16,
overflow: 'hidden',
},
iconButton: {
alignItems: 'center',
justifyContent: 'center',
width: 32,
height: 32,
borderRadius: 16,
},
buttons: {
flexDirection: 'row',
alignItems: 'center',
},
marginRight: {
marginRight: 8,
},
center: {
position: 'absolute',
left: 0,
right: 0,
top: 0,
bottom: 0,
alignItems: 'center',
justifyContent: 'center',
zIndex: -1,
flexDirection: 'row',
},
separator: {
width: 8,
},
});
export default WriteHeader;
# screens > CalendarScreen.js
import {format} from 'date-fns';
import React, {useContext, useMemo, useState} from 'react';
import CalendarView from '../components/CalendarView';
import FeedList from '../components/FeedList';
import LogContext from '../contexts/LogContext';
function CalendarScreen() {
const {logs} = useContext(LogContext);
const [selectedDate, setSelectedDate] = useState(
format(new Date(), 'yyyy-MM-dd'),
);
const markedDates = useMemo(
() =>
logs.reduce((acc, current) => {
const formattedDate = format(new Date(current.date), 'yyyy-MM-dd');
acc[formattedDate] = {marked: true};
// console.log('formattedDate : ', formattedDate); // formattedDate : 2022-01-13
// console.log('acc : ', acc); // acc : {"2022-01-13": {"marked": true}}
return acc;
}, {}),
[logs],
);
// {
// console.log('markedDates : ', markedDates); // markedDates : {"2022-01-13": {"marked": true}}
// }
const filteredLogs = logs.filter(
log => format(new Date(log.date), 'yyyy-MM-dd') === selectedDate,
);
// {
// console.log('filteredLogs : ', filteredLogs); // markedDates : {"2022-01-13": {"marked": true}}
// console.log('selectedDate : ', selectedDate); // markedDates : {"2022-01-13": {"marked": true}}
// }
return (
<FeedList
logs={filteredLogs}
ListHeaderComponent={
<CalendarView
markedDates={markedDates}
selectedDate={selectedDate}
onSelectDate={setSelectedDate}
/>
}
/>
);
}
export default CalendarScreen;
# screens > WriteScreen.js
import {useNavigation} from '@react-navigation/native';
import React, {useContext, useState} from 'react';
import {Alert, StyleSheet, KeyboardAvoidingView, Platform} from 'react-native';
import {SafeAreaView} from 'react-native-safe-area-context';
import WriteEditor from '../components/WriteEditor';
import WriteHeader from '../components/WriteHeader';
import LogContext from '../contexts/LogContext';
function WriteScreen({route}) {
// 옵셔널 체이닝 문법
const log = route.params?.log; // --> route.params ? route.params.log : undefined
const [title, setTitle] = useState(log?.title ?? ''); // --> useState(log ? log.title : '')
const [body, setBody] = useState(log?.body ?? '');
const navigation = useNavigation();
const [date, setDate] = useState(log ? new Date(log.date) : new Date());
const {onCreate, onModify, onRemove} = useContext(LogContext);
const onSave = () => {
if (log) {
onModify({
id: log.id,
// date: log.date,
date: date.toISOString(),
title,
body,
});
} else {
onCreate({
title,
body,
// 날짜를 문자열로 변환
// date: new Date().toISOString(),
date: date.toISOString(),
});
}
navigation.pop();
};
const onAskRemove = () => {
Alert.alert(
'삭제',
'정말로 삭제하시겠어요?',
[
{text: '취소', style: 'cancel'},
{
text: '삭제',
onPress: () => {
onRemove(log?.id);
navigation.pop();
},
style: 'destructive',
},
],
{
cancelable: true,
},
);
};
return (
<SafeAreaView style={styles.block}>
<KeyboardAvoidingView
style={styles.avoidingView}
behavior={Platform.OS === 'ios' ? 'padding' : undefined}>
<WriteHeader
onSave={onSave}
onAskRemove={onAskRemove}
isEditing={!!log}
date={date}
onChangeDate={setDate}
/>
<WriteEditor
title={title}
body={body}
onChangeTitle={setTitle}
onChangeBody={setBody}
/>
</KeyboardAvoidingView>
</SafeAreaView>
);
}
const styles = StyleSheet.create({
block: {
flex: 1,
backgroundColor: 'white',
},
avoidingView: {
flex: 1,
},
});
export default WriteScreen;
'React Native > React Native_study' 카테고리의 다른 글
[리액트 네이티브를 다루는 기술 #15] 8장 Firebase로 사진 공유 앱 만들기1 (p .407 ~ 418) (0) | 2022.01.20 |
---|---|
[리액트 네이티브를 다루는 기술 #14] 7장 다이어리 앱 만들기2 (p .399 ~ 406) (0) | 2022.01.17 |
[리액트 네이티브를 다루는 기술 #12] 7장 다이어리 앱 만들기2 (p .357 ~ 379) (2) | 2022.01.11 |
[리액트 네이티브를 다루는 기술 #11] 6장 다이어리 앱 만들기1 (p.330 ~ 356) (0) | 2022.01.07 |
[리액트 네이티브를 다루는 기술 #10] 6장 다이어리 앱 만들기1 (p.306 ~ 329) (0) | 2022.01.03 |