elevne's Study Note

React Native 로 To-Do App 만들기 (2) 본문

Frontend/React Native

React Native 로 To-Do App 만들기 (2)

elevne 2023. 3. 26. 12:21

앱에 기능들을 추가해주기 위해 Redux 를 사용한다. 우선 Redux 관련 모듈들을 아래 명령어를 통해 전부 추가해준다.

 

 

 

npm install @reduxjs/toolkit react-redux redux --save

 

 

 

그 후 slices 를 담을 slices 폴더, todoSlice.js, store.js 파일을 아래와 같은 구조로 생성해준다.

 

 

 

 

 

 

todoSlice.js 는 아래와 같이 작성해준다.

 

 

 

const { createSlice } = require("@reduxjs/toolkit");

const todoSlice = createSlice({
    name: "todo",
    initialState: {
        currentId: 4,
        todos: [],
    },
    reducers: {}
});

export default todoSlice.reducer;

 

 

 

작성한 reducer 을 store 에 등록을 해줘야한다. store.js 파일은 아래와 같이 작성한다. (또, 작성한 store 은 App.js 에서 Provider 컴포넌트로 감싸줄 때 사용한다)

 

 

 

import { configureStore } from "@reduxjs/toolkit";
import { todoReducer } from "./slices/todoSlice"

export const store = configureStore({
    reducer: {
        todo: todoReducer
    }
})

 

 

 

그 다음으로는 Action type 에 매칭되는 리듀서 함수를 생성해줘야 한다. addTodo, updateTodo, deleteTodo 액션을 만들어야 했다. 이는 todoSlice.js 에서 작성한 slice 의 reducers 부분에 작성해준다.

 

 

 

import { createSlice } from "@reduxjs/toolkit";

const todoSlice = createSlice({
    name: "todo",
    initialState: {
        currentId: 4,
        todos: [],
    },
    reducers: {
        addTodo: (state, action) => {
            state.todos.push({
                id: state.currentId++,
                text: action.payload.trim(),
                state: 'todo'
            })
        },
        updateTodo: (state, action) => {
            const item = state.todos.findIndex((item) => item.id === action.payload); // Key값이 같은 TODO 찾기
            state.todos[item].state = state.todos[item].state === "todo" ? "done" : "todo";
            state.todos.push(state.todos.splice(item, 1)[0]);
        },
        deleteTodo: (state, action) => {
            const item = state.todos.findIndex((item) => item.id === action.payload);
            if (item > -1) { // ITEM 이 있을 경우를 뜻함
                state.todos.splice(item, 1)
            }
        }
    }
});

export default todoSlice.reducer;
export const {addTodo, updateTodo, deleteTodo} = todoSlice.actions;

 

 

 

reducer 들을 정의해준 후 export 해주는 과정을 거쳤다. Reducer 내부에서 splice 라는 메서드가 사용되었다. 이는 배열로부터 특정 범위를 삭제하거나 새로운 값을 추가 또는 기존 값을 대체할 수 있는 메서드이다.

 

 

 

이제 Redux 를 사용할 준비는 마무리가 되었다. 이제 컴포넌트들을 수정해야할 차례였다. 우선 InputForm 컴포넌트부터 아래와 같이 수정하였다.

 

 

 

import { KeyboardAvoidingView, Pressable, StyleSheet, Text, TextInput, View } from 'react-native'
import React, { useState } from 'react'
import { useDispatch } from 'react-redux';
import { addTodo } from '../redux/slices/todoSlice';

const InputForm = () => {
    const [currentValue, setCurrentValue] = useState("");
    const dispatch = useDispatch();

    const handleSubmit = () => {
        if (currentValue !== '') {
            dispatch(addTodo(currentValue));
            setCurrentValue('');
        }
    }

    return (
        <KeyboardAvoidingView
            behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
            style={styles.addFormContainer}>
            <TextInput
                style={styles.inputField}
                placeholder="할 일을 작성해주세요."
                value={currentValue}
                onChangeText={setCurrentValue}
                onSubmitEditing={handleSubmit}
            />
            <Pressable style={styles.addButton} onPress={handleSubmit}>
                <Text style={styles.addButtonText} >+</Text>
            </Pressable>
        </KeyboardAvoidingView>
    )
}

export default InputForm

