티스토리 뷰

*피드백은 언제나 환영합니다🙌

 


저번 글에서는 할 일 목록 추가에 대해 다뤘다.

오늘은 할 일 목록을 삭제, 완료 체크, 수정하는 과정까지 다뤄보겠다.


1. 할 일 목록 삭제


먼저 Today.js에서 onRemove 함수를 생성한다.
.filter를 이용해 각 아이템의 고유 id를 받아와서 해당 아이디를 가진 아이템만 제외하고 새로운 배열을 만들었다.

const onRemove = id => e => {
    setTodos(todos.filter(todo => todo.id !== id));
  };
filter() 메서드는 해석 그대로 걸러주는 역할을 하는 함수다. 주로 특정 조건을 만족하는 새로운 배열이 필요할 때 사용한다.



생성한 onRemove 함수를 TodoList 컴포넌트에 전달하고

<TodoList todos={todos} onRemove={onRemove} />



TodoList 컴포넌트에서 onRemove 함수를 받아와서 TodoListItem 컴포넌트에 전달했다.

function TodoList({todos, onRemove}) {
  return(
    <div>
      {
        todos.map(todo => (
            <TodoListItem 
              {...todo} 
              key={todo.id} 
              todos={todos} 
              onRemove={onRemove} 
            />         
          )
        )
      }
    </div>
  )
}


결론적으로 onRemove를 Today.js → TodoList .js → TodoListItem.js 순으로 전달했는데 만약 전달해야 하는 컴포넌트가 더 있다면..? 100개라면....??

다행히 Context API로 전역 값을 관리 할 수 있다고 한다.
앞으로 전달해야 할 props들이 몇 개 더 있는데 벌써 번거롭다 😫
일단 지금 하고 있는 작업은 끝내고 Context API에 대해 따로 공부해야겠다.


다음으로 TodoListItem 컴포넌트에서 휴지통 모양에 onClick 이벤트를 생성하고 onRemove() 함수를 할당했다.

<div className='removeboxDiv'>
    <FaRegTrashAlt className='removebox' onClick={onRemove(id)}/>
</div>


이렇게 하면 휴지통 모양을 클릭시 할 일 목록을 삭제할 수 있게 된다.


삭제 기능

 

 

 

 

2. 할 일 목록 완료 체크


삭제 기능 다음으로 완료 체크 기능을 다뤄보겠다.

먼저 각 아이템 왼쪽에 있는 네모 박스를 클릭하면 체크표시가 되고 한 번 더 클릭하면 체크가 해제되도록 구현하기위해 onToggle 함수를 만들었다.

const onToggle = id => e => {
    setTodos(
      todos.map(todo =>
        todo.id === id ? {...todo, checked: !todo.checked} : todo,
      ),
    );
  };

onRemove 함수와 동일하게 아이템의 id를 받아와서 해당하는 아이템의 속성값을 반대로 변경했다. (true면 false로, false면 true로)


이후 onToggle 함수를 TodoList 컴포넌트에 전달 한 뒤

<TodoList todos={todos} onRemove={onRemove} onToggle={onToggle} />



TodoList 컴포넌트에서 onToggle 함수를 받아 TodoListItem 컴포넌트로 전달했다.

function TodoList({todos, onRemove, onToggle}) {
  return(
    <div>
      {
        todos.map(todo => (
            <TodoListItem 
              {...todo} 
              key={todo.id} 
              todos={todos} 
              onRemove={onRemove} 
              onToggle={onToggle} 
            />         
          )
        )
      }
    </div>
  )
}



마지막으로 TodoListItem 컴포넌트에서 div에 onClick 속성을 만들어 onToggle() 함수를 할당 해 주었다.

<div onClick={onToggle(id)} className='checkbox'>  
  { 
  checked ? // checked: true
  <MdOutlineCheckBox className='checkbox'/> : 
  <MdOutlineCheckBoxOutlineBlank className='checkbox'/> 
  }
</div>

 

완료 체크 기능

 

 

 

 

 

3. 할 일 수정 기능


원래 수정할 수 있는 버튼을 따로 만드려고 했었는데 더블클릭 시 수정 가능한 기능도 괜찮을 것 같아 계획을 바꿨다.

내가 원하는 수정 기능은 아래와 같다.
1. 완료 체크를 안 한 상태에서만 수정이 가능(= 완료 체크를 한 것은 불가능)
2. 텍스트 더블클릭 시 수정 가능
3. 엔터클릭시 제출

