컴퓨터 프로그래밍/React Native

[React Native] Todo 어플리케이션

한33 2025. 3. 30. 16:23

 

 

 

✔️ TouchableOpacity, ...styles

<TouchableOpacity onPress={work}>
    <Text style={{...styles.btnText, color: working ? "white" : theme.toDoBg}}>Work</Text>
</TouchableOpacity>
<TouchableOpacity onPress={travel}>
    <Text style={{...styles.btnText, color: !working ? "white" : theme.toDoBg}}>Travel</Text>
</TouchableOpacity> 

 

header 에서 onPress 를 활용해서 work 와 travel 의 상태를 true 로 저장시켰다.

이 상태값을 활용해서 이후에 placeholder, list 들을 상태값에 따라 다르게 보여지게 한다.

 

styles 앞에 ... 을 붙이면 기존 객체를 그대로 가져와서 새로 추가하는 내용만 수정한다는 뜻이다.

 

✔️ TextInput

<TextInput
    onSubmitEditing={addTodo}
    returnKeyType={"done"}
    value={text}
    onChangeText={onChangeText}
    placeholder={working ? "Add a To Do" : "Where do you wannt to go?"}
    style={styles.textInput}
/>

 

TextInput 의 다양한 prop 을 이용해서 Customizing 해주었다.

완료 버튼을 done 으로 바꿔주었다.

onSubmitEditing 에 addTodo 함수를 연결해주었다.

 

✔️ Todo 저장

const onChangeText = (payload) => setText(payload)

 

onChangeText 는 text 를 받아와서 state 에 text 를 담았다.

const addTodo = async () => {
    if (text === "") {
        return;
    }
    const newToDos = {
        ...toDos, [Date.now()]: {text, working: working},
    };
    setToDos(newToDos)
    await saveToDos(newToDos)
    setText("");
    console.log(toDos)
}

 

  • toDos 가 비어있으면 그대로 끝내고,
  • 새로운 toDo 가 있다면 기존에 toDos 에서 Date.now() 를 key 로 가지고 있는 새로운 객체와 결합해서  newToDos 를 만들어낸다.
  • text 는 state 에서 가져오고 working 이 true 인지 false 인지 역시 기존에 선택한 tab 에 저장되어있는 state 로 부터 가져온다.
  • 이후 setToDos 로 state 에 저장하고 saveToDos 함수를 호출한다.
  • 그리고 setText("")  로 다시 입력창을 빈 칸으로 만들어둔다.

✔️ Todo삭제

<TouchableOpacity onPress={() => deleteToDo(key)}>
    <Fontisto name="trash" size={18} color={theme.toDoBg} />
</TouchableOpacity>

 

휴지통 아이콘을 배치시키면서 클릭할 시에 onPress 에 deleteToDo 함수가 key 값과 호출되도록 설정했다.

const deleteToDo = async (key) => {
    Alert.alert("Delete To Do?", "Are you sure?", [
            {
                text: "Ok",
                onPress: () => {
                    const newToDos = {...toDos}
                    delete newToDos[key]
                    setToDos(newToDos);
                    saveToDos(newToDos);
                }
            },
            {
                text: "Cancel", style: "destructive"
            }
        ]
    )
}

 

Alert Api 를 활용해서 사용자에게 alert 를 보내주는데, 우리가 마음대로 메세지와 버튼 문구, callback 등을 설정할 수 있었다.

style 은 ios 에서만 가능했다.

✔️ JSON.stringify / JSON.parse

const saveToDos = async (toSave) => {
    await AsyncStorage.setItem(STORAGE_KEY, JSON.stringify(toSave))
}
const loadToDos = async () => {
    setLoading(true);
    const s = await AsyncStorage.getItem(STORAGE_KEY);
    if (s) {
        setToDos(JSON.parse(s))
    }
    setLoading(false)
}

 