const styles = StyleSheet.create({
    addFormContainer: {
        flexDirection: 'row',
        marginTop: 'auto',
        marginBottom: 30,
        paddingHorizontal: 20,
        backgroundColor: '#f7f8fa'
    },
    inputField: {
        flex: 1,
        height: 42,
        padding: 5,
        marginRight: 25,
        borderRadius: 4,
        borderColor: 'rgba(0, 0, 0, 0.2)',
        borderWidth: 1,
        color: '#000000',
        fontSize: 15,
        textAlignVertical: 'center'
    },
    addButton: {
        justifyContent: 'center',
        alignItems: 'center',
        width: 42,
        height: 42,
        borderRadius: 4,
        backgroundColor: 'rgba(0,0,0,0.7)',
        shadowColor: '#000000',
        shadowOpacity: 0.14,
        shadowRadius: 8,
        shadowOffset: {
            width: 0,
            height: 4
        }
    },
    addButtonText: {
        color: 'white',
        fontSize: 25
    }
})

 

 

 

useDispatch, dispatch 메서드가 사용되었다. 또, TextInput 내에서 Enter 키로 Form 이 Submit 되는 경우를 다루기 위해서 onSubmitEditing 도 추가해주었다. 이제 추가한 Todo 들을 화면에 리스트로 보여줄 차례였다.

 

 

 

Web 에서 React 를 사용하여 목록을 보여줄 때는 <li> 혹은 <ul> 등의 태그로 구성해볼 수 있다. React Native 에서는 FlatListScrollView 를 사용할 수 있는데, 이 둘에는 큰 차이가 있다고 한다. ScrollView 는 Component 가 로드된 직후 Item 들을 로드한다. 즉, 모든 데이터는 RAM 에 저장되며 성능 저하로 인해 그 안에 있는 수많은 항목들을 사용할 수 없게될 수 있다고 한다. 반대로 FlatList 에는 10 개의 Item (기본값) 을 화면에 탑재하고 사용자가 보기를 스크롤하면 다른 Item 이 탑재되게 한다. 이러한 점에서 ScrollView 대신에 FlatList 를 사용하는 편이 좋다고 한다. (적은 수의 Item 일 때는 ScrollView, 많을 때는 FlatList 를 사용하면 된다고 한다)

 

 

 

MainScreen.js 를 아래와 같이 작성한다. 아래에서 사용된 FlatList 는 data, renderItem, keyExtractor 을 받게된다. data 는 리스트들을 위한 데이터, renderItem 은 렌더링하는 부분을 넣어준다. 그 다음 keyExtractor 은 지정된 인덱스에서 지정된 항목에 대한 고유 키를 추출하는데 사용된다. 키는 캐싱에 사용되며 item re-rendering 을 추적하기 위한 반응 키로 사용된다고 한다. 이 때, store 에 접근하기 위해 useSelector 메서드가 사용된 것을 확인할 수 있다.

 

 

 

import { View, Text, SafeAreaView, Platform, FlatList } from 'react-native'
import React from 'react'
import { StyleSheet } from 'react-native'
import { StatusBar } from 'expo-status-bar'
import InputForm from '../components/InputForm'
import TodoItem from '../components/TodoItem'
import { useSelector } from 'react-redux'

const MainScreen = () => {
  const todos = useSelector(state => state.todo.todos);

  const todoTasks = todos.filter((item) => item.state === "todo");
  const completedTasks = todos.filter((item) => item.state === "done");
  return (
    <SafeAreaView style={styles.container}>
      <StatusBar
      barStyle={"default"}
      />
      <Text style={styles.pageTitle}>ToDo App</Text>
      <View style={styles.listView}>
        <Text style={styles.listTitle}>TO DO</Text>
        {todoTasks.length !== 0 ? (
          <FlatList
            data={todoTasks}
            renderItem={({item}) => <TodoItem {...item} />}
            keyExtractor={(item) => item.id}
          ></FlatList>
        ) : (<Text style={styles.emptyListText}>NOTHING TO DO!</Text>)}
      </View>
      <View style={styles.separator}/>
      <View style={styles.listView}>
        <Text style={styles.listTitle}>DONE</Text>
        {completedTasks.length !== 0 ? (
          <FlatList
            data={completedTasks}
            renderItem={({item}) => <TodoItem {...item} />}
            keyExtractor={(item) => item.id}
          ></FlatList>
        ) : (<Text style={styles.emptyListText}>NOTHING DONE YET</Text>)}
      </View>
      <InputForm/>
    </SafeAreaView>
  )
}

export default MainScreen

