[리액트] 투두리스트 만들기(2) - 추가 편

비얌·2022년 9월 28일
13
post-thumbnail

개요

저번 포스팅 리액트로 ToDo앱 만들기(1) - UI편에서는 강의 따라 하며 배우는 리액트 A-Z에서 '섹션 2. 간단한 To-Do 앱 만들며 리액트 익히기'를 학습한 후 강의에서 사용한 코드를 최대한 보지 않고 ToDo 앱의 UI를 만들어 보았었다.

오늘의 목표도 역시 강의의 코드를 보지 않고 검색과 혼자의 힘으로 (삽질하며) 투두앱을 만드는 것이다!

오늘은 저번 UI 편에 이어 할 일 목록 '추가' 기능을 구현해 볼 것이다.


할 일 목록 추가하기

저번 포스팅에서는 UI까지 만들어보았는데, UI 완성본은 아래와 같다.

여기서 청소하기 목록은 밑에 위치한 입력창에서 입력받은 것이 아닌 하드코딩한 결과이다.

할 일 목록을 추가한다는 것은 text input 창에 할 일 목록을 입력하고 입력 버튼을 누르면 입력한 내용이 ToDo 앱에 추가되는 것이다.

차근차근 기능을 구현해 보자!

1. 입력 창에 이벤트 만들기

첫 번째로 입력 창에 입력을 하면 입력한 대로 입력 창 안의 내용이 바뀌게 해보려고 한다.

하지만 아래의 코드처럼 input 창을 만들면 입력 창에 아무리 값을 입력해도 아무것도 입력되지 않는다.

<input 
  className='inputText' 
  type='text' 
  placeholder='해야 할 일을 입력하세요.'
  value={text}
/>

(가만히 있는 거 아니고 계속 입력하고 있음)

✨ 그 이유는 바로 value={text} 때문인데, input 엘리먼트에 value 속성을 지정해 두면 이 엘리먼트는 value 값에 의해서만 변경되기 때문에 사용자가 값을 입력해도 input은 변경되지 않는다.


이를 해결하려면 onChange를 써서 input 값이 변경 가능하도록 만들어야 한다고 한다.

input 창이 변화하면(onChange) onChangeInput라는 함수(이벤트)가 실행되게 만들어보자.

1) onChangeInput 이벤트 만들기

<input 
  // input 안의 내용이 변경되면 onChangeInput 이벤트 실행
  onChange={onChangeInput} 
  className='inputText' 
  // input 창의 type은 한 줄을 입력받는 기능인 'text'
  type='text' 
  // 창에 표시될 도움말
  placeholder='해야 할 일을 입력하세요.'
  // value를 사용하는 이유를 정확히 모르겠지만 value는 input 창에 입력되는 내용을 가리킨다. 
  // text는 아래 2)에서 state로 선언해 줄 것이다
  value={text}
/>
  • input 안의 내용이 바뀌면 onChangeInput이라는 이벤트가 실행되도록 onChange={onChangeInput}라는 코드를 추가했다.

  • value={text}라는 부분을 통해 input 안에 입력되는 내용을 text라고 정함


2) useState로 text state 관리하기

input에 입력되는 값인 text는 입력값이 바뀔 때마다 화면에 반영되어야 한다.

따라서 useState를 이용하여 state인 text를 선언했다.

const [text, setText] = useState('');

const onChangeInput = (e) => {
  setText(e.target.value);
};
  • const [text, setText] = useState('');로 state인 변수 text를 선언했다.

  • onChangeInput 이벤트가 실행되면 textsetText(e.target.value)를 통해 새롭게 입력한 값으로 변경된다.

  • onChangeInput 이벤트가 발생하면 이벤트 객체가 자동으로 만들어지는데, 이것을 e라고 하고 입력한 내용을 의미하는 e.target.value을 text를 업데이트하는 함수 setText 안에 넣어줌으로써 text 값을 현재 입력하고 있는 값인 e.target.value로 변하게 하는 원리이다. (console.log(e.target.value)를 직접 찍어보면 입력하는대로 입력값이 출력되는데, 이를 통해 e.target.value가 input 창 안에 입력되는 값임을 알 수 있다.)


이제 input 창에 값을 입력하면 입력하는 대로 값이 정상적으로 변화하는 것을 확인할 수 있다.


2. 목록이 추가됨에 따라 투두리스트 갱신하기

위에서는 입력 창에 글자를 입력하면 입력하는 대로 입력 창 안의 글자가 바뀌게 해보았다.

