Front-End/React Native

[React Native] 리액트 네이티브 To Do 앱 만들기 (AsyncStorage, Map)

현기 2022. 10. 20. 21:12

https://nomadcoders.co/react-native-for-beginners

 

왕초보를 위한 React Native 101 – 노마드 코더 Nomad Coders

React Native로 2개의 앱 만들기

nomadcoders.co

 

프로젝트를 하면서 앱을 개발해야 하는 일이 생겨서

니꼬 선생님의 강의를 듣고 React Native를 사용해

To Do 앱을 만들었습니다.

 

Expo를 사용하면 환경 설정이 간단하고

제공해 주는 API, 컴포넌트들이 많아서

쉽고 빠르게 개발이 가능합니다. 

 


📝 To Do 앱

 

 

⦁ 🧾 기능

1. To Do List / 여행 리스트 추가 삭제

 

2. 체크박스 선택 시 텍스트 줄긋기 및 색 변경 ( 완료 체크 )

 

3. AsyncStorage 사용 ( Local Storage 비슷한 기능 )

 


📝 To Do 추가 / 삭제

 

⦁ To Do 추가

const travle = () => setWorking(false);
const work = () => setWorking(true);
const [checked, setChecked] = React.useState(false); //UI 라이브러리 사용

//Submit
const addToDo = async () => {
if (text === "") {
  return
}

//todo 저장 Object.assign 오브젝트 합치기
const newToDos = { ...toDos, [Date.now()]: { text, working, checked}}
setToDos(newToDos)

//스토리지에 저장
saveToDos(newToDos)

//텍스트 초기화
setText("")
}

 

✔ Object 타입으로 데이터를 저장합니다.

 

State를 직접 사용하면 안되기 때문에 수정하기 위해서는 원본을 copy하고 set 메서드를 사용해야 합니다.

따라서 newToDos를 선언했습니다.

 

✔ES6 문법
const newToDos = {...toDos, [Date.now()] : {text, work: working}}
...toDos는 ES6 문법인데 Object.assign과 동일하게 기존 오브젝트에 이어붙일 수 있습니다.

 

 

⦁ To Do 삭제

To Do 삭제
const deleteToDo = (key) => {
Alert.alert("Delete To Do", "Are you sure?", [
  { text: "Cancel" },
  {
    text: "I'm Sure", onPress: async () => {
      const newToDos = { ...toDos }
      delete newToDos[key]
      setToDos(newToDos)
      await saveToDos(newToDos)
    }
  }
]);
}

✔ 네이티브팀의 Alert.alert을 사용해서 사용자에게 확인 메세지를 보냅니다.

Alert.alert("제목", "확인 메시지", [ 버튼1, 버튼2 ] )

 

⦁ To Do 객체 Map으로 표시하기

<ScrollView>
        {
          Object.keys(toDos).map((key) => (
           <View style={styles.toDo} key={key}>
            <Text style={styles.toDoText} >{toDos[key].text}</Text>
          </View>
        ))}
</ScrollView>

 

✔ keys() 메서드를 사용해서 오브젝트의 key값만 추출하고,

map을 사용해서 반복합니다. (for문과 비슷합니다.)

 

 

https://reactnative.dev/docs/0.60/scrollview

네이티브 팀에서 제공하는 스크롤 뷰를 사용하면

화면을 위 아래로 스크롤 할 수 있습니다.

 


📝 체크박스 선택 시 완료 기능

 

https://callstack.github.io/react-native-paper/checkbox.html

 

Checkbox · React Native Paper

Checkboxes allow the selection of multiple options from a set.

callstack.github.io

✔ 체크박스는 리액트 네이티브 UI 라이브러리react-native-paper 를 사용했습니다.

사용하기 편리하고 디자인이 예뻐요 !

 

⦁ 소스코드

  //Todo 체크 메서드
  const checkToDo = async (key) => {
    const newToDos = { ...toDos }
    const temp = newToDos[key].checked

    newToDos[key].checked = !temp

    setToDos(newToDos)
    await saveToDos(newToDos)
  }

