📎 https://github.com/choi-day/react-native-todo
프로젝트를 생성한다
expo init react-naitive-todo
스타일드 컴포넌트 라이브러리와 prop-types 라이브러리를 설치한다
npm install styled-components prop-types
src/App.js 작성
import React from "react";
import styled, {ThemeProvider} from 'styled-components/native'
import {theme} from './theme';
const Container = styled.View`
flex: 1;
background-color: ${({theme}) => theme.background};
align-items: center;
justify-content: center;
`;
export default function App() {
return (
<ThemeProvider theme={theme}>
<Container></Container>
</ThemeProvider>
)
}
App.js 수정
import App from './src/App'
export default App;
📝 현재 동작
1. App을 ThemeProvider로 감싼다.
2. theme 안에 정의된 값들이 모든 styled-component에 전달된다.
3. Container는 theme.background 색을 배경으로 사용한다.
4. flex 1로 화면 전체를 채우고, 자식을 중앙에 정렬한다.
App.js 수정(title 삽입)
import React from "react";
import { StatusBar } from "react-native";
import styled, {ThemeProvider} from 'styled-components/native'
import {theme} from './theme';
const Container = styled.SafeAreaView`
flex: 1;
background-color: ${({theme}) => theme.background};
align-items: center;
justify-content: flex-start;
`;
const Title = styled.Text`
font-size: 40px;
font-weight: 600;
color: ${({theme}) => theme.main};
align-self: flex-start;
margin: 20px;
`;
export default function App() {
return (
<ThemeProvider theme={theme}>
<Container>
<StatusBar
barStyle="light-content"
backgroundColor={theme.background}
/>
<Title>TODO List</Title>
</Container>
</ThemeProvider>
);
}
📝 현재 동작
- 화면은 SafeArea 위에서 시작됨 (노치/상단바 안 겹침)
- StatusBar 텍스트는 흰색 (light-content)
- Android에서는 StatusBar 배경도 theme.background 색으로 변경됨
- 배경은 theme.background 색
- TODO List 제목은 왼쪽 상단에 크게 표시됨
component/input.js 작성
import React from 'react';
import styled from 'styled-components/native';
import { Dimensions, useWindowDimensions } from 'react-native';
import PropTypes from 'prop-types';
import { theme } from '../theme';
const StyledInput = styled.TextInput.attrs(({ theme }) => ({
placeholderTextColor: theme.main,
}))`
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 = ({placeholder, value, onChangeText, onSubmitEditing,}) => {
const width = useWindowDimensions().width
return( <StyledInput
width = {width}
placeholder={placeholder}
maxLength={50}
autoCapitalize='none'
autoCorrect={false}
returnKeyType='done'
keyboardAppearance='dark'
value={value}
onChangeText={onChangeText}
onSubmitEditing={onSubmitEditing}
/>
);
};
Input.propTypes = {
placeholder: PropTypes.string,
value: PropTypes.string.isRequired,
onChangeText: PropTypes.func.isRequired,
onSubmitEditing: PropTypes.func.isRequired,
};
export default Input;
📝 Input 컴포넌트 동작
- 현재 화면 너비를 자동으로 가져옴 (useWindowDimensions)
→ 기기 크기에 따라 입력창 너비 자동 조절- 입력창 폭을 화면 너비 - 40px 로 설정
- placeholder 색, 배경색, 글자색을 theme에서 가져와 적용
- 입력 이벤트를 외부에서 제어하도록 설정됨
- value → 현재 입력값
- onChangeText → 입력할 때마다 실행
- onSubmitEditing → 엔터(완료) 눌렀을 때 실행
- TextInput 공통 설정 적용됨
- 자동대문자 없음
- 자동완성/자동수정 꺼짐
- returnKeyType=‘done’
- iOS 다크 키보드 사용
App.js 수정
export default function App() {
const [newTask, setNewTask] = useState('');
const _addTask = () => {
alert(`Add: ${newTask}`);
setNewTask('');
};
const _handleTextChange = text => {
setNewTask(text);
}
...
<Input
placeholder = "+ Add a Task"
value = {newTask}
onChangeText={_handleTextChange}
onSubmitEditing={_addTask}
/>
📝 수정된 동작 요약
사용자가 입력창에 적는 텍스트가 newTask 에 저장됨.
입력창에서 글자 입력할 때
→ onChangeText → _handleTextChange 실행
→ newTask 값이 실시간 업데이트됨.
엔터(Submit) 키를 누르면
→ onSubmitEditing → _addTask 실행
→ 현재 입력된 newTask를 alert로 표시
→ 입력이 완료되면 newTask 값을 빈 문자열로 초기화(입력창 비워짐).
Input 컴포넌트는 newTask를 value로 받아 표시함
→ 상태 변화가 입력창에 실시간 반영됨.
아이콘이 될 이미지를 assets/icons 아래 다운 받는다. 파일 명을 동일한 이름으로 사용하면서 뒤에 @2x, @3x 를 붙이면 자동으로 화면에 알맞은 이미지를 불러와 사용한다

src/images.js
import CheckBoxOutline from '../assets/icons/check_box_outline.png';
import CheckBox from '../assets/icons/check_box.png';
import DeleteForever from '../assets/icons/delete_forever.png';
import Edit from '../assets/icons/edit.png';
export const images = {
uncompleted: CheckBoxOutline,
completed: CheckBox,
delete: DeleteForever,
update: Edit,
};
📝 images.js 동작 요약
- 각 아이콘 이미지를 파일에서 불러온다.
- 체크박스 비활성
- 체크박스 활성
- 삭제 아이콘
- 수정 아이콘
- 불러온 이미지들을 하나의 객체(images)에 정리해서 저장한다.
components/IconButton.js
import React from 'react';
import { TouchableOpacity } from 'react-native';
import styled from 'styled-components/native';
import PropTypes from 'prop-types';
import { images } from '../images';
const Icon = styled.Image`
tint-color: ${({ theme, completed }) =>
completed ? theme.done : theme.text};
width: 30px;
height: 30px;
margin: 10px;
`;
const IconButton = ({ type, onPressOut}) => {
return (
<TouchableOpacity onPressOut={_onPressOut}>
<Icon source={type} completed={completed} />
</TouchableOpacity>
);
};
IconButton.propTypes = {
type: PropTypes.oneOf(Object.values(images)).isRequired,
onPressOut: PropTypes.func,
};
export default IconButton;
📝 코드 동작 요약
- IconButton은 TouchableOpacity 를 사용해 터치 가능한 버튼을 만든다.
- 버튼 안에는 type으로 전달된 아이콘 이미지(source) 를 표시한다.
- 아이콘은 styled-components로 스타일링되어 30×30 크기 + margin 10 으로 표시된다.
- 버튼을 떼는 순간(onPressOut) 전달된 함수가 실행된다.
- type은 반드시 images 객체 안의 아이콘 중 하나여야 하고, PropTypes로 타입 검사를 한다.
components/task.js
import React, { useState } from 'react';
import styled from 'styled-components/native';
import PropTypes from 'prop-types';
import IconButton from './IconButton';
import { images } from '../images';
const Container = styled.View`
flex-direction: row;
align-items: center;
background-color: ${({ theme }) => theme.itemBackground};
border-radius: 10px;
padding: 5px;
margin: 3px 0px;
`;
const Contents = styled.Text`
flex: 1;
font-size: 24px;
color: ${({ theme }) => theme.text};
`;
const Task = ({ text }) => {
return (
<Container>
<IconButton type={images.uncompleted}/>
<Contents>{text}</Contents>
<IconButton type={images.update} />
<IconButton type={images.delete} />
</Container>
);
};
Task.propTypes = {
text: PropTypes.string.isRequired
};
export default Task;
📝 Task 컴포넌트 동작 요약
- Task는 한 줄짜리 할 일 아이템을 표시하는 컴포넌트이다.
- 왼쪽에는 완료 상태를 나타내기 위한 체크박스 아이콘이 배치된다.
- 가운데에는 text prop으로 전달된 할 일 내용이 표시되며, flex:1로 공간을 넓게 차지한다.
- 오른쪽에는 수정 아이콘과 삭제 아이콘이 각각 버튼 형태로 배치된다.
- text는 반드시 문자열이어야 하고 PropTypes로 타입 검사를 한다.
App.js 수정
export default function App() {
const width = Dimensions.get('window').width;
const [newTask, setNewTask] = useState('');
const [tasks, setTasks] = useState({
'1': { id: '1', text: 'Hanbit', completed: false },
'2': { id: '2', text: 'React Native', completed: true },
'3': { id: '3', text: 'React Native Sample', completed: false },
'4': { id: '4', text: 'Edit TODO Item', completed: false },
});
const _addTask = () => {
const ID = Date.now().toString();
const newTaskObject = {
[ID]: { id: ID, text: newTask, completed: false },
};
setNewTask('');
setTasks({ ...tasks, ...newTaskObject });
...
return (
...
<List width={width}>
{Object.values(tasks)
.reverse()
.map(item => (
<Task key={item.id} text={item.text} />
))}
</List>
...
);
}
📝 ****추가 기능 동작 요약
기존 newTask만 관리하던 상태에 tasks라는 할 일 목록 상태 객체를 추가한다.
→ 각 할 일은 id, text, completed 필드를 가진다.
_addTask 함수는 Date.now().toString()으로 고유 ID를 생성하고,
그 ID를 key로 갖는 새 할 일 객체(newTaskObject)를 만든다.
새로 만든 할 일을 기존 tasks와 합쳐서 상태를 갱신한다.
→ 입력창은 setNewTask('')로 비워져서 다시 입력할 수 있게 된다.
영역에서는Object.values(tasks)로 상태에 있는 모든 할 일을 꺼내 동적 렌더링한다.
.reverse()를 사용해 가장 최근에 추가한 할 일일수록 위에 보이도록 목록 순서를 뒤집은 뒤 렌더링한다.
App.js
const _deleteTask = id => {
const currentTasks = Object.assign({}, tasks);
delete currentTasks[id];
setTasks(currentTasks);
};
...
<Task
key={item.id}
item={item}
deleteTask={_deleteTask}
/>
Task.js
<IconButton
type={images.delete}
id={item.id}
onPressOut={deleteTask}
/>
IconButton.js
const IconButton = ({ type, onPressOut, id }) => {
const _onPressOut = () => {
if (typeof onPressOut === 'function') onPressOut(id);
};
return (
<TouchableOpacity onPressOut={_onPressOut}>
<Icon source={type} />
</TouchableOpacity>
);
};
📝 삭제 기능 동작 요약
- 삭제 아이콘을 누르면 IconButton 내부에서 id를 포함한 onPressOut이 실행된다.
- Task 컴포넌트는 deleteTask 함수에 해당 id를 전달한다.
- App.js의 deleteTask 함수는 tasks 객체를 복사한 뒤 해당 id를 가진 항목을 삭제한다.
- 삭제된 tasks 객체를 setTasks로 다시 저장하여 화면이 갱신된다.
- 결과적으로 터치한 할 일 항목이 목록에서 사라진다.
App.js
const _toggleTask = id => {
const currentTasks = Object.assign({}, tasks);
currentTasks[id]['completed'] = !currentTasks[id]['completed'];
setTasks(currentTasks);
};
...
<Task
key={item.id}
item={item}
deleteTask={_deleteTask}
toggleTask={_toggleTask}
/>
Task.js
<IconButton
type={item.completed ? images.completed : images.uncompleted}
id={item.id}
onPressOut={toggleTask}
completed={item.completed}
/>
...
<Contents completed={item.completed}>{item.text}</Contents>
...
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'};
`;
IconButton.js
const IconButton = ({ type, onPressOut, id, completed }) => {
const _onPressOut = () => {
if (typeof onPressOut === 'function') onPressOut(id);
};
return (
<TouchableOpacity onPressOut={_onPressOut}>
<Icon source={type} completed={completed} />
</TouchableOpacity>
);
};
📝 완료 기능 동작 요약
- 체크 아이콘을 누르면 IconButton 내부에서 id를 포함한 onPressOut 함수가 실행된다.
- Task 컴포넌트는 전달받은 toggleTask 함수에 해당 id를 전달한다.
- App.js의 toggleTask 함수는 tasks 객체에서 해당 id의 completed 값을 반전시킨다.
- setTasks로 업데이트된 tasks를 저장하면 UI가 자동으로 다시 렌더링된다.
- 완료된 항목은 체크박스 이미지가 변경되고, 텍스트 색상과 스타일이 line-through로 바뀐다.
App.js
const [isEditing, setIsEditing] = useState(false);
const [currentTask, setCurrentTask] = useState({});
...
const _startEditing = item => {
setIsEditing(true);
setCurrentTask(item);
};
...
const _updateTask = () => {
const updatedTasks = Object.assign({}, tasks);
updatedTasks[currentTask.id] = currentTask;
saveTasks(updatedTasks);
setIsEditing(false);
setCurrentTask({});
};
Task.js
<IconButton
type={images.update}
onPressOut={() => startEditing(item)}
/>
Input.js
<Input
placeholder="+ Add a Task"
value={isEditing ? currentTask.text : newTask}
onChangeText={text =>
isEditing
? setCurrentTask({ ...currentTask, text })
: setNewTask(text)
}
onSubmitEditing={isEditing ? updateTask : addTask}
/>
📝 수정 기능 동작 요약
- 수정 기능을 위해 isEditing 상태와 현재 수정 중인 할 일을 저장하는 currentTask 상태를 추가한다.
- 수정 버튼을 누르면 startEditing이 실행되어 수정 모드로 전환되고, 입력창에는 해당 할 일의 텍스트가 채워진다.
- 입력창에서 텍스트를 변경하면 currentTask.text 값만 업데이트된다.
- 엔터(Submit)를 누르면 updateTask가 실행되어 해당 id의 할 일을 새로운 텍스트로 덮어쓴다.
- 수정 후에는 isEditing이 false가 되어 입력창이 다시 기본 입력 모드로 돌아간다.
AsyncStorage를 이용해 로컬에 데이터를 저장하고 불러올 수 있다. 비동기로 동작하며 문자열로 된 키-값의 형태의 데이터를 기기에 저장하고 불러오는 기능을 제공한다.
expo install @react-native-community/async-storage
const _saveTasks = async tasks => {
try {
await AsyncStorage.setItem('tasks', JSON.stringify(tasks));
setTasks(tasks);
} catch (e) {
console.
error(e);
}
}
이후 setTasks를 _saveTasks로 바꿔준다
📝 데이터 저장 동작 요약
- AsyncStorage에서 'tasks' 키로 저장된 데이터를 가져온다.
- 불러온 문자열 데이터를 JSON.parse 해서 tasks 상태에 넣고, 에러가 나면 콘솔에 출력한다.
- 할 일을 추가/삭제/완료 토글할 때마다 _saveTasks를 통해 tasks 상태를 업데이트하면서 동시에 AsyncStorage에 다시 JSON.stringify(tasks) 형태로 저장한다.
const _loadTasks = async () => {
const loadedTasks = await AsyncStorage.getItem('tasks');
setTasks(JSON.parse(loadedTasks || '{}'));
};
📝 데이터 불러오기 동작 요약
- AsyncStorage에서 'tasks' 키로 저장된 데이터를 읽고, 없으면 빈 객체 {}를 기본값으로 사용한다.
- 가져온 문자열 데이터를 JSON.parse 후 tasks 상태에 넣어 화면에서 사용할 수 있게 만든다.

중간에 stylecomponents 오류가 나서 한참을 삽질하다가 아래 글을 참고하여 고쳤다... 버전때문에 오류가 많이 나는 듯 하다
https://github.com/styled-components/styled-components/issues/5576