먼저 완료 체크가 안 된 상태에서만 수정이 가능하게 하기 위해 삼항연산자 사용했다. 완료 체크가 되었을 때는 삼항연산자를 이용하여 텍스트 중간에 줄이 그어지도록 했다.

삼항연산자를 썼는데 if문 보다 깔끔하고 편해서 자주 쓸 거 같다.

{
    !checked ? ( // 체크 안 된 상태
      
      // 수정 모드
      
      ) : <p 
            className={checked ? 'line' : 'noLine'}
          >
            {textValue}
          </p> // 체크 된 상태  
}



다음으로 수정모드인지 확인하는 edited를 넣었다. 초기값은 false로 지정했다.
또 텍스트를 더블클릭시 edited 값을 true로 바꾸기 위해 onClickEditButton를 추가했다.

수정모드인 상태에서는 <input />으로 바뀐다. 그래야 값을 입력할 수 있기 때문이다.
수정모드가 아닌 상태에서는 <p> 로 바뀐다. 이 상태에서 더블클릭하면 edited 값이 true로 바뀌기 때문에 다시 <input /> 상태로 돌아갈 수 있다.
 const [edited, setEdited] = useState(false); // 수정 모드인지 확인하기 위한 값
    .
    .
    .
  
  const onClickEditButton = () => {
    // 더블클릭시 edited 값을 true로 바꿈
    setEdited(true);
  };


{
    !checked ? ( 
    // 체크 안 된 상태
      edited ? (  
      // 수정 모드
        <input 
        type="text" 
        /> 
        ) : ( 
        // 수정 모드 X
          <p 
            onDoubleClick={onClickEditBtn} 
            className={checked ? 'line' : 'noLine'}
          >
              {textValue}
          </p>
      )
      ) :
      // 체크 된 상태
      <p 
            className={checked ? 'line' : 'noLine'}
          >
            {textValue}
          </p>  
}



여러 조건들을 설정했으니 이제 텍스트를 더블클릭해서 수정한 뒤 엔터를 누르면 내용이 변하도록 하는 과정을 다뤄보겠다.


먼저 새로운 아이템 내용을 담을 state를 만들었다.

let [newText, setnewText] = useState(todos.textValue);



그리고 input에 onchange 속성을 줘서 현재 입력한 값을 가져오도록 했다.

  function onChangeEditInput(e) {
    setnewText(e.target.value);
  }



엔터키를 누르면 내용이 제출되게 하기 위해 input에 onKeyUp 속성을 주었다.
그리고 &&연산자를 사용하여 엔터를 눌렀을 때 텍스트 길이가 0보다 크면 onDoubleClick()을 실행하도록 했다.

 function onKeyUp (e) {
    e.preventDefault();
    if(e.key === 'Enter' && e.target.value.length > 0) {
        onDoubleClick()
    } 
  }



onDoubleClick()에서는 새로운 아이템 내용을 담을 배열을 만들기 위해 nextTodoList를 만들었다. map을 사용해서 기존의 todoList는 그대로 가져오고, 해당 아이템과 일치하는 id값의 텍스트만 변경하도록 했다. 이후 setTodoList에 수정 된 할 일 목록이 담긴 새로운 리스트를 넣고 수정모드를 다시 읽기 모드로 변경했다.

  function onDoubleClick() {
    const nextTodoList = todos.map(todo => ({ 
        ...todo,
        textValue: todo.id === id ? newText : todo.textValue // 새로운 아이템 내용을 넣어줌
    }));
    setTodos(nextTodoList); // 새로운 리스트를 넣어줌
    setedited(false); // 수정모드를 다시 읽기모드로 변경
  }



그리고 수정 모드일때 input에 포커싱이 되게하기 위해 useRef를 사용했다.
useRef는 특정 DOM을 선택해야 할 때 사용하는데 Javascript로 치면 getElementById나 querySeletor 같은 함수다. 벨로퍼트님이 잘 설명해주신 글이 있어 공유해본다.

https://react.vlpt.us/basic/10-useRef.html

useRef는 Ref안에 있는 값을 아무리 변경해도 컴포컨트가 재렌더링 되지 않아 유용하다고 한다.
대표적으로 input 요소를 클릭하지 않고 포커스를 주고자 할 때 사용된다.


먼저 Ref 객체를 만들어 준다.

const editInputRef = useRef();


그리고 선택하고 싶은 DOM에 속성으로 ref 값을 설정해준다.

