수정 기능 만들기

비얌·2023년 2월 22일
6
post-thumbnail

🧹 개요

이어서 투두리스트 수정 기능을 만들어보자.



✨ 결과 미리보기

완성된 코드이다.

// TodoItem.js
import styles from "./TodoItem.module.css";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faTrashCan, faPenToSquare } from "@fortawesome/free-regular-svg-icons";
import { faCheck, faXmark} from "@fortawesome/free-solid-svg-icons";
import { useState, useRef } from "react";

const TodoItem = (props) => {
  const editedText = useRef(null);

  const [isEditClicked, setIsEditClicked] = useState(false);
  const [isDeleteClicked, setIsDeleteClicked] = useState(false);
  const [isChecked, setIsChecked] = useState(false);
  const [updatedText, setUpdatedText] = useState('');

  const submitEditedContent = () => {
    if (updatedText === '') {
      setIsEditClicked(false);
      return;
    }
    props.submitEditedContent(updatedText, props.todo.id);
    setUpdatedText('');
    setIsEditClicked(false);
  }

  const openEdit = () => {
    setIsEditClicked(true);
    editedText.current.focus();
  }

  const openDelete = () => {
    setIsDeleteClicked(true);
  };

  const cancelEdit = () => {
    setUpdatedText('');
    setIsEditClicked(false);
  }

  const cancelDelete = () => {
    setIsDeleteClicked(false);
  };


  const onDelete = (id) => {
    props.onDelete(id);
  };

  return (
    <div className={styles.textAndDeteteBtn}>
      <div className={styles.checkboxAndText}>
        <label>
          <input type="checkbox" onClick={() => setIsChecked(!isChecked)} />
          <div>
            <FontAwesomeIcon icon={faCheck} color="#1a202c" className={styles.checkIcon} />
          </div>
        </label>
        {isEditClicked
          ? (
            <div className={isChecked? `${styles.text} ${styles.checked}` : `${styles.text}`}>
              <input
                value={updatedText}
                onChange={(e) => setUpdatedText(e.target.value)}
                ref={editedText}
              />
            </div>
          ) : (
          <div className={isChecked? `${styles.text} ${styles.checked}` : `${styles.text}`}>
            {props.todo.text}
          </div>)
        }
        
      </div>
      <div>
      <div className={styles.editBtnAndDeleteBtn}>
        {(isEditClicked && !isDeleteClicked) ? (
            <div>
              <button className={styles.submitIcon} onClick={submitEditedContent}>
                <FontAwesomeIcon icon={faCheck} size="2x" color="white" />
              </button>
              <button className={styles.cancelIcon} onClick={cancelEdit}>
                <FontAwesomeIcon icon={faXmark} size="2x" color="white" />
              </button>
            </div>
          ) : ((!isEditClicked && isDeleteClicked) ? (
            <div>
              <button
                className={styles.submitIcon}
                onClick={() => onDelete(props.todo.id)}
              >
                <FontAwesomeIcon icon={faCheck} size="2x" color="white" />
              </button>
              <button className={styles.cancelIcon} onClick={cancelDelete}>
                <FontAwesomeIcon icon={faXmark} size="2x" color="white" />
              </button>
            </div>
          ) : (
            <div>
              <button className={styles.editIcon} onClick={openEdit}>
                <FontAwesomeIcon icon={faPenToSquare} size="2x" color="white" />
              </button>
              <button className={styles.delteIcon} onClick={openDelete}>
                <FontAwesomeIcon icon={faTrashCan} size="2x" color="white" />
              </button>
            </div>
          ))}
        </div>
      </div>
    </div>
  );
};

export default TodoItem;

// Todo.js
import { useState } from 'react';
import TodoInput from '../TodoInput/TodoInput';
import TodoList from '../TodoList/TodoList';
import styles from './Todo.module.css';

