본문 바로가기

모바일 개발/React Native-이론

캘린더에 to do list 만들기, 컴포넌트 정리

<개요>

먼저 캘린더 밑 부분에 To do List를  flatList로서 넣는다. 

캘린더와 to do List가 따로 Scroll 된다면 그것도 사용자에게 불편함을 줄 것이다. 

그래서 하나의 FlatList에 캘린더와 Scroll이 같이 있게 만들어줘야 한다. 

 

(1)따라서 지금까지 만들었던 캘린더 로직을 하나의 파일로 따로 빼서, toDoList FlatList의 ListHeaderComponent로 넣어줄 것이다. 

 

(2) 그리고 화면 뒤에 배경 이미지도 넣어줄 것이다. 

이때 화면과 View 가 겹칠 때 누가 앞에 뜰 것인지 우선순위를 정하는 것이 중요하다.

여기서는 CSS를 이용하여 두 태그간의 우선순위를 나눌 것인데, position과 zindex가 쓰인다.

 

(3) 마지막으로 밑에 todo를 추가할 수 있는 textInput 태그를 넣을 것이다.

여기서 중요한 것은 인풋을 눌러서 디바이스 장치의 Keyboard 부분이 올라왔다면  인풋 부분도 같이 올라오게 해야한다는 것이다. 키보드 장치에 가려서 내가 뭘 적고 있는지 안 보여서는 안된다.

 

 

1. 캘린더 로직 정리

App.js에 적혀 있던 캘린더 로직

flatList(column, renderItem, ListHeaderComponent)를 다른 파일(Calendar.js)에 옮기고, 해당 Flatlist를 App.js에서 수입하여 쓸 수 있도록 하는 것이다. 

 

우리는 이 Calendar.js 를 수입하여 todoList Flatlist의 HeaderComponent로 집어넣을 것이다. 

 

<Header Component로 집어넣는 이유? >

하나의 스크롤 안에 두 덩이의 내용이 같이 움직이도록 하기 위함이다. 캘린더는 스크롤이 다르고 todoList도 스크롤이 다르다면, 사용자에게 복잡함과 불편감을 준다. 

 

(1)로직 정리 설계도 

 

(2) 해야하는 작업 순서

a. App.js에 있는 Calendar 구현에 쓰인 FlatList와 그 내부 컴포넌트 모두 Calendar.js 라는 파일을 만들어 옮기기.

b. 필요한 값들 인수로 받기. App.js에서는 해당 값들을 인수로 보내서 연결 시켜주면 된다.

import dayjs from "dayjs";
import React from "react";
import { View,FlatList, TouchableOpacity, Text } from "react-native";
import { getStatusBarHeight } from "react-native-iphone-x-helper";
import {SimpleLineIcons} from "@expo/vector-icons";
import { getDayColor, getDayText } from "./util";


const statusBarHeight = getStatusBarHeight(true);

const columnSize = 35;


const Column = ({
  text,
  color,
  opacity,
  disabled,
  onPress,
  isSelected,
}) => {
  return (
    <TouchableOpacity
      disabled={disabled}
      onPress={onPress}
      style={{ 
        width: columnSize, 
        height: columnSize, 
        justifyContent: "center", 
        alignItems: "center",
        backgroundColor: isSelected ? "#c2c2c2" : "transparent",
        borderRadius: columnSize / 2,
        }}>
          <Text style={{color, opacity}}>{text}</Text>
    </TouchableOpacity>
  )
}
const ArrowButton = ({ iconName, onPress }) => {
  return (
    <TouchableOpacity onPress={onPress} style={{ paddingHorizontal: 20, paddingVertical: 15 }}>
      <SimpleLineIcons name={iconName} size={15} color="#404040" />
    </TouchableOpacity>
  )
}