이제 전송(입력) 버튼을 누르면 새롭게 입력된 투두리스트와 이전에 만들어진 투두리스트를 합쳐서 투두리스트를 갱신하는 작업을 할 것이다. (최종적으로 이걸 투두앱에 추가해서 화면에 보여주면 됨!)

1) onSubmit 이벤트 만들기

'submit' 타입의 input 버튼이 눌리면 실행되는 onSubmit 이벤트를 만들었다.

<form onSubmit={onSubmit}>
  
  <input 
      className='inputSubmit' 
      type='submit' 
      value='입력'
  />
</form>
  • 입력받는 부분과 입력 버튼을 form 태그로 감싸 form 태그 안에서 onSubmit되면(submit 타입의 버튼이 눌리면) onSubmit 이벤트가 발생되도록 했다.

2) 입력받은 값이 담길 todoList state 생성하기

const [todoList, setTodoList] = useState([]);
  • useState로 todoList라는 state를 생성하고 빈 배열로 초기화했다.

  • useState로 초기화할 때 빈 문자열인 ''가 아니라 빈 배열[]로 초기화한 것은 todoList가 객체 배열이기 때문이다. 뒤에서 설명하겠지만 todoList는 id, text, checked가 담긴 객체'들'을 담는 배열이기 때문에 초기값도 같은 타입인 배열로 선언해 준다.


3) todoList 갱신하기

const onSubmit = (e) => {
  const nextTodoList = todoList.concat({
    id: todoList.length,
    text,
    checked: false,
  });

  setTodoList(nextTodoList);
};
  • onSubmit 이벤트가 실행될 때 새로 입력받은 값을 반영하여 todoList가 갱신되게 할 것이다.

  • 변수 nextTodoList를 만들고 새로운 값이 onSubmit될 때마다 기존의 todoList와 새로운 값을 concat으로 합쳐준다. 이때 각 원소는 id와 text, checked가 포함된 객체로 받아준다.

  • id는 todoList의 길이(리스트의 개수)로 정했다.

  • text는 위에서 useState를 통해 만든 state이며, input 창에 입력되는 문자열을 의미한다. (<input value={text}/>)


4) 입력창 비우기, 페이지 새로고침 방지하기

  setTodoList(nextTodoList);
  setText('');
  e.preventDefault();
  • preventDefault()는 현재 이벤트의 default 동작을 중단한다는 뜻이다.

  • preventDefault()는 submit의 default 동작인 페이지 새로고침을 막아줄 수 있는데, submit은 작동하되 새로운 페이지로 이동하고 싶지 않을 때 사용된다.

  • 만약에 e.preventDefault();가 없다면 아래와 같이 페이지가 새로고침되어 기존 정보가 유지되지 않을 것이다.


3. 새로운 투두리스트 화면에 추가하기

마지막으로, 투두리스트 값들을 담은 todoList를 투두앱 화면에 추가해 보자.

<div className='lists'>
  {todoList.map((todoItem) =>(
  	<div>
      <input className='checkbox' type='checkbox'/>
      <span className='listContent'>{todoItem.text}</span>
      <button type='button' className='deleteBtn'>x</button>
  	</div>
  ))}
</div>
  • map()함수를 이용해서 todoList에 있는 값 모두를 꺼내 =>() 안에 있는 형식으로 뿌려준다. 이 과정을 거치면 체크박스-입력한문자열-x버튼 형식의 리스트가 todoList의 길이만큼(todoList의 개수만큼) 화면에 그려지게 된다.

이 부분에서 삽질한 두 가지 경우를 소개해 보려고 한다.

(1) 중괄호 {}와 JSX 표현식에서의 루트 컴포넌트를 안 써서 생긴 문제

아래의 코드는 초기에 작성한 코드인데, 잘못된 부분이 두 가지 있었다.

<form onSubmit={onSubmit}>
  <div className='lists'>
    todoList.map((todoItem) =>(
        <input className='checkbox' type='checkbox'/>
        <span className='listContent'>{todoItem.text}</span>
        <button type='button' className='deleteBtn'>x</button>
    ))
  </div>
  1. todoList.map 부분을 중괄호 {}로 묶어줬어야 했다. 오류가 발생해서 찾아보니, 리액트에서 js 코드를 쓰려면 중괄호로 묶어서 사용해야 한다고 한다. 따라서 {todoList.map(()=>)}와 같이 중괄호로 묶어줘야 한다.

  2. todoList.map의 리턴하는 부분에서 수정이 필요한데, 세 개의 태그(input, span, button)를 감싸는 태그를 추가해야 한다. JSX 표현식에서는 하나의 루트 컴포넌트가 필요하기 때문이다.