const styles = StyleSheet.create({
  container: {
    flex: 1,
    paddingTop: Platform.OS === 'android' ? 20 : 0, // ANDROID 와 IOS 의 분기처리 (이유: IOS 에서는 이미 SafeAreaView 를 사용했기 때문)
    backgroundColor: "#f7f8fa"
  },
  pageTitle: {
    marginBottom: 35,
    paddingHorizontal: 15,
    fontSize: 50,
    fontWeight: "600"
  },
  separator: {
    marginHorizontal: 10,
    marginTop: 25,
    marginBottom: 10,
    borderBottomWidth: 1,
    borderBottomColor: "rgba(0,0,0,0.2)"
  },
  listView: {
    flex: 1,
  },
  listTitle: {
    marginBottom: 25,
    paddingHorizontal: 15,
    fontSize: 40,
    fontWeight: "500"
  },
  emptyListText: {
    paddingTop: 10,
    paddingBottom: 15,
    paddingHorizontal: 15,
    fontSize: 15,
    lineHeight: 20,
    color: "#737373"
  }
})

 

 

 

 

그 다음으로는 TodoItem 컴포넌트를 아래와 같이 수정해준다.

 

 

 

import { View, Text, Pressable, StyleSheet } from 'react-native'
import React from 'react'
import CheckboxUnchecked from "../assets/checkbox-unchecked.svg"
import CheckboxChecked from "../assets/checkbox-checked.svg"
import DeleteIcon from "../assets/delete.svg"
import { useDispatch } from 'react-redux'
import { updateTodo, deleteTodo } from '../redux/slices/todoSlice'

const TodoItem = (props) => { // PROPS 를 가져올 수 있는 것은 FlatList 에서 item 넣어주는 부분이 있기 때문

  const dispatch = useDispatch();

  return (
    <View style={styles.itemContainer} >

      <Pressable style={styles.itemCheckbox} hitSlop={10}
      onPress={() => dispatch(updateTodo(props.id))}>
        {props.state === "todo" ?
        <CheckboxUnchecked/> :
        <CheckboxChecked style={styles.itemCheckboxCheckedIcon} /> }
      </Pressable>

      <Text style={[styles.itemText, 
        props.state === "done" ? styles.itemTextChecked : "" ]}>
        {props.text}
      </Text>

      <Pressable style={[styles.deleteButton,
        props.state === "done" ? styles.deleteButtonDone : ""]} hitSlop={10}
        onPress={() => dispatch(deleteTodo(props.id))}>
        <DeleteIcon />
      </Pressable>

    </View>
  )
}

export default TodoItem

const styles = StyleSheet.create({
    itemContainer: {
        flexDirection: "row",
        alignItems: "center",
        paddingTop: 10,
        paddingBottom: 15,
        paddingHorizontal: 15,
        backgroundColor: "#f7f8fa"
    },
    itemCheckbox: {
        justifyContent: "center",
        alignItems: "center",
        width: 20,
        height: 20,
        marginRight: 15,
        borderRadius: 5,
    },
    itemCheckboxCheckedIcon: {
        shadowColor: "#000000",
        shadowOpacity: 0.2,
        shadowRadius: 8,
        shadowOffset: {
            width: 0,
            height: 4
        }
    },
    itemText: {
        marginRight: "auto",
        paddingRight: 25,
        fontSize: 15,
        lineHeight: 20,
        color: "#737373",
    },
    itemTextChecked: {
        opacity: 0.5,
        textDecorationLine: "line-through"
    },
    deleteButton: {
        opacity: 0.8
    },
    deleteButtonDone: {
        opacity: 0.3
    }
})

 

 

 

여기까지 작성하고 실행하면 모든 의도한 기능들이 정상 동작하는 것을 확인할 수 있다. 

 

 

 

result

 

 

 

이제 모든 기능이 잘 동작하는 것도 확인해보았다. 기본 기능들은 완료되었고, 다음 번에는 추가적으로 회원 기능 등을 만들어볼 예정이다.

 

 

 

 

 

 

Reference:

https://www.inflearn.com/course/%EB%94%B0%EB%9D%BC%ED%95%98%EB%A9%B0-%EB%B0%B0%EC%9A%B0%EB%8A%94-%EB%A6%AC%EC%95%A1%ED%8A%B8-%EB%84%A4%EC%9D%B4%ED%8B%B0%EB%B8%8C/dashboard

'Frontend > React Native' 카테고리의 다른 글

React Native 로 To-Do App 만들기 (4)  (0) 2023.04.24
React Native 로 To-Do App 만들기 (3)  (0) 2023.04.23
React Native: Redux (2)  (0) 2023.03.25
React Native: Redux  (0) 2023.03.24
React Native 로 To-Do App 만들기 (1)  (0) 2023.03.23