const Todo = () => {
  const [displayInputs, setDisplayInputs] = useState([]);

  const submitEditedContent = (updatedText, id) => {
    setDisplayInputs(displayInputs.map(todo => {
      if (todo.id === id) {
        return {
          ...todo,
          text: updatedText
        };
      }
      return todo;
    }))
  }

  const onSaveGoal = (goal) => {
    setDisplayInputs([...displayInputs, goal]);
  }
  
  const onDelete = (id) => {
    setDisplayInputs(displayInputs.filter((todo) => todo.id !== id));
  }
  
  return (
    <div className={styles.container}>
      <h1 className={styles.title}>목표를 이루기 위해 <br/>해야 할 것들을 적어주세요!</h1>
      <TodoInput onSaveGoal={onSaveGoal} />
      <TodoList item={displayInputs} onDelete={onDelete} submitEditedContent={submitEditedContent} />
    </div>
  );
};

export default Todo;


🛫 과정

수정 버튼 만들기

먼저 UI를 만들기 위해 삭제 버튼 왼쪽에 수정 버튼을 추가해줬다.

// TodoItem.js
<button className={styles.editIcon} onClick={openEdit}>
  <FontAwesomeIcon icon={faPenToSquare} size="2x" color="white" />
</button>

그리고 flexbox를 설정해서 이렇게 가로로 놓이게 했다.


수정 버튼을 누르면 삭제 버튼이 사라지고 대신 수정확인/수정취소 버튼만 나타나게 하기

맨 처음에는 투두의 오른쪽에 이렇게 수정버튼/삭제버튼이 있었다.

이때 수정버튼을 누르면 수정버튼과 삭제버튼이 사라지고, 수정확인/수정취소 버튼이 나타나게 하고 싶었다.(동일하게 삭제버튼을 누르면 수정버튼과 삭제버튼이 사라지고 삭제확인/삭제취소 버튼이 나타나도록)


처음에는 이렇게 됐다. 하지만 내가 원하는 것은 수정버튼을 눌렀을 때 삭제에 관련한 버튼들이 모두 사라지고, 삭제 버튼을 누르면 수정 버튼에 관한 버튼들이 모두 사라지는 것이다.


그래서 일단 그렇게 하는 것에 성공했다! 하지만 이중 삼항 연산자를 이용해서....😂


이중 삼항 연산자를 이용하여 구현하려고 한 것을 의사코드로 나타내면 다음과 같다.

(수정 버튼이 클릭되고 && 삭제 버튼이 클릭되지 않으면) 
  ? 수정확인/수정취소 버튼을 표시하고 
  : (
      (수정버튼이 클릭되지 않고 && 삭제 버튼이 클릭되면) 
       ? 삭제확인/삭제취소 버튼을 표시하고 
       : 아무것도 클릭되지 않으면(평소에) 수정 버튼과 삭제 버튼을 표시한다
     )

그리고 코드로 구현하면 아래와 같다.

// TodoItem.js
{(isEditClicked && !isDeleteClicked) ? (
  <div>
    <button className={styles.submitIcon} onClick={submitEditedContent}>
      <FontAwesomeIcon icon={faCheck} size="2x" color="white" />
    </button>
    <button className={styles.cancelIcon} onClick={cancelEdit}>
      <FontAwesomeIcon icon={faXmark} size="2x" color="white" />
    </button>
  </div>
) : ((!isEditClicked && isDeleteClicked) ? (
  <div>
    <button
      className={styles.submitIcon}
      onClick={() => onDelete(props.todo.id)}
      >
      <FontAwesomeIcon icon={faCheck} size="2x" color="white" />
    </button>
    <button className={styles.cancelIcon} onClick={cancelDelete}>
      <FontAwesomeIcon icon={faXmark} size="2x" color="white" />
    </button>
  </div>
) : (
  <div>
    <button className={styles.editIcon} onClick={openEdit}>
      <FontAwesomeIcon icon={faPenToSquare} size="2x" color="white" />
    </button>
    <button className={styles.delteIcon} onClick={openDelete}>
      <FontAwesomeIcon icon={faTrashCan} size="2x" color="white" />
    </button>
  </div>
))}