<input 
    type="text" 
    value={newText}
    className='editedInput'
    ref={editInputRef} // ref로 DOM에 접근
    onChange={onChangeEditInput}
    onKeyUp={onKeyUp}
/>



나는 수정 모드일 때 포커싱을 하고 싶어서 useEffect를 이용해 2번째 인자값에 edited을 넣었다.
editInputRef.current.focus(); 에서 current는 우리가 선택하고자 하는 DOM을 가리킨다. 그리고 input에 포커스를 하는 focus() DOM API 를 호출했다.

const editInputRef = useRef();

useEffect(()=> {
    //edit 모드일 때 포커싱 한다
    if(edited) {
      editInputRef.current.focus();
    }
  }, [edited]);
  
  {
            !checked ? ( // 체크 안 된 상태
              edited ? (  // 수정 모드
                <input 
                type="text" 
                value={newText || '' || undefined}
                className='editedInput'
                ref={editInputRef} // ref로 DOM에 접근
                onChange={onChangeEditInput}
                onKeyUp={onKeyUp}
                /> 
                ) : ( // 수정 모드 X
                  <p 
                    onDoubleClick={onClickEditBtn} 
                    className={checked ? 'line' : 'noLine'}
                  >
                      {textValue}
                  </p>
              )
              ) : <p // 체크 된 상태
                    className={checked ? 'line' : 'noLine'}
                  >
                    {textValue}
                  </p>  
          }


이렇게 할 일 목록 삭제, 완료 체크, 수정 기능도 끝났다!
는 에러떴다..


텍스트를 더블클릭하고 input에 새로운 텍스트를 작성하니 아래와 같은 에러가 떴다. 

 

음.. 뭔소리야

검색 끝에 리액트 공식문서에서 관련 답을 찾았다.

 

제어 컴포넌트 vs. 비제어 컴포넌트

React는 두 가지 방식으로 form 입력을 처리합니다.
React에 의해 입력값이 제어되는 엘리먼트를 제어 컴포넌트(controlled component) 라고 합니다. 사용자가 제어 컴포넌트에 데이터를 입력하면 변경 이벤트 핸들러가 호출되고 코드가 (업데이트된 값으로 다시 렌더링에 의해) 입력의 유효 여부를 결정합니다. 다시 렌더링하지 않으면 form 엘리먼트는 변경되지 않은 상태로 유지됩니다.

비제어 컴포넌트(uncontrolled component)는 form 엘리먼트가 React 외부에서 작동하는 것처럼 작동합니다. 사용자가 form 필드(input box, dropdown 등)에 데이터를 입력하면 업데이트된 정보가 React에서 별도 처리할 필요 없이 엘리먼트에 반영됩니다. 그러나, 이는 특정 필드가 특정 값을 갖도록 강제할 수 없다는 의미이기도 합니다.
대부분은 controlled component를 사용해야 합니다.


간단하게 정리하자면 input 태그에 uncontrolled 값이 controlled로 바뀌었을 때 뜨는 에러라고 한다. uncontrolled 는 undefined 값 input이고 controlled는 값이 유효한 input을 말한다.

 

수정 모드일 때 value 필드가 빈 객체여서 초기값이 없는 uncontrolled 상태이다. (value 초기값을 설정하지 않으면 uncontrolled로 처리됨) 근데 텍스트를 작성하고 엔터를 누르면 value 필드에 유효한 값이 채워져 controlled 상태로 변한다. 따라서 input 엘리먼트가 uncontrolled 에서 controlled 로 변해 에러가 뜨는 것 같다.

해결방법은 input의 value 값이 없어도(undefined 이어도) undefined 값이 뜨지 않도록 디폴트 값을 넣어주면 된다.

undefined || '' 는 ''을 반환한다.
function onDoubleClick() {
const nextTodoList = todos.map(todo => ({ 
    ...todo,
    textValue: todo.id === id ? newText : todo.textValue
}));
setTodos(nextTodoList);
setedited(false);
}

<input 
    type="text" 
    value={newText || ''}
    className='editedInput'
    ref={editInputRef} 
    onChange={onChangeEditInput}
    onKeyUp={onKeyUp}
    onBlur={handleInputBlur}
/>

 

수정 기능

 

 


이제 진짜 할 일 목록 삭제, 완료 체크, 수정 기능이 끝났다! 최최최종.jpg
처음에는 막막했는데 해냈다!

 

추가, 완료, 수정, 삭제 기능

다음 글에서는 localStorage를 사용해 데이터를 저장하는 것을 다뤄 볼 예정이다