✔ 체크 선택시 오브젝트의 checked 값을 반대로 변경해줍니다.

 

 

// style안에도 삼항연산자 가능
<Text style={{...styles.toDoText, 
  textDecorationLine: toDos[key].checked? 'line-through' : null,
  color : toDos[key].checked ? 'green': 'white'}} >
  {toDos[key].text}
</Text>

style 안에도 위처럼 삼항연산자가 가능합니다.

체크가 되어있으면 글자 색을 변경해주고 line-through CSS를 적용해 선을 긋습니다.

 

 


📝 AsyncStorage

 

https://react-native-async-storage.github.io/async-storage/docs/usage/

 

Usage | Async Storage

Async Storage can only store string data, so in order to store object data you need to serialize it first.

react-native-async-storage.github.io

 

✔ 리액트 네이티브 팀이 더 이상 지원하지 않기 때문에 외부 라이브러리를 사용해야합니다.

npm install @react-native-async-storage/async-storage
import AsyncStorage from '@react-native-async-storage/async-storage';

 

⦁ 사용하는 이유

데이터를 사용자의 디바이스에 저장하기 위해 사용합니다.

생성한 To Do 리스트가 앱을 재부팅하거나 새로고침해도 존재하게 됩니다.

 

⦁ 사용 방법

리액트의 Local Storage와 굉장히 비슷합니다.

하지만 String만 저장할 수 있기 때문에 적절한 형 변환이 필요합니다.

 

  //async스토리지
  const saveToDos = async (toSave) => {
    await AsyncStorage.setItem(STORAGE_KEY, JSON.stringify(toSave)) //Object to String
  }
  const loadToDos = async () => {
    try {
      const s = await AsyncStorage.getItem(STORAGE_KEY)
      //String to Object
      setToDos(JSON.parse(s))

    } catch (e) {
      console.log(e)
    }
  }

🧾변환하는 메서드

JSON.stringify() : 오브젝트 to String

JSON.parse() : String to 오브젝트

 


 

⦁ 📝 전체 소스코드 보기

더보기
import { StatusBar } from 'expo-status-bar';
import { useEffect, useState } from 'react';
import { StyleSheet, Text, View, TouchableOpacity, TextInput, ScrollView, Alert, Platform } from 'react-native';
import { theme } from './color';
import AsyncStorage from '@react-native-async-storage/async-storage';
import { Fontisto } from '@expo/vector-icons';
import { Checkbox } from 'react-native-paper';
import * as React from 'react';

const STORAGE_KEY = "@toDos";