수정 기능 추가하기

수정하는 걸 어떻게 표현할까 고민했는데, 따로 모달로 띄워서 수정 후 투두에 반영하는 방법은 클릭을 여러번 해야해서 불편할 것 같았다.

그래서 수정 버튼을 누르면 수정하려는 투두 자체가 input창으로 바뀌어서 그곳에서 수정을 할 수 있게끔 하기로 했다.

먼저 완성된 코드는 아래와 같다.

import styles from "./TodoItem.module.css";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faTrashCan, faPenToSquare } from "@fortawesome/free-regular-svg-icons";
import { faCheck, faXmark} from "@fortawesome/free-solid-svg-icons";
import { useState, useRef } from "react";

const TodoItem = (props) => {
  const editedText = useRef(null);

  const [isEditClicked, setIsEditClicked] = useState(false);
  const [isDeleteClicked, setIsDeleteClicked] = useState(false);
  const [isChecked, setIsChecked] = useState(false);
  const [updatedText, setUpdatedText] = useState('');

  const submitEditedContent = () => {
    if (updatedText === '') {
      setIsEditClicked(false);
      return;
    }
    props.submitEditedContent(updatedText, props.todo.id);
    setUpdatedText('');
    setIsEditClicked(false);
  }

  const openEdit = () => {
    setIsEditClicked(true);
    editedText.current.focus();
  }

  const openDelete = () => {
    setIsDeleteClicked(true);
  };

  const cancelEdit = () => {
    setUpdatedText('');
    setIsEditClicked(false);
  }

  const cancelDelete = () => {
    setIsDeleteClicked(false);
  };


  const onDelete = (id) => {
    props.onDelete(id);
  };

  return (
    <div className={styles.textAndDeteteBtn}>
      <div className={styles.checkboxAndText}>
        <label>
          <input type="checkbox" onClick={() => setIsChecked(!isChecked)} />
          <div>
            <FontAwesomeIcon icon={faCheck} color="#1a202c" className={styles.checkIcon} />
          </div>
        </label>
        {isEditClicked
          ? (
            <div className={isChecked? `${styles.text} ${styles.checked}` : `${styles.text}`}>
              <input
                value={updatedText}
                onChange={(e) => setUpdatedText(e.target.value)}
                ref={editedText}
              />
            </div>
          ) : (
          <div className={isChecked? `${styles.text} ${styles.checked}` : `${styles.text}`}>
            {props.todo.text}
          </div>)
        }
        
      </div>
      <div>
      <div className={styles.editBtnAndDeleteBtn}>
        {(isEditClicked && !isDeleteClicked) ? (
            <div>
              <button className={styles.submitIcon} onClick={submitEditedContent}>
                <FontAwesomeIcon icon={faCheck} size="2x" color="white" />
              </button>
              <button className={styles.cancelIcon} onClick={cancelEdit}>
                <FontAwesomeIcon icon={faXmark} size="2x" color="white" />
              </button>
            </div>
          ) : ((!isEditClicked && isDeleteClicked) ? (
            <div>
              <button
                className={styles.submitIcon}
                onClick={() => onDelete(props.todo.id)}
              >
                <FontAwesomeIcon icon={faCheck} size="2x" color="white" />
              </button>
              <button className={styles.cancelIcon} onClick={cancelDelete}>
                <FontAwesomeIcon icon={faXmark} size="2x" color="white" />
              </button>
            </div>
          ) : (
            <div>
              <button className={styles.editIcon} onClick={openEdit}>
                <FontAwesomeIcon icon={faPenToSquare} size="2x" color="white" />
              </button>
              <button className={styles.delteIcon} onClick={openDelete}>
                <FontAwesomeIcon icon={faTrashCan} size="2x" color="white" />
              </button>
            </div>
          ))}
        </div>
      </div>
    </div>
  );
};

