@ 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(a,[b])
- 컴포넌트 마운트 또는 언마운트 시점에 따른 구현 방법 확인
- 여러개 사용 시 등록된 순서대로 작동함
- 컴포넌트가 가장처음 렌더링됐을 때, b가 변경될 때 호출됨
> a : 주시하고 싶은 값이 바뀌었을 때 호출하고 싶은 함수
> b : 주시하고 싶은 값을 배열안에 넣음
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
@ Result
@ 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 (
<SafeAreaProvider>
<SafeAreaView edges={['bottom']} style={styles.block}>
<KeyboardAvoidingView
// behavior={Platform.OS === 'ios' ? 'padding' : undefined} // 삼항연산자 이용 시
behavior={Platform.select({ios: 'padding', android: undefined})}
style={styles.avoid}>
<DateHead date={today} />
{todos.length === 0 ? (
<Empty />
) : (
<TodoList todos={todos} onToggle={onToggle} onRemove={onRemove} />
)}
<AddTodo onInsert={onInsert} />
</KeyboardAvoidingView>
</SafeAreaView>
</SafeAreaProvider>
);
}
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 = (
<View style={styles.buttonStyle}>
<Image source={require('../assets/icons/add_white/add_white.png')} />
</View>
);
return (
<View style={styles.block}>
<TextInput
placeholder="할일을 입력하세요."
style={styles.input}
value={text}
onChangeText={setText}
onSubmitEditing={onPress} // Enter 눌렀을 시 키보드 사라짐
returnKeyType="done" // Enter 타입 지정
/>
{Platform.select({
ios: <TouchableOpacity onPress={onPress}>{button}</TouchableOpacity>,
android: (
<View style={styles.circleWrapper}>
<TouchableNativeFeedback onPress={onPress}>
{button}
</TouchableNativeFeedback>
</View>
),
})}
</View>
);
}
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 (
<>
<View style={[styles.statusBarPlaceholder, {height: top}]} />
<StatusBar backgroundColor="#26a69a" barStyle="light-content" />
<View style={styles.block}>
<Text style={styles.dateText}>
{year}년 {month}월 {day}일
</Text>
</View>
</>
);
}
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 (
<View style={styles.block}>
<Image
source={require('../assets/images/young_and_happy.png')}
style={styles.image}
resizeMode="cover"
/>
<Text style={styles.description}>할일이 없습니다.</Text>
</View>
);
}
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 (
<View style={styles.item}>
<TouchableOpacity
onPress={() => {
onToggle(id);
// console.log('id : ' + Platform.OS + ' / ' + id);
}}>
<View style={[styles.circle, done && styles.filled]}>
{done && (
<Image
source={require('../assets/icons/check_white/check_white.png')}
/>
)}
</View>
</TouchableOpacity>
<Text style={[styles.text, done && styles.lineThrough]}>{text}</Text>
{done ? (
// <TouchableOpacity
// onPress={() => {
// onRemove(id);
// }}>
// <Icon name="delete" size={32} color="red" />
// </TouchableOpacity>
<TouchableOpacity onPress={remove}>
<Icon name="delete" size={32} color="red" />
</TouchableOpacity>
) : (
<View style={styles.removePlaceholder} />
)}
</View>
);
}
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 (
<FlatList
ItemSeparatorComponent={() => <View style={styles.separator} />}
style={styles.list}
data={todos}
renderItem={({item}) => (
<TodoItem
id={item.id}
text={item.text}
done={item.done}
onToggle={onToggle}
onRemove={onRemove}
/>
)}
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
(...)
<!-- iOS 벡터 아이콘 적용 -->
<key>UIAppFonts</key>
<array>
<string>MaterialIcons.ttf</string>
</array>
</dict>
</plist>
# 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
'React Native > React Native_study' 카테고리의 다른 글
[리액트 네이티브를 다루는 기술 #8] 5장 리액트 내비게이션으로 여러 화면 관리하기 (p.244 ~ 284) (0) | 2021.12.30 |
---|---|
[리액트 네이티브를 다루는 기술 #7] 5장 리액트 내비게이션으로 여러 화면 관리하기 (p.215 ~ 243) (0) | 2021.12.27 |
[리액트 네이티브를 다루는 기술 #5] 4장 할일 목록 만들기2 (p.161 ~ 178) (0) | 2021.12.23 |
[리액트 네이티브를 다루는 기술 #4] 3장 할일 목록 만들기1 (p.133 ~ 160) (1) | 2021.12.22 |
[리액트 네이티브를 다루는 기술 #3] 3장 할일 목록 만들기1 (p.109 ~ 132) (0) | 2021.12.20 |