export default ({
  columns,
  selectedDate,
  onPressLeftArrow,
  onPressRightArrow,
  onPressHeaderDate,
  onPressDate,

}) => {

  const ListHeaderComponent = () => {
    const currentDateText = dayjs(selectedDate).format("YYYY.MM.DD.");
    return (
      <View>
        {/* < YYYY.MM.DD. > */}
        <View style={{ flexDirection: "row", justifyContent: "center", alignItems: "center" }}>
          <ArrowButton iconName="arrow-left" onPress={onPressLeftArrow} />
          <TouchableOpacity onPress={onPressHeaderDate}>
            <Text style={{ fontSize: 20, color: "#404040" }}>{currentDateText}</Text>
          </TouchableOpacity>
          <ArrowButton iconName="arrow-right" onPress={onPressRightArrow} />
        </View>
  
        {/* 일 ~ 토 */}
        <View style={{ flexDirection: "row" }}>
          {[0, 1, 2, 3, 4, 5, 6].map(day => {
            const dayText = getDayText(day);
            const color = getDayColor(day);
            return (
              <Column
                key={`day-${day}`} 
                text={dayText} 
                color={color} 
                opacity={1} 
                disabled={true}
              />
            )
          })}
        </View>
      </View>
    )
  }
  const renderItem = ({ item: date }) => {
    const dateText = dayjs(date).get('date');
    const day = dayjs(date).get('day');
    const color = getDayColor(day);
    const isCurrentMonth = dayjs(date).isSame(selectedDate, 'month');
    const onPress = () => onPressDate(date) //이 Calendar.js  가 받은 onPressDate라는 함수에 date라는 renderItem의 인수를 넣어준다.
    const isSelected = dayjs(date).isSame(selectedDate, 'date');
    return (
      <Column
        text={dateText} 
        color={color} 
        opacity={isCurrentMonth ? 1 : 0.4} 
        onPress={onPress}
        isSelected={isSelected}
      />
    )
  }

  return (
      <FlatList
      data={columns}
      scrollEnabled={false}
      keyExtractor={(_, index) => `column-${index}`}
      numColumns={7}
      renderItem={renderItem}
      ListHeaderComponent={ListHeaderComponent}
    />
  )
}
  ListHeaderComponent = () => {
    return(
      <View>
        <Calendar
          columns={columns}
          selectedDate ={selectedDate}
          onPressLeftArrow ={onPressLeftArrow}
          onPressHeaderDate ={onPressHeaderDate}
          onPressRightArrow ={onPressRightArrow}
          onPressDate = {onPressDate}
        />
        <Margin height={10}/>

C. Calendar.js 속의 FlatList는 ScrollEnable = {false}로 지정하여 스크롤 되는 것 막기 

이유? : TodoList를 위한 FlatList 안에 HeaderComponent로 넣을 건데, 이건 스크롤 되는 대상을 하나로 통일하기 위함이었음. 따라서 스크롤 되는 리스트 안의 스크롤 되는 리스트가 하나 더 생기는 것을 막기 위함.

D. 파일이 달라져서 인수로 받아야 하는 값들 받아오기 (재명칭) 

원래 App.js 라는 같은 파일에 있었을 시, onPressHeaderDate 대신에 ShowDayTimePicker를 직접 썼고, OnPressDate 대신에 setSelectedDate를 직접 썼음. 하지만 파일이 달라지면서 ShowDayTimePicker를 OnPressHeaderDate라는 인수로 받고, setSelectedDate를 OnpressDate라는 인수로 받았음. 

 

이유는 파일이 달라지면서  ShowDayTimePicker나 setSelectedDate라는 함수의 이름을 달라진 Calendar.js에서도 똑같이 써버리면, 가독성이 떨어지고, 다른 개발자가 열어보거나, 시간이 지나서 열게 되면 무슨 내용인지 알 수 없어지는 일이 ㅇ생길 수 있으므로, 바꿨다.

 

E. Calendar.js 자체를 todoList FlatList의 HeaderComponent로 넣기.

 

2. TODO List FlatList 만들기 

ListHeaderComponent는 만들었으니 renderItem을 구현함.

 

(1)renderItem

item으로 받은 to do List는 객체들이 나열된 배열이다. 

renderItem은 낱개로 들어온 객체들의 멤버를 객체.member로 불러내서 사용하고 있고, 

여기서 isSuccess와 content를 불러내 사용하고 있다.

isSuccess는 true일때 false일때, 즉 완료 되었을 때 아닐 때를 삼항연산자로 표현하여 구분하였다. 

다른 별 다른 내용은 아직 없고, 전부 styling이다. 

 

3. 사용자 Input 칸 만들기

 placeholder에는 backsheet를 줘서 사용자가 누른 날짜 값으로 placeholder 내용이 바뀌도록 설정해놓았다.

onChangeText는 Value 값이 바뀌었을 때 활성화되는 속성이다. 

해당 경우를 보면 View라는 큰 태그 안에 TextInput과 Icon 태그가 있는 것을 볼 수 있다. 

 

여기서 TextInput의 style에 flex:1 을 설정한 것을 알 수 있다. 저번에도 말했듯이 flex: 1 은 flex-basis를 0으로 두고, grow와 shrink를 1로 둔다. 이것의 의미는 고정 너비 없이 화면의 크기에 맞게 비율을 조정하겠다는 뜻이다. 

grow와 shrink에 적힌 값이 클수록 형제들 사이에서 늘어나거나 줄어드는 우선권을 가진다. 

TextInput은 Icon과 형제인데, Icon 태그는 딱히 flex 속성이 없으므로 default 값이 고정너비로 너비가 계산된다. 

따라서 부모 공간의 나머지 여백은 TextInput이 다 가진다.

(1) 입력칸이 Keyboard 위로 올라오도록 만들기 

<KeyBoardAvoidVIew>라는 태그를 사용하면 그 안에 있는 값들은 키보드가 켜질 시 그 키보드 위로 값이 올라간다. 

해당 태그는 안에 자식 태그가 무조건 하나여야 해서 , <View>로 한번 자식 태그들을 감쌌다.

<KeyBoardAvoidView>를 쓰려면 안에 behavior 속성에 무조건 값을 줘야한다. 

behavior의 내용은 PlatForm이 ios이면 해당 KeyboardAvoidView 동작 시  Padding Bottom에 값을 주어 해당 내용물을 KeyBoard 위로 올리라는 뜻이고, ios가 아니면 태그 styling에 height를 줘서 키보드 작동 시 내용물을 Keyboard 위로 올리라는 뜻이다. 

(2) 화면 중 빈공간 아무 곳이나 터치해도 KeyBoard가 닫히도록 만들기

전체 화면을 감싸는 RootView를 View에서 pressable로 바꾼다. 

**Pressable과 TouchableOpacity의 차이점**

TouchalbeOpacity는 누르는 순간 누른 버튼이 희미해진다. 이는 ActiveOpacity가 기본적으로 설정 되어 있어서 그런 것이다. Opacity란 불투명도로서 0에 가까울 수록 희미해진다. 

우리가 만약 TouchableOpacity에서 누르는 순간의 투명도를 없애주려면 속성의 ActiveOpacity를 1로 만들어주면 된다. 

<TouchableOpacity ActiveOpacity = {1}>

여기서 pressable은 ActiveOpacity가 1로 된 TouchableOpacity와 의미가 같다. 

여기서 우리는 onPress가 Keyboard.dismiss로 되어있는 것을 볼 수 있는데, 이는 눌렀을 시 keyboard를 끄라는 내용이다. 

위와 같이 onPress = {Keyboard.dismiss()}라고 적어줄 수 있는 이유는 Keyboard.dismiss라는 것이 인수가 없는 함수라서 그런 것이다. 

// 둘은 같은 말
// onPress = {() => Keyboard.Dismiss}
onPress = {keyboard.Dismiss}

4. use-Todo-List 내부 

todoList FlatList를 위한 데이터와 함수들이 적혀 있는 파일이다. todo는 id, content, date, isSuccess라는 멤버를 가진 객체 변수이고, 이러한 todo들이 배열에 원소로 차례대로 들어가 있는 형국이다. 

(0)defaultList

우리는 일단 테스트를 위하여 defaultTodoList를 만들어 사용해 보겠다. 

(1) useState인 useTodoList 

todo 객체의 멤버인 date에 값을 넣어주기 위해서 selectedDate를 인수로 받는다. 

todoList useState의 초기값은 일단 defaultTodoList로 정한다. 

input은 사용자가 적은 값이 들어가는 칸이다. 아직  여기에 대해서는 크게 구체화를 시켜놓지 않았다.

(2) addTodo 컴포넌트

List 안에 새로운 todo를 추가하는 함수이다. 

먼저 기존 todoList의 길이를 알아낸다. 

길이가 0이면 LastId는 0이고, 아니라면 Lastid는 맨 마지막 원소의 id값이다. (배열의 원소는 0부터 세는 것을 인지하여 len-1을 했다. 길이 자체는 1부터 센다는 것을 인지하자. )

 

다음은 새로운 newTodoList를 만들고, 거기에 기존 todolist의 원소들을 전개 연산자를 통해 풀어서 써놓고, 

(전개 연산자를 쓰면 배열이 사라지고, 해당 배열의 원소들만 차례대로 나열된다.)

그리고 Lastid에 1을 더한 새로운 객체를 맨 마지막에 집어넣는다.   

그리고 이거를 새로운 상태변수의 값으로 집어넣는다. 

(3) removeTodo 컴포넌트

java에서 stream 쓰는 거랑 똑같다. filter는 해당 조건에서 참인 것만 뽑아서 새로운 배열을 만든다. 

해당 명령문의 의미는 인수로 우리가 빼야할 객체인 todo의 id가 들어왔다고 가정한다. 

그리고 배열 내에서 각 객체 중 해당 인수와 id가 같은 것을 제외하고 새로운 배열을 만든다. 

그리고 그것을 상태변수의 새로운 값으로 할당하는 것이다. 

(4) toggleTodo 컴포넌트 

toggleTodo 컴포넌트는 어떤 todo를 했을 때 안 했을 때 check icon을 토글 시키기 위한 로직이다. 

위에서 살펴보았듯이 checkicon은 isSuccess값에 의해 색깔이 흐려지거나 진해졌다.

 

여기서는 새로운 todolist를 만드는데 map 함수를 사용했다.

map 함수는 원소 하나하나에 특정 기능을 먹여서 새로운 값으로 바꾼 뒤 그 원소들로 새로운 배열을 만드는 기능을 한다. 

예를 들어 [a,b,c,d,e,] .map(각 원소에 하이푼을 붙이는 기능 ) 이면

반환 값은 [a',b',c',d',e']가 된다. map은 filter랑 다르게 이탈자가 없다. 

 

위의 함수의 로직은 todoId가 성공 실패가 최신화되는 객체의 id라고 쳤을 때, todo중 해당 id와 다른 거는 그냥 그대로 넣고,  우리가 찾던 id와 같은 객체일 경우 {} 객체 안에 ...todo로 원소들을 풀어서 적은 다음에 isSuccess를 한번 더 적는다. 

같은 name 값에 대해 Value를 재할당하면, 최근에 적은 값으로 value가 최신화됨을 이용한 것이다.