export default TodoItem;

  1. 두 개의 state를 만든다.
    isEditClicked는 수정 버튼을 클릭했을 때와 클릭하지 않았을 때의 상태를 의미한다.
    수정 버튼을 클릭하면 해당 투두를 div에서 input으로 바꾸고/수정 버튼과 삭제 버튼을 없애고/수정확인&수정취소 버튼을 보여줘야 하기 때문에 사용했다.
    updatedText는 수정된 투두이다. 수정된 내용을 상위에 있는 컴포넌트 Todo.js에 올려주어 화면에 표시될 투두리스트들을 담은 displayInputs를 변경하기 위해 사용했다.

    const [isEditClicked, setIsEditClicked] = useState(false);
    const [updatedText, setUpdatedText] = useState('');
  2. 수정 버튼을 클릭하면 openEdit이라는 함수가 실행된다.
    이 함수는 isEditClickedtrue로 변경한다.

    const openEdit = () => {
      setIsEditClicked(true);
    }
  3. isEditClickedtrue가 되면 아래의 요소가 렌더링되는데, 평소에는 div에 투두 문자열을 표시하고 수정 버튼이 클릭되면 div가 문자열을 입력할 수 있는 input으로 바뀌어 수정할 수 있다. valueonChange 속성을 통해 입력한 문자열이 input창에 반영된다.

    {isEditClicked
      ? (
      <div className={isChecked? `${styles.text} ${styles.checked}` : `${styles.text}`}>
        <input
          value={updatedText}
          onChange={(e) => setUpdatedText(e.target.value)}
          />
      </div>
    ) : (
      <div className={isChecked? `${styles.text} ${styles.checked}` : `${styles.text}`}>
        {props.todo.text}
      </div>)
    }
  4. 수정 후 수정확인 버튼을 누르면 submitEditedContent 함수가 실행되고, 수정취소 버튼을 누르면 cancelEdit 함수가 실행된다.

    <div>
      <button className={styles.submitIcon} onClick={submitEditedContent}>
        <FontAwesomeIcon icon={faCheck} size="2x" color="white" />
      </button>
      <button className={styles.cancelIcon} onClick={cancelEdit}>
        <FontAwesomeIcon icon={faXmark} size="2x" color="white" />
      </button>
    </div>
  • cancelEdit 함수는 수정취소를 눌렀을 때 실행되는 함수이다. 따라서 투두를 수정했더라도 마지막엔 수정취소를 했으므로 updatedText를 빈 문자열로 초기화해준다. 그리고 isEditClickedfalse로 설정해준다.

    const cancelEdit = () => {
      setUpdatedText('');
      setIsEditClicked(false);
    }
  • submitEditedContent 함수는 수정 확인 버튼을 눌렀을 때 실행되는 함수이다.
    - 빈 문자열을 입력하고 확인을 누르면 반영되지 않게끔 return했다.
    - 이때 입력창을 닫아주기 위해 isEditClickedfalse로 설정했다.
    - 상위에서 맨 처음에 추가했을 때 input으로 입력하여 추가한 투두들을 displayInputs라는 state에 모두 저장해서 map으로 하나씩 꺼내었었다. displayInputs에 수정한 텍스트를 반영하기 위해 이 state가 선언된 todo.js라는 상위 컴포넌트로 데이터들을 올렸다(수정된 텍스트와 그 텍스트가 있는 투두의 id)
    - 마지막으로 입력창을 닫아주기 위해 마지막에 isEditClickedfalse로 설정했다.

    const submitEditedContent = () => {
      if (updatedText === '') {
        setIsEditClicked(false);
        return;
      }
      props.submitEditedContent(updatedText, props.todo.id);
      setUpdatedText('');
      setIsEditClicked(false);
    }
  1. 실행될 submitEditedContent 함수에 가보자. 모든 투두가 저장된 displayInputs를 map으로 펼친 후 거기서 수정될 투두를 찾아 기존의 text를 수정한 텍스트인 updatedText로 바꿨다.

    const [displayInputs, setDisplayInputs] = useState([]);
    
    const submitEditedContent = (updatedText, id) => {
      setDisplayInputs(displayInputs.map(todo => {
        if (todo.id === id) {
          return {
            ...todo,
            text: updatedText
          };
        }
        return todo;
      }))
    }


