React Native/React Native_study

[리액트 네이티브를 다루는 기술 #13] 7장 다이어리 앱 만들기2 (p .380 ~ 398)

bocoder
728x90
반응형

@ 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

iOS / android - CalendarView 구현

 

012
iOS / android - CalendarView Cutomizing

 

012
iOS / android - 날짜/시간 변경 구현

 

@ 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;

 

728x90
반응형