Todo list 기본 UI 만드는 방법은 아래 게시글에 자세히 나와있다.
To-do list 만들기(1)

4. 기능 구현하기

1) add : 추가 기능

to-do list에 필요한 값

  • id : 항목을 구분할 수 있는 고유한 값
  • text : 항목 내용
  • completed : 완료 여부

구현

  • 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 });
  };

2) delete : 삭제 기능

_deleteTask라는 함수를 만든다.

구현

  • 삭제 버튼을 클릭했을 때 항목의 id를 이용하여 tasks에서 해당 항목을 삭제한다.
  //삭제 함수
  const _deleteTask = (id) => {
    const currentTasks = Object.assign({}, tasks);
    delete currentTasks[id];
    setTasks(currentTasks);
  };
  • Task 컴포넌트에 항목 삭제 함수(_deleteTaks)와 함께 항목 내용 전체(item 자체!)를 전달해 자식 컴포넌트에서도 항목의 id를 확인할 수 있도록 수정한다.

src/App.js

      <ScrollView>
        {Object.values(tasks)
          .reverse()
          .map((item) => (
            <Task key={item.id} item = {item} deleteTask={_deleteTask}/>
          ))}
      </ScrollView>
  • Task 컴포넌트에서는 받은 itemtext를 랜더링하고 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>
  );
};
  • 이제 IconButton 컴포넌트에서 전달된 함수를 이용하도록 해야한다.
    순서 : App.js -> Task 컴포넌트 -> IconButton 컴포넌트
    IconButton 컴포넌트가 클릭되었을 때 전달된 함수가 호출되도록 작성한다.

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: () => {},
};

결과물

3) complete : 완료 기능

_toggleTask라는 함수를 만든다.

  • 체크박스를 누르면 완료, 다시 누르면 미완료 상태로 돌아올 수 있도록 한다.

구현

  • 함수가 호출될 때마다 완료 여부를 나타내는 completed 값이 전환되는 함수를 작성한다.

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>
  • Task 컴포넌트에서는 props로 전달된 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>
  );
};

결과물

토글이 잘 되는 것을 확인할 수 있다. 여기까지만 구현해도 되지만 편의를 위해 추가로 더 개발을 하고자 한다.

  1. 완료된 항목은 수정 버튼이 나타나지 않는다.
  2. 완료된 항목은 글자색이 연해지고 취소선이 나타난다.
  3. 완료된 항목의 icon도 연해진다.
  1. 완료된 항목은 수정 버튼이 나타나지 않는다.
{item.completed || <IconButton type={images.edit} />}
  1. 완료된 항목은 글자색이 연해지고 취소선이 나타난다.
<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',
  },
});
  1. 완료된 항목의 icon도 연해진다.

IconButton 컴포넌트에도 completed를 전달하여 completed 값에 따라 다른 스타일이 적용되도록 한다.
해결중...

4) update : 수정 기능

수정 버튼을 클릭하면 해당 항목이 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 컴포넌트의 값으로 설정한다.
  • isEditingtext변수는 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 컴포넌트가 랜더링 되도록 한다. 만약 isEditingtrue이면 Input 컴포넌트가 나타나고, 만약 isEditingfalse이면 원래의 컴포넌트 대로 나온다.

  • 만약 수정중인 경우,
    input 컴포넌트에 text, onChangeText, _onSubmitEditing 함수를 전달한다. (_onSubmitEditing 함수가 잘 이해되지 않는다.)
  • Input 컴포넌트에서 완료버튼을 누르면 App 컴포넌트에서 전달된 updateTask 함수가 호출되도록 한다.
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>
  );
};

5) 부가기능 : 입력 취소하기

0개의 댓글

Powered by GraphCDN, the GraphQL CDN