JSON.stringify 는 자바스크립트 객체를 string 으로 변환시켜주기 때문에 saveToDos 함수에서는 위와 같이 사용되었다면,

반대로 loadToDos 함수에서는 기존에 변환된 string 을 다시 자바스크립트 객체로 변환시켜주기 위해서 JSON.parse(s) 를 사용했다.

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

 

useEffect 에 loadToDos 를 넣어서 화면이 로딩되면 자동으로 데이터들이 보이도록 설정했다.

✔️ 전체 코드

import {StatusBar} from 'expo-status-bar';
import {Keyboard, TouchableWithoutFeedback} from "react-native";
import {ActivityIndicator, Alert, ScrollView, StyleSheet, Text, TextInput, TouchableOpacity, View} from 'react-native';
import React, {useEffect, useState} from "react";
import {theme} from "./colors";
import AsyncStorage from "@react-native-async-storage/async-storage";
import Fontisto from '@expo/vector-icons/Fontisto';

const STORAGE_KEY_TODO = "@toDos"
const STORAGE_KEY_MENU = "@menu"

export default function App() {
    const [working, setWorking] = useState(true);
    const [isCompleted, setIsCompleted] = useState(false);
    const [text, setText] = useState("");
    const [toDos, setToDos] = useState({});
    const [loading, setLoading] = useState(true);
    const [editingKey, setEditingKey] = useState(null);
    const [editText, setEditText] = useState("");
    const travel = async () => {
        setWorking(false);
        await saveMenu(false)
    };
    const work = async () => {
        setWorking(true);
        await saveMenu(true);
    }
    const saveMenu = async (isWorking) => {
        await AsyncStorage.setItem(STORAGE_KEY_MENU, JSON.stringify(isWorking))
    }
    const loadMenuState = async () => {
        const savedState = await AsyncStorage.getItem(STORAGE_KEY_MENU);
        if (savedState !== null) {
            setWorking(JSON.parse(savedState));
        }
    }
    const onChangeText = (payload) => setText(payload)
    const saveToDos = async (toSave) => {
        await AsyncStorage.setItem(STORAGE_KEY_TODO, JSON.stringify(toSave))
    }
    const loadToDos = async () => {
        setLoading(true);
        const s = await AsyncStorage.getItem(STORAGE_KEY_TODO);
        if (s) {
            setToDos(JSON.parse(s))
        }
        setLoading(false)
    }
    useEffect(() => {
        loadToDos();
        loadMenuState();
    }, []);
    const addTodo = async () => {
        if (text === "") {
            return;
        }
        const newToDos = {
            ...toDos, [Date.now()]: {text, working: working, isCompleted: isCompleted},
        };
        setToDos(newToDos)
        await saveToDos(newToDos)
        setText("");
        console.log(toDos)
    }
    const deleteToDo = async (key) => {
        Alert.alert("Delete To Do?", "Are you sure?", [
                {
                    text: "Ok",
                    onPress: () => {
                        const newToDos = {...toDos}
                        delete newToDos[key]
                        setToDos(newToDos);
                        saveToDos(newToDos);
                    }
                },
                {
                    text: "Cancel", style: "destructive"
                }
            ]
        )
    }
    const editToDo = (key, currentText) => {
        setEditingKey(key);
        setEditText(currentText);
        console.log(key, currentText)
    }
    const saveEdit = async () => {
        if (editText.trim() === "") return;
        const updatedToDos = {
            ...toDos,
            [editingKey]: {...toDos[editingKey], text: editText}
        };
        setToDos(updatedToDos);
        await saveToDos(updatedToDos);
        setEditingKey(null);
        setEditText("");
    };
    const updateToDos = async (key) => {
        const updatedToDos = {
            ...toDos,
            [key]: {...toDos[key], isCompleted: !toDos[key].isCompleted}
        }
        setToDos(updatedToDos);
        await saveToDos(updatedToDos);
    }

    return (
        <TouchableWithoutFeedback onPress={Keyboard.dismiss} accessible={false}>
            <View style={styles.container}>
                <StatusBar style="auto"/>
                <View style={styles.header}>
                    <TouchableOpacity onPress={work}>
                        <Text style={{...styles.btnText, color: working ? "white" : theme.toDoBg}}>Work</Text>
                    </TouchableOpacity>
                    <TouchableOpacity onPress={travel}>
                        <Text style={{...styles.btnText, color: !working ? "white" : theme.toDoBg}}>Travel</Text>
                    </TouchableOpacity>
                </View>
                <View>
                    <TextInput
                        onSubmitEditing={addTodo}
                        returnKeyType={"done"}
                        value={text}
                        onChangeText={onChangeText}
                        placeholder={working ? "Add a To Do" : "Where do you wannt to go?"}
                        style={styles.textInput}
                    />
                    {
                        loading ? (
                            <ActivityIndicator size="large" color="white" style={styles.loading}/>
                        ) : (
                            <ScrollView>
                                {
                                    Object.keys(toDos).map(key =>
                                        toDos[key].working === working ? (
                                            <View style={styles.toDo} key={key}>
                                                {editingKey === key ? (
                                                    <TextInput
                                                        value={editText}
                                                        onChangeText={setEditText}
                                                        onSubmitEditing={saveEdit}
                                                        style={styles.editInput}
                                                        autoFocus
                                                    />
                                                ) : (
                                                    <Text
                                                        style={[
                                                            styles.toDoText,
                                                            toDos[key].isCompleted && {
                                                                textDecorationLine: "line-through",
                                                                color: "gray"
                                                            }
                                                        ]}
                                                    >
                                                        {toDos[key].text}
                                                    </Text>
                                                )}
                                                <View style={styles.btnContainer}>
                                                    <TouchableOpacity onPress={() => updateToDos(key)}>
                                                        <Fontisto name="check" size={18}
                                                                  color={toDos[key].isCompleted ? "gray" : theme.toDoBg}/>
                                                    </TouchableOpacity>
                                                    <TouchableOpacity onPress={() => editToDo(key, toDos[key].text)}>
                                                        <Fontisto name="commenting" size={18} color={theme.toDoBg}/>
                                                    </TouchableOpacity>
                                                    <TouchableOpacity onPress={() => deleteToDo(key)}>
                                                        <Fontisto name="trash" size={18} color={theme.toDoBg}/>
                                                    </TouchableOpacity>
                                                </View>
                                            </View>
                                        ) : null
                                    )}
                            </ScrollView>
                        )
                    }
                </View>
            </View>
        </TouchableWithoutFeedback>
    );
}

const styles = StyleSheet.create({
    container: {
        flex: 1,
        backgroundColor: theme.bg,
        paddingHorizontal: 20,
    },
    header: {
        justifyContent: "space-between",
        flexDirection: "row",
        marginTop: 100,
    },
    btnText: {
        fontSize: 38,
        fontWeight: "600",
    },
    textInput: {
        backgroundColor: "white",
        paddingVertical: 15,
        paddingHorizontal: 20,
        borderRadius: 20,
        marginVertical: 20,
        fontSize: 18,
    },
    toDo: {
        backgroundColor: theme.bg,
        marginBottom: 10,
        paddingVertical: 10,
        paddingHorizontal: 40,
        borderRadius: 15,
        flexDirection: "row",
        alignItems: "center",
        justifyContent: "space-between"
    },
    toDoText: {
        color: "white",
        fontSize: 16,
        fontWeight: "500",
    },
    loading: {
        marginTop: 20,
    },
    btnContainer: {
        flexDirection: "row",
        gap: 10,
    },
    editInput: {
        backgroundColor: "white",
        paddingVertical: 5,
        paddingHorizontal: 10,
        borderRadius: 10,
        fontSize: 16,
        flex: 0.8,
    },
});