Todo list 기본 UI 만드는 방법은 아래 게시글에 자세히 나와있다.
To-do list 만들기(1)
to-do list에 필요한 값
구현
useState
를 이용해 할 일 목록을 저장하고 관리할 tasks 변수를 생성한 후 초기값으로 임의의 내용을 입력한다..reverse()
: 최신 항목이 가장 앞에 보이도록 tasks를 역순으로 렌더링.map()
: 고차함수 사용src/App.js
export default function App() {
const [newTask, setNewTask] = useState('');
const [tasks, setTasks] = useState({
1: { id: '1', text: 'todo list 1', completed: false },
2: { id: '2', text: 'todo list 2', completed: false },
3: { id: '3', text: 'todo list 3', completed: false },
4: { id: '4', text: 'todo list 4', completed: false },
5: { id: '5', text: 'todo list 5', completed: false },
});
//생략
return (
<View style={styles.container}>
<StatusBar style="auto" />
<Title title="Todo List✔️"></Title>
<Input
value={newTask}
onChangeText={_handleTextChange}
onSubmitEditing={_addTask}
/>
<ScrollView>
{Object.values(tasks)
.reverse()
.map((item) => (
<Task text={item.text} />
))}
</ScrollView>
</View>
);
}
Warning : Each child in a list should have a unique "key" prop.
list를 사용할 때 항상 나오는 warning으로 key
를 지정하지 않아서 나타나는 경고 메세지이다.
key
: 리액트에서 컴포넌트 배열을 렌더링 했을 때 어떤 아이템이 추가, 수정, 삭제 되었는지 식별하는 것을 돕는 고유값으로 리액트에서 특별하게 관리되며, 자식 컴포넌트의 props로 전달되지 않는다.
해결 방법
각 항목마다 고유한 id를 key로 지정한다.
<ScrollView>
{Object.values(tasks)
.reverse()
.map((item) => (
<Task key = {item.id} text={item.text} />
))}
</ScrollView>
이제 Input 컴포넌트에서 값을 입력했을 때 내용이 추가될 수 있도록 _addTask()
함수를 수정한다.
id
: 여기서 id는 할 일 항목이 추가되는 시간의 타임스탬프를 이용할 것이다.text
: Input 컴포넌트에 입력된 값을 지정한다.completed
: 새로 입력되는 항목이므로 완료 여부를 나타내는 completed는 항상 false가 된다.newTask
의 값을 빈 문자열로 지정해서 Input 컴포넌트를 초기화하고, 기존의 목록을 유지한 상태에서 새로운 항목이 추가되도록 구성한다. src/App.js
const _addTask = () => {
const ID = Date.now().toString();
const newTaskObject = {
[ID]: { id: ID, text: newTask, completed: false },
};
setNewTask('');
setTasks({ ...tasks, ...newTaskObject });
};
_deleteTask
라는 함수를 만든다.
구현
//삭제 함수
const _deleteTask = (id) => {
const currentTasks = Object.assign({}, tasks);
delete currentTasks[id];
setTasks(currentTasks);
};
_deleteTaks
)와 함께 항목 내용 전체(item 자체!)를 전달해 자식 컴포넌트에서도 항목의 id를 확인할 수 있도록 수정한다.src/App.js
<ScrollView>
{Object.values(tasks)
.reverse()
.map((item) => (
<Task key={item.id} item = {item} deleteTask={_deleteTask}/>
))}
</ScrollView>
item
의 text
를 랜더링하고 deleteTask
함수를 통해 버튼이 눌렸다가 떼질 때 함수를 실행한다.src/components/Task.js
const Task = ({ item, deleteTask }) => {
return (
<View style={styles.container}>
<IconButton type={images.uncompleted} />
<Text style={{ fontSize: 20, flex: 1 }}>{item.text}</Text>
<IconButton type={images.edit} />
<IconButton type={images.delete} id={item.id} onPressOut={deleteTask}/>
</View>
);
};
src/components/IconButton.js
const IconButton = ({ type, onPressOut, id }) => {
const _onPressOut = () => {
onPressOut(id);
};
return (
<TouchableOpacity style={styles.iconbutton} onPressOut={_onPressOut}>
<Image source={type} />
</TouchableOpacity>
);
};
추가 기능 : defaultProps
props로 onPressOut이 전달되지 않았을 경우에도 문제가 발생하지 않도록 defaultProps를 이용해 onPressOut의 기본값을 지정한다.
IconButton.defaultProps = {
onPressOut: () => {},
};
결과물
_toggleTask
라는 함수를 만든다.
구현
src.App.js
const _toggleTask = (id) => {
const currentTasks = Object.assign({}, tasks);
currentTasks[id]['completed'] = !currentTasks[id]['completed'];
setTasks(currentTasks);
};
_deleteTask()
함수와 마찬가지로 작성된 함수를 Task 컴포넌트로 전달한다.src.App.js
<ScrollView>
{Object.values(tasks)
.reverse()
.map((item) => (
<Task
key={item.id}
item={item}
deleteTask={_deleteTask}
toggleTask={_toggleTask}
/>
))}
</ScrollView>
toggleTask
함수를 완료 상태를 나타내는 버튼의 onPressOut
으로 설정하고, 항목 완료 여부에 따라 버튼 이미지가 다르게 나타나도록 수정한다. src/components/Task.js
const Task = ({ item, deleteTask, toggleTask }) => {
return (
<View style={styles.container}>
<IconButton
type={item.completed ? images.completed : images.uncompleted}
id={item.id}
onPressOut={toggleTask}
/>
//생략
</View>
);
};
결과물
토글이 잘 되는 것을 확인할 수 있다. 여기까지만 구현해도 되지만 편의를 위해 추가로 더 개발을 하고자 한다.
- 완료된 항목은 수정 버튼이 나타나지 않는다.
{item.completed || <IconButton type={images.edit} />}
- 완료된 항목은 글자색이 연해지고 취소선이 나타난다.
<Text style={item.completed ? styles.completed : styles.text}>{item.text}</Text>
const styles = StyleSheet.create({
text: {
fontSize: 20,
flex: 1,
color: 'black',
},
completed: {
fontSize: 20,
flex: 1,
color: 'gray',
textDecorationLine: 'line-through',
},
});
- 완료된 항목의 icon도 연해진다.
IconButton 컴포넌트에도 completed를 전달하여 completed 값에 따라 다른 스타일이 적용되도록 한다.
해결중...
수정 버튼을 클릭하면 해당 항목이 Input 컴포넌트로 변경되면서 내용을 수정할 수 있도록 구현한다.
우선 App.js 컴포넌트에서 수정 완료된 항목이 전달되면 task에서 해당 항목을 변경하는 함수(_updateTask
)를 작성한다. 그리고 Task 컴포넌트에서 사용할 수 있도록 함수를 전달한다.
export default function App() {
//생략
const _updateTask = (item) => {
const currentTasks = Object.assign({},tasks);
currentTasks[item.id]=item;
setTasks(currentTasks);
}
return (
//생략
<ScrollView>
{Object.values(tasks)
.reverse()
.map((item) => (
<Task
key={item.id}
item={item}
deleteTask={_deleteTask}
toggleTask={_toggleTask}
updateTask={_updateTask}
/>
))}
</ScrollView>
);
}
이제 Task 컴포넌트에서 수정 버튼을 클릭하면 항목의 현재 내용을 가진 Input 컴포넌트가 랜더링되어 사용자가 수정할 수 있도록 한다.
isEditing
변수를 생성하고 수정 버튼이 클릭되면 값이 변경되도록 한다. text
변수를 생성하고 Input 컴포넌트의 값으로 설정한다.isEditing
과 text
변수는 useState
로 관리한다.src/components/Task.js
const Task = ({ item, deleteTask, toggleTask }) => {
const [isEditing, setIsEditing] = useState(false);
const [text, setText] = useState(item.text);
return (
//생략
);
};
todo list 화면에서 edit icon(연필 모양의 icon)을 눌렀을 경우 _handleUpdateButtonPress
함수가 호출 된다. 이 함수는 isEditing
변수를 true로 바꿔주면서 수정중인 상태로 바꿔준다.
const _handleUpdateButtonPress = () => {
setIsEditing(true);
};
//중간 생략
{item.completed || (
<IconButton type={images.edit} onPressOut={_handleUpdateButtonPress} />
)}
화면은 isEditing
의 값에 따라 항목 내용이 아닌 Input 컴포넌트가 랜더링 되도록 한다. 만약 isEditing
이 true
이면 Input
컴포넌트가 나타나고, 만약 isEditing
이 false
이면 원래의 컴포넌트 대로 나온다.
text
, onChangeText
, _onSubmitEditing
함수를 전달한다. (_onSubmitEditing
함수가 잘 이해되지 않는다.)const Task = ({ item, deleteTask, toggleTask, updateTask }) => {
const [isEditing, setIsEditing] = useState(false);
const [text, setText] = useState(item.text);
const _handleUpdateButtonPress = () => {
setIsEditing(true);
};
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}
/>
) : (
<View style={styles.container}>
<IconButton
type={item.completed ? images.completed : images.uncompleted}
id={item.id}
onPressOut={toggleTask}
/>
<Text style={item.completed ? styles.completed : styles.text}>
{item.text}
</Text>
{item.completed || (
<IconButton type={images.edit} onPressOut={_handleUpdateButtonPress} />
)}
<IconButton type={images.delete} id={item.id} onPressOut={deleteTask} />
</View>
);
};