on
[리액트 네이티브를 다루는 기술 #6] 4장 할일 목록 만들기2 (p.179 ~ 213)
[리액트 네이티브를 다루는 기술 #6] 4장 할일 목록 만들기2 (p.179 ~ 213)
@ List
* 4.3 새 항목 등록하기
* 4.4 할일 완료 상태 토글하기
* 4.5 항목 삭제하기
** 4.5.1 벡터 아이콘 사용하기
** 4.5.2 항목 삭제 함수 만들기
** 4.5.3 항목을 삭제하기 전에 한번 물어보기
* 4.6 AsyncStorage로 앱이 꺼져도 데이터 유지하기
** 4.6.1 Promise가 무엇인가요?
** 4.6.2 AsyncStorage 설치하기
** 4.6.3 AsyncSotrage의 기본 사용법
** AsyncStorage 적용하기
* 4.7 정리
@ Note
1. 최대값 반환 내장함수 : Math.max()
- todos.length > 0 ? Math.max(...todos.map(todo => todo.id)) + 1 : 1;
2. react-native-vector-icons 적용 시 네이티브 소스 수정 필요
- ios/TodoApp/Info.plist 수정 후, pod install 설치 후, 재실행
- android/app/build.gradle 수정 후, 재실행
3. removePlaceholder 적용의 중요성
- 아이콘이 보이지 않을 때도 삭제 아이콘이 보일 영역을 미리 차지해 두기 위함
- 해당 작업을 하지 않으면 항목의 내용이 긴 경우 토글할 때마다 텍스트가 보이는 영역이 변경됨
4. Promise 객체의 중요성
- setTimeout( , ) 에서 두번째 인자에 0을 넣으면 자바스크립스 런타임 환경에 따라 4ms~10ms 이후 함수가 실행됨 (Node.js는 1ms 이후 실행)
- new Promise ((resove, reject) => { }) 과 .then( ) 의 관계
- async 와 await 의 관계
- 오류에 대한 예외 처리 시, try/catch 구문 사용
- console.error()는 콘솔에 결과를 출력 시 빨간색으로 강조해 출력함
5. useEffect()
- 컴포넌트 마운트 또는 언마운트 시점에 따른 구현 방법 확인
- 여러개 사용 시 등록된 순서대로 작동함
-
6. AsyncStorage
- 값 저장 시 문자열 타입이어야 하고, 객체 및 배열 타입 저장 시 JSON.stringify() 사용 (불러올때 JSON.parse() 사용)
- Android는 기본적으로 최대용량이 6MB로 설정 되어있고, 용량을 늘리려면 android/gradle.properties 파일에 코드 추가
- iOS는 최대용량이 지정되어 있지 않음
- AsyncStorage는 소규모 데이터를 다룰 때 사용하는 것이 좋으며, 규모가 커졌을 경우 react-native-sqlite-storage 가 있음
- API : AsyncStorage.setItem(), AsyncStorage.getItem(), AsyncStorage.clear() 등
> referece : https://react-native-async-storage.github.io/async-storage/docs/api
@ Git
https://github.com/eunbok-bocoder/TodoApp
# Source tree
# App.js
import React, {useState, useEffect, useReducer} from 'react'; import {StyleSheet, KeyboardAvoidingView, Platform} from 'react-native'; // for IOS StatusBar import {SafeAreaProvider, SafeAreaView} from 'react-native-safe-area-context'; // import AsyncStorage from '@react-native-community/async-storage'; import DateHead from './components/DateHead'; import AddTodo from './components/AddTodo'; import Empty from './components/Empty'; import TodoList from './components/TodoList'; import todosStorage from './storages/todosStorage'; function App() { const today = new Date(); const [todos, setTodos] = useState([ {id: 1, text: '작업환경 설정', done: true}, {id: 2, text: '리액트 네이티브 기초 공부', done: false}, {id: 3, text: '투두리스트 만들어보기', done: false}, ]); // // 데이터 불러오기 (데이터 저장보다 위에 있어야 함) // useEffect(() => { // async function load() { // try { // const rawTodos = await AsyncStorage.getItem('todos'); // const savedTodos = JSON.parse(rawTodos); // setTodos(savedTodos); // } catch (e) { // console.log('Failed to load todos'); // } // } // load(); // }, []); // 배열이 비어있으면 마운트될 때 딱 한 번만 함수가 호출됨 // // 데이터 저장 // useEffect(() => { // // console.log(todos); // async function save() { // try { // await AsyncStorage.setItem('todos', JSON.stringify(todos)); // } catch (e) { // console.log('Failed to load todos'); // } // } // save(); // }, [todos]); //storage > todosStorage.js 사용 useEffect(() => { todosStorage.get().then(setTodos).catch(console.error); }, []); useEffect(() => { todosStorage.set(todos).catch(console.error); }, [todos]); const onInsert = text => { // 새로 등록할 항목의 id를 구함 // 등록된 항목 중에서 가장 큰 id를 구하고, 그 값에 1을 더함 // 만약 리스트가 비어있다면 1을 id로 사용함 const nextId = todos.length > 0 ? Math.max(...todos.map(todo => todo.id)) + 1 : 1; const todo = { id: nextId, text, done: false, }; setTodos(todos.concat(todo)); console.log('Math.max() : ' + Math.max(...todos.map(todo => todo.id))); console.log('nextId : ' + nextId); }; const onToggle = id => { const nextTodos = todos.map(todo => todo.id === id ? {...todo, done: !todo.done} : todo, ); setTodos(nextTodos); }; const onRemove = id => { const nextTodos = todos.filter(todo => todo.id !== id); setTodos(nextTodos); }; return ( {todos.length === 0 ? ( ) : ( )} ); } const styles = StyleSheet.create({ block: { flex: 1, backgroundColor: 'white', }, avoid: { flex: 1, }, }); export default App;
# components > AddTodo.js
import React, {useState} from 'react'; import { View, StyleSheet, TextInput, Image, Platform, TouchableOpacity, TouchableNativeFeedback, Keyboard, } from 'react-native'; function AddTodo({onInsert}) { const [text, setText] = useState(''); const onPress = () => { onInsert(text); setText(''); Keyboard.dismiss(); // button 눌렀을 시 키보드 사라짐 }; const button = ( ); return ( {Platform.select({ ios: {button}, android: ( {button} ), })} ); } const styles = StyleSheet.create({ block: { backgroundcolor: 'white', height: 64, paddingHorizontal: 16, // 좌우 여백 bordercolor: '#bdbdbd', borderTopWidth: 1, borderBottomWidth: 1, alignItems: 'center', //상하 정렬 flexDirection: 'row', }, input: { flex: 1, // TextInput 란 확장 fontSize: 16, paddingVertical: 8, // 상하 터치영역 확장 }, buttonStyle: { alignItems: 'center', justifyContent: 'center', width: 48, height: 48, backgroundColor: '#26a69a', borderRadius: 24, }, circleWrapper: { overflow: 'hidden', // 지정한 영역 외 바깥 영역 숨김 borderRadius: 24, }, }); export default AddTodo;
# components > DataHead.js
import React from 'react'; import {View, Text, StyleSheet, StatusBar} from 'react-native'; // for IOS StatusBar import {useSafeAreaInsets} from 'react-native-safe-area-context'; function DateHead() { const d = new Date(); const year = d.getFullYear(); const month = d.getMonth() + 1; // getMonth 범위 : 0 ~ 11 까지 const day = d.getDate(); const formatted = `${year}년 ${month}월 ${day}일`; const {top} = useSafeAreaInsets(); return ( <> {year}년 {month}월 {day}일 ); } const styles = StyleSheet.create({ statusBarPlaceholder: { backgroundColor: '#26a69a', }, block: { padding: 16, backgroundColor: '#26a69a', }, dateText: { fontSize: 24, color: 'white', }, }); export default DateHead;
# Empty.js
import React from 'react'; import {View, Text, Image, StyleSheet} from 'react-native'; function Empty() { return ( 할일이 없습니다. ); } const styles = StyleSheet.create({ block: { flex: 1, alignItems: 'center', justifyContent: 'center', }, image: { width: 240, height: 179, marginBottom: 16, }, description: { fontSize: 24, color: '#9e9e9e', }, }); export default Empty;
# components > TodoItem.js
import React, {useEffect} from 'react'; import { View, Text, StyleSheet, Image, Touchable, TouchableOpacity, Platform, Alert, } from 'react-native'; import Icon from 'react-native-vector-icons/MaterialIcons'; function TodoItem({id, text, done, onToggle, onRemove}) { // useEffect(() => { // console.log('컴포넌트가 마운트될 때 출력됨'); // return () => { // console.log('컴포넌트가 언마운트될 때 출력됨'); // }; // }, []); const remove = () => { // 제목, 내용 Alert.alert('삭제', '정말로 삭제하시겠어요?', [ // 왼쪽 버튼 {text: '취소', onPress: () => {}, style: 'cancel'}, // 오른쪽 버튼 { text: '삭제', onPress: () => { onRemove(id); }, style: 'destuctive', }, ]); }; return ( { onToggle(id); // console.log('id : ' + Platform.OS + ' / ' + id); }}> {done && ( )} {text} {done ? ( // { // onRemove(id); // }}> // // ) : ( )} ); } const styles = StyleSheet.create({ item: { flexDirection: 'row', padding: 16, alignItems: 'center', }, circle: { width: 24, height: 24, borderRadius: 12, borderWidth: 1, borderColor: '#26a69a', marginRight: 16, }, filled: { alignItems: 'center', backgroundColor: '#26a69a', }, text: { flex: 1, fontSize: 16, color: '#212121', }, lineThrough: { color: '#9e9e9e', textDecorationLine: 'line-through', }, // 내용이 긴 경우 텍스트 영역이 달라지는 것을 막기 위함 removePlaceholder: { width: 32, height: 32, }, }); export default TodoItem;
# components > TodoList.js
import React from 'react'; import {FlatList, View, Text, StyleSheet} from 'react-native'; import TodoItem from './TodoItem'; function TodoList({todos, onToggle, onRemove}) { return ( } style={styles.list} data={todos} renderItem={({item}) => ( )} keyExtractor={item => item.id.toString()} /> ); } const styles = StyleSheet.create({ list: { flex: 1, }, separator: { backgroundColor: '#e0e0e0', height: 1, }, }); export default TodoList;
# storages > todosStorage.js
import AsyncStorage from '@react-native-community/async-storage'; import {get} from 'lodash'; const key = 'todos'; const todosStorage = { async get() { try { const rawTodos = await AsyncStorage.getItem(key); if (!rawTodos) { //저장된 데이터가 없으면 사용하지 않음 throw new Error('No saved todos'); } const savedTodos = JSON.parse(rawTodos); return savedTodos; } catch (e) { throw new Error('failed to load todos'); } }, async set(date) { try { await AsyncStorage.setItem(key, JSON.stringify(date)); } catch (e) { throw new Error('Failed to save todos'); } }, }; export default todosStorage;
# ios > TodoApp > Info.plist
(...) UIAppFonts MaterialIcons.ttf
# android > app > build.gradle
(...) // Android 벡터 아이콘 적용 apply from: file("../../node_modules/react-native-vector-icons/fonts.gradle")
# android > gradle.properties
(...) #AsyncStorage 최대 용량 변경 (default : 5MB) AsyncStorage_db_size_in_MB=10
이전 이전 01 ios / android - 새 항목 등록 및 상태 토글
이전 이전 012 ios / android - 벡터 아이콘 사용 및 removePlaceholder 설정
이전 이전 012 ios / android - 삭제 전 Alert 띄우기
ios / android - reload 후 데이터 보존 확인
반응형
from http://bocoder.tistory.com/64 by ccl(A) rewrite - 2021-12-24 20:01:16