(2) button에 type를 안 줘서 생긴 문제

buttontype에는 3가지 값 submit, reset, button을 지정해 줄 수 있다고 한다. 하지만 이때 아무런 값도 지정하지 않는다면 기본값은 submit이 된다. 이는 일반적인 버튼을 만들었다고 해도 type='button'을 써주지 않는다면 <button type='submit'>x</button>과 같은 동작을 할 것이라는 뜻이다.

<form onSubmit={onSubmit}>
  <div className='lists'>
    todoList.map((todoItem) =>(
        <input className='checkbox' type='checkbox'/>
        <span className='listContent'>{todoItem.text}</span>
        <button className='deleteBtn'>x</button>
    ))
  </div>

따라서 위와 같이 type='button'을 명시해 주지 않는다면 이 버튼을 누를 때 form 태그 안에서 onSubmit이 실행되고, onSubmit 이벤트를 호출하게 된다.

👉 위에서 onSubmit는 새로 입력한 값을 투두앱에 추가해 주는 기능이었기 때문에, 이 버튼을 누르면 값이 없는 투두리스트가 의도치 않게 투두앱에 추가되게 된다. button에는 타입을 꼭 써주자!



완성!!

드디어 ... 완성했다 투두리스트 추가기능!!!

비록 검색할 수 있었기에 완성할 수 있었지만, 그래도 처음으로 리액트로 기능을 구현해 봤다!! 🥳🥳🥳🥳



이제는 찾아볼 수 있다, 예전에 강의 들으면서 공부한 코드!

포스팅 목적이 검색과 나의 지식만으로 삽질하면서 만들어보자는 거였기 때문에, 포스팅하는 동안에는 이전에 배운 코드를 보지 못했었다.

하지만 이제는 성공했으니 이전에 배운 내용을 다시 보고 비교해 보기로 했다.

1. 기존 입력값과 새로운 입력값을 합치는 방식이 다르다

// 강의에서 쓴 코드

const [todoData, setTodoData] = useState([]);
const [value, setValue] = useState("");

  const handleSubmit = (e) => {
    let newTodo = {
      id: Date.now(),
      title: value,
      completed: false,
    };
    
    // 원래 있던 일에 새로운 할 일 더해주기
    // 입력란에 있던 글씨 지워주기
    setTodoData(prev => [...prev, newTodo]);
  }
// 내가 작성한 코드

const [todoList, setTodoList] = useState([]);
const [text, setText] = useState('');

const onSubmit = (e) => {
    const nextTodoList = todoList.concat({
      id: todoList.length,
      text,
      checked: false,
    });
  • 투두리스트를 갱신하기 위해 setTodoData(prev => [...prev, newTodo]);라고 작성했는데, todoData를 업데이트하는 함수인 setTodoData 안에 prev => [...prev, newTodo]라고 작성함으로써 기존 값에 newTodo 객체를 더한 배열으로 todoData를 업데이트했다.

  • id에 값을 부여하는 과정에서 강의는 Date.now(), 나는 todoList.length를 썼다. 둘 다 고유한 값을 부여하니까 된 거 아닐까? id를 만들 때 ref라는 것을 사용하는 것 같은데 아직 안 배웠으므로 자세히 보지는 않았다.


궁금증..

그런데 여기서 궁금한 점이 생겼다.

  1. prev를 사전에 따로 선언하지 않았는데도 어떻게 과거의 값이라는 것을 알까? (바보같은 질문 같은데 이해가 안 간다)
    👉🏻 커뮤니티에 질문했고 많은 답변을 받았는데도.. 이해가 잘 안 간다.. 이후에 이해하게 되면 정리해서 이곳에 추가해야겠다.

  2. setTodoData(prev => [...prev, newTodo])setTodoData([...todoData, newTodo])와 같은 의미일까?? 이렇게 바꿔도 동작할까?
    👉 바꿔서 실행시켜봤는데 정상적으로 동작한다!! 같은 의미인 것 같다.



🐹 회고

드디어 ToDo앱 추가 기능 만들기가 끝났다! 모르는 게 너무 많아서 막힐 때마다 공부해가면서 하느라 시간이 정말 많이 걸린 것 같다..

그래도 혼자 만들어보는 과정은 꼭 필요한 것 같다. 다음 포스팅에서는 완료 표시하기, 항목 삭제 기능을 구현해 볼 것이다!

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

2개의 댓글

comment-user-thumbnail
2022년 9월 28일

모르시는분이지만 잘보고 가요!!
(이꽉물 모름)

1개의 답글