✨ 결과

수정 기능을 완성했다~!!



🔮 개선하고 싶은 부분

1. 수정버튼을 누르면 바로 수정할 수 있도록 focus 주기

수정 버튼을 누르면 div에서 input으로 바뀐 투두에 바로 입력할 수 있도록 포커스를 두게 하려고 했는데, 잘 되지 않았다.

useRef를 써서 해보려고 했으나, 이렇게 수정 버튼을 눌러서 openEdit 함수를 실행하고 editedText 부분에 focus를 둬!라고 해도

const editedText = useRef(null);

const openEdit = () => {
  setIsEditClicked(true);
  editedText.current.focus();
}

조건부렌더링이라서ref={editedText}가 있는 input이 렌더링되기도 전에 editedText.current.focus()가 실행되어 계속 undefined가 나오는 것 같다. (맞는지 확실히 모르겠지만) 그래서 해결하면 따로 포스팅을 하려고 한다!

{isEditClicked
  ? (
  <div className={isChecked? `${styles.text} ${styles.checked}` : `${styles.text}`}>
    <input
      value={updatedText}
      onChange={(e) => setUpdatedText(e.target.value)}
      ref={editedText}
      />
  </div>
) : (
  <div className={isChecked? `${styles.text} ${styles.checked}` : `${styles.text}`}>
    {props.todo.text}
  </div>)
}

2. input 경고 표시 개선하기

이건 수정 기능과 관련된 건 아니지만, 개선하고 싶은 부분이 생겨 기록하려고 한다.

기존에 input에 아무것도 입력하지 않고 입력 버튼을 누르면 input이 전체적으로 빨간 색으로 변하면서 경고를 하는 기능을 만든 적이 있다.

이 기능은 현재 어떤 문제가 있는데, 다른 곳을 클릭했다가 다시 input 창을 클릭했을 때 경고가 사라지지 않는다.

빈 문자열을 입력하고 입력 버튼을 눌렀을 때만 경고를 표시하고, 다른 곳을 클릭하면 input창이 원래대로 돌아갔으면 한다. 하지만 어떻게 해야할지 모르겠어서 일단 기록만 남겨두려고 한다.



🐹 회고

작년에 투두리스트를 만들어본 적 있는데(첫 번째 투두리스트), 수정 기능을 만들다가 포기했었어서 😂 이번에 성공한 것이 더욱 기뻤던 것 같다.

이 투두리스트는 딱 2주만 만들고 배포를 하려고 한다. 같은 실력으로 만들기만 계속 하는 것보다 기간을 잡고 만들고, 강의를 들으면서 공부를 하고 그 후에 더 아는게 많아진 머리로 뭘 또 만들고... 를 반복하는 것이 좋을 것 같아서이다.

그래서 만족하지 못하더라도 모레인 22.2.24에 배포를 하려고 한다! (이후에는 다시 강의를 들으면서 지식을 쌓고, 더 넓어진 시야로 나중에 리팩토링을 하러 올 것이다😊)

그 전까지 추가하고 싶은 기능을 열심히 만들어보자!!

profile
🐹강화하고 싶은 기억을 기록하고 공유하자🐹

2개의 댓글

comment-user-thumbnail
2023년 2월 23일

오~~! 햄쥑쥑님의 강화 투두리스트 잘 보고 가요 ' - ' 설명이 넘 보기좋네요
더 진화하길 기대할게요

1개의 답글