export default function App() {
  const [working, setWorking] = useState(true);
  const travle = () => setWorking(false);
  const work = () => setWorking(true);
  const [checked, setChecked] = React.useState(false);

  //todolist obj
  const [toDos, setToDos] = useState({});

  //텍스트인풋
  const [text, setText] = useState("");
  const onChangeText = (payload) => setText(payload);

  //async스토리지
  const saveToDos = async (toSave) => {
    await AsyncStorage.setItem(STORAGE_KEY, JSON.stringify(toSave)) //Object to String
  }
  const loadToDos = async () => {
    try {
      const s = await AsyncStorage.getItem(STORAGE_KEY)
      //String to Object
      setToDos(JSON.parse(s))

    } catch (e) {
      console.log(e)
    }
  }

  useEffect(() => {
    loadToDos();
  }, [])

  //Submit
  const addToDo = async () => {
    if (text === "") {
      return
    }
    //todo 저장 Object.assign 오브젝트 합치기
    const newToDos = { ...toDos, [Date.now()]: { text, working, checked} }
    setToDos(newToDos)

    //스토리지에 저장
    saveToDos(newToDos)

    //텍스트 초기화
    setText("")
  }

  //Todo 삭제
  //삭제하기 위해 원래 오브젝트 복제하고 삭제하고 set
  const deleteToDo = (key) => {
    if(Platform.OS === "web"){
      const ok = confirm("delete To do?");
      if(ok){
        const newToDos = { ...toDos }
        delete newToDos[key]
        setToDos(newToDos)
        saveToDos(newToDos)
      }
    }
    Alert.alert("Delete To Do", "Are you sure?", [
      { text: "Cancel" },
      {
        text: "I'm Sure", onPress: async () => {
          const newToDos = { ...toDos }
          delete newToDos[key]
          setToDos(newToDos)
          await saveToDos(newToDos)
        }
      }
    ]);
  }

  //Todo 체크
  const checkToDo = async (key) => {
    const newToDos = { ...toDos }
    const temp = newToDos[key].checked

    newToDos[key].checked = !temp

    setToDos(newToDos)
    await saveToDos(newToDos)
  }

  console.log()
  return (
    <View style={styles.container}>
      <StatusBar style="auto" />
      <View style={styles.header}>
        <TouchableOpacity onPress={work}>
          <Text style={{ ...styles.btnText, color: working ? "white" : theme.grey }}>Work</Text>
        </TouchableOpacity>
        <TouchableOpacity onPress={travle}>
          <Text style={{ ...styles.btnText, color: working ? theme.grey : "white" }}>Travel</Text>
        </TouchableOpacity>
      </View>
      <TextInput
        returnKeyType='done'
        onSubmitEditing={addToDo}
        onChangeText={onChangeText}
        placeholder={working ? "Add a To Do" : "어디로 여행갈까?"}
        style={styles.input}>
      </TextInput>

      <ScrollView>
        {
          Object.keys(toDos).map((key) => (
            toDos[key].working === working ?
              <View style={styles.toDo} key={key}>
                <Text style={{...styles.toDoText, 
                  textDecorationLine: toDos[key].checked? 'line-through' : null,
                  color : toDos[key].checked ? 'green': 'white'}} >
                  {toDos[key].text}
                </Text>

                <View style={styles.btnView}>
                  <Checkbox key = {key}
                    status={toDos[key].checked ? 'checked' : 'unchecked'}
                    onPress={() => {
                      checkToDo(key);
                    }}
                    color="green"
                  />

                  <TouchableOpacity onPress={() => deleteToDo(key)}>
                    <Fontisto name="trash" size={18} color="white" />
                  </TouchableOpacity>
                </View>

              </View>
              : null
          ))}
      </ScrollView>

    </View>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: theme.bg,
    paddingHorizontal: 20,
  },
  header: {
    justifyContent: "space-between",
    flexDirection: "row",
    marginTop: 100
  },
  btnText: {
    fontSize: 44,
    fontWeight: "600",
  },
  input: {
    backgroundColor: "white",
    paddingVertical: 15,
    paddingHorizontal: 20,
    borderRadius: 30,
    marginTop: 20,
    fontSize: 18,
    marginBottom: 15,
  },
  toDo: {
    flex:1,
    backgroundColor: theme.grey,
    marginBottom: 10,
    paddingVertical: 20,
    paddingHorizontal: 30,
    borderRadius: 15,
    flexDirection: 'row',
    alignItems: 'center',
    justifyContent: 'space-between',
  },
  toDoText: {
    flex:0.7,
    fontSize: 16,
    fontWeight: "500",
  },
  btnView:{
    flex:0.3,
    flexDirection:"row",
    alignItems:'center',
    justifyContent:'space-around',
  },
});

 

📝 color.js (theme)

export const theme = {
    bg:"black",
    grey:"#3A3D40",
    toDoBg:"#5C5C60",
};

 

 

 


 


리액트 네이티브를 이용하면

안드로이드, IOS 심지어

웹과 Mac, Window까지 개발이 가능하다고 합니다.

 

아직까지 성능 면에서 완전히 네이티브를

이길 수는 없다고 하지만 러닝 커브도 적고

여러 플랫폼마다 개발해야 하는 수고가 없어지기 때문에

공부할 가치가 충분한 것 같습니다!

무엇보다 재미있습니다. 😆

 


참고 문헌 : 

https://nomadcoders.co/react-native-for-beginners/lobby