To-Do-List 만들기
- 등록 : 할 일항목을 추가하는기능
- 수정 : 완료되지 않은 할 일 항목을 수정하는 기능
- 삭제 : 할 일항목을 삭제하는기능
- 완료 : 할 일 항목의 완료 상태를 관리하는 기능
프로젝트를 진행하기 앞서 styled-components/native를 사용하는 데 있어 아래와 같은 오류가 발해서 추가로 패키지를 설치해주었다.
Could not find a declaration file for module 'styled-components/native'.
npm install --save-dev @types/styled-components-react-native
1. SafeAreaView 컴포넌트
: 외부 요인에 의해 컴포넌트 일부가 가려지는 것을 방지하기 위해 사용
: 자동으로 padding 값 적용
: iOS 기기에만 적용됨
const Container = styled.SafeAreaView`
flex: 1;
background-color: ${({ theme }) => theme.background};
align-items: center;
justify-content: flex-start;
`;
2. StatusBar 컴포넌트
: 외부 요인에 의해 컴포넌트 일부가 가려지는 것을 방지하고 상태 바를 변경해 잘보이게 하는 데 사용
: 안드로이드에 적용
export default function App() {
return (
<ThemeProvider theme={theme}>
<Container>
<StatusBar
barStyle="light-content"
backgroundColor={theme.background}
/>
<Title>TODO List</Title>
</Container>
</ThemeProvider>
)
}
3. Dimensions
: 현재 화면의 크기를 알 수 있다.
: 다양한 크기의 기기에 동일한 모습으로 적용될 수 있도록 코드 작성 가능
: 처음 값을 받아왔을 때의 크기로 고정된 되기 때문에 변화된 화면의 크기와 일치하지 않을 수 있다. => 이를 대비 하기 위해 이벤트 리스너를 등록해 변화에 대응할 수 있도록 기능을 제공한다.
- useWindowDimensions : 리액트 네이티브에서 제공하는 Hooks, 화면의 크기 가 변경되면 화면의 크기, 너비, 높이를 자동으로 업데이트
import React from 'react'
import styled from 'styled-components/native'
import { Dimensions } from 'react-native';
const StyledInput = styled.TextInput`
width: ${({ width }) => width - 40}px;
height: 60px;
margin: 3px 0;
padding: 15px 20px;
border-radius: 10px;
background-color: ${({ theme }) => theme.itemBackground};
font-size: 25px;
color: ${({ theme }) => theme.text};
`;
const Input = () => {
const width = Dimensions.get('window').width;
return (
<StyledInput width={width} />
)
}
export default Input
const width = useWindowDimensions().width;
4. Textinput 컴포넌트
: 첫 글자가 대문자로 나타나고 오타 입 력 시 자동으로 수정하는 기능이 존재
- autoCapitalize : 첫 글자 대문자
- autoCorrect : 오타 자동 수정
- returnKeyType : iOS에서 키보드 완료 버튼 설정
- keyboardAppearance : iOS 키보드 색상 변경
<StyledInput
width={width}
placeholder={placeholder}
maxLength={50}
autoCapitalize="none"
autoCorrect={false}
returnKeyType="done"
keyboardAppearance="dark"
/>
실습
1️⃣ 추가 기능
useState를 이용해 to do list 를 저장하고 관리할 tasks 변수를 생성 후 초기값을 입력했다.
또한 최신 항목이 가장 앞에 보이도록 역순으로 렌더링 했고,
고유의 key 값도 설정했다.
{Object.values(tasks)
.reverse()
.map(item => (
<Task
key={item.id}
text={item.text}
/>
))}
_addTask 함수를 정의하고 추가 기능을 넣어주겠다.
id는 타임스탬프를 이용하고 text는 입력된 값을 지정해준 후 새 입력값은 completed는 항상 false 가 된다.
마지막으로 newTask 값을 빈 문자열로 지정해 input을 초기화한 후 기존 목록은 유지하고 새 항목이 추가되도록 정의했다.
const _addTask = () => {
const ID = Date.now().toString();
const newTaskObject = {
[ID]: { id: ID, text: newTask, completed: false },
};
setNewTask('');
setTasks({ ...tasks, ...newTaskObject });
};
2️⃣ 삭제 기능
_deleteTask 함수에서 id를 이용해 해당항목을 삭제하고
onPressOut 이벤트를 정의해 아이콘에 기능을 적용해주었다.
const _deleteTask = id => {
const currentTasks = Object.assign({}, tasks);
delete currentTasks[id];
setTasks(currentTasks);
}
<TouchableOpacity onPressOut={_onPressOut}>
<Icon source={type} completed={completed} />
</TouchableOpacity>
3️⃣ 완료 기능
completed 값을 통해 완료 - 미완료 상태가 전환되도록 함수를 정의했다.
id를 이용해 해당 항목의 완료 상태를 전달하도록 _toggleTask 함수를 만들었고
삼항연산자를 사용해 아이콘을 상태에 따라 불러와주었다.
또한,
명확한 구분을 위해 complete 값에 따른 스타일을 조정해서 기능을 완성했다.
const _toggleTask = id => {
const currentTasks = Object.assign({}, tasks);
currentTasks[id]['completed'] = !currentTasks[id]['completed'];
setTasks(currentTasks);
}
const Contents = styled.Text`
flex: 1;
font-size: 24px;
color: ${({ theme, completed }) => (completed ? theme.done : theme.text)};
text-decoration-line: ${({ completed }) => (completed ? 'line-through' : 'none')}
`;
4️⃣ 수정 기능
수정은 버튼을 클릭했을 때 해당 항목이 Input 컴포넌트로 변경되면서 내용을 수정하도록 만들었다.
_updateTask 함수에서 id를 통해 해당 항목을 전달했고
isEditing 변수로 상태 변화를 감지해 Input 컴포넌트를 불러오도록 정의했다.
const _onSubmitEditing = () => {
if (isEditing) {
const editedTask = Object.assign({}, item, { text });
setIsEditing(false);
updateTask(editedTask);
}
}
return isEditing ? (
<Input
value={text}
onChangeText={text => setText(text)}
onSubmitEditing={_onSubmitEditing}
onBlur={_onBlur}
/>
) : (
<Container>
<IconButton
type={item.completed ? images.completed : images.uncompleted}
id={item.id}
onPressOut={toggleTask}
completed={item.completed}
/>
<Contents completed={item.completed}>{item.text}</Contents>
{item.completed || (
<IconButton
type={images.update}
onPressOut={_handleUpdateButtonPress}
/>)
}
<IconButton
type={images.delete}
id={item.id}
onPressOut={deleteTask}
completed={item.completed}
/>
</Container>
)
5️⃣ 입력 취소 기능
onBlur 함수를 정의해 focus가 input에서 사라졌을 시 입력이 취소되도록 했다.
_onBlur 함수에 setNewTask를 빈 값으로 줌으로써 간단히 정의해주었다.
const _onBlur = () => {
setNewTask('');
}
const _onBlur = () => {
if (isEditing) {
setIsEditing(false);
setText(item.text);
}
}
데이터 저장하기
리액트 네이티브에서는 AsyncStorage를 이용해 로컬에 데이터를 저장하고 불러오는 기능을 구현할 수 있다.
- AsyncStorage : 비동기로 동작, 문자열로 된 키-값 형태 데이터를 기기에 저장하고 불러옴
아직 사용할 수 있지만 공식문서에서는 Deprecated라 되어 있다.
AsyncStorage대신 react- native-community 에서 관리하는 async - storage를 설치해서 사용하는 것이 권장된다.
+ @react-native-community/async-storage는 더 이상 유지보수되지 않기 때문에
@react-native-async-storage/async-storage 패키지를 사용하는 것을 권장한다.
npm install @react-native-async-storage/async-storage
tasks 초기값은 삭제 후 AsnycStorage를 이용해 tasks 라는 문자열로 키를 전달해 항목들을 문자열로 변환해서 저장하는
_saveTasks 함수를 정의했다.
값이 변경될때마다 저장해야하므로 setTasks 세터함수를 이용하는 곳에서 호출하도록 수정해주었다.
const [tasks, setTasks] = useState({});
const _saveTask = async tasks => {
try {
await AsyncStorage.setItem('tasks', JSON.stringify(tasks));
setTasks(tasks);
} catch (e) {
console.log(e);
}
};
const _addTask = () => {
const ID = Date.now().toString();
const newTaskObject = {
[ID]: { id: ID, text: newTask, completed: false },
};
setNewTask('');
_saveTask({ ...tasks, ...newTaskObject });
};
const _deleteTask = id => {
const currentTasks = Object.assign({}, tasks);
delete currentTasks[id];
_saveTask(currentTasks);
}
const _toggleTask = id => {
const currentTasks = Object.assign({}, tasks);
currentTasks[id]['completed'] = !currentTasks[id]['completed'];
_saveTask(currentTasks);
}
const _updateTask = item => {
const currentTasks = Object.assign({}, tasks);
currentTasks[item.id] = item;
_saveTask(currentTasks);
}
저장된 데이터를 불러오기 위해 _loadTasks 함수를 정의해주었고 객체로 변환해 tasks에 입력하도록 해주었다.
첫 화면이 나타나기 전에 불러온 항목이 렌더링 완료되게 하려면 Expo에서 제공하는 AppLoading 컴포넌트를 이용해 쉽게 구현할 수 있다.
+ AppLoading 컴포넌트는 expo 버전 44 이후부터는 expo-app-loading 패키지로 분리되었기 때문에 별도의 패키지를 설치해야한다.
npm install expo-app-loading
- AppLoading : 특정 조건에서 로딩 화면이 유지되도록 하는 기능, 렌더링 전에 처리해야하는 작업 수행할 때 유용
- startAsync: AppLoading 컴포넌트가 동작하는 동안 실행될 함수
- onFinish: startAsync가 완료되면 실행할 함수
- onError: startAsync에서 오류가 발생하면 실행할 함수
const _loadTasks = async () => {
const loadTasks = await AsyncStorage.getItem('tasks');
setTasks(JSON.parse(loadTasks || '{}'));
};
<AppLoading
startAsync={_loadTasks}
onFinish={() => setIsReady(true)}
onError={console.error}
/>
https://github.com/BB545/ReactNative_ExpoProject/tree/master/react-native-todo
'공부 > ReactNative' 카테고리의 다른 글
[ReactNative] useMemo, 커스텀 Hooks | 처음 배우는 리액트네이티브 6-4장, 6-5장 (0) | 2025.01.22 |
---|---|
[ReactNative] useState, useEffect, useRef | 처음 배우는 리액트네이티브 6-1장, 6-2장, 6-3장 (0) | 2025.01.22 |
[ReactNative] 스타일 | 처음 배우는 리액트네이티브 4장 (0) | 2025.01.21 |
[ReactNative] props, state, event | 처음 배우는 리액트네이티브 3-3장, 3-4장 (0) | 2025.01.16 |
[ReactNative] 컴포넌트 | 처음 배우는 리액트네이티브 3-2장 (0) | 2025.01.15 |