저번 포스팅 리액트로 ToDo앱 만들기(1) - UI편에서는 강의 따라 하며 배우는 리액트 A-Z에서 '섹션 2. 간단한 To-Do 앱 만들며 리액트 익히기'를 학습한 후 강의에서 사용한 코드를 최대한 보지 않고 ToDo 앱의 UI를 만들어 보았었다.
오늘의 목표도 역시 강의의 코드를 보지 않고 검색과 혼자의 힘으로 (삽질하며) 투두앱을 만드는 것이다!
오늘은 저번 UI 편에 이어 할 일 목록 '추가' 기능을 구현해 볼 것이다.
저번 포스팅에서는 UI까지 만들어보았는데, UI 완성본은 아래와 같다.
여기서 청소하기 목록은 밑에 위치한 입력창에서 입력받은 것이 아닌 하드코딩한 결과이다.
할 일 목록을 추가한다는 것은 text input 창에 할 일 목록을 입력하고 입력
버튼을 누르면 입력한 내용이 ToDo 앱에 추가되는 것이다.
차근차근 기능을 구현해 보자!
첫 번째로 입력 창에 입력을 하면 입력한 대로 입력 창 안의 내용이 바뀌게 해보려고 한다.
하지만 아래의 코드처럼 input 창을 만들면 입력 창에 아무리 값을 입력해도 아무것도 입력되지 않는다.
<input
className='inputText'
type='text'
placeholder='해야 할 일을 입력하세요.'
value={text}
/>
(가만히 있는 거 아니고 계속 입력하고 있음)
✨ 그 이유는 바로 value={text}
때문인데, input 엘리먼트에 value 속성을 지정해 두면 이 엘리먼트는 value 값에 의해서만 변경되기 때문에 사용자가 값을 입력해도 input은 변경되지 않는다.
이를 해결하려면 onChange
를 써서 input 값이 변경 가능하도록 만들어야 한다고 한다.
input 창이 변화하면(onChange
) 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
라고 정함
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
이벤트가 실행되면 text
는 setText(e.target.value)
를 통해 새롭게 입력한 값으로 변경된다.
onChangeInput
이벤트가 발생하면 이벤트 객체가 자동으로 만들어지는데, 이것을 e
라고 하고 입력한 내용을 의미하는 e.target.value
을 text를 업데이트하는 함수 setText
안에 넣어줌으로써 text
값을 현재 입력하고 있는 값인 e.target.value
로 변하게 하는 원리이다. (console.log(e.target.value)
를 직접 찍어보면 입력하는대로 입력값이 출력되는데, 이를 통해 e.target.value
가 input 창 안에 입력되는 값임을 알 수 있다.)
이제 input 창에 값을 입력하면 입력하는 대로 값이 정상적으로 변화하는 것을 확인할 수 있다.
위에서는 입력 창에 글자를 입력하면 입력하는 대로 입력 창 안의 글자가 바뀌게 해보았다.
이제 전송(입력) 버튼을 누르면 새롭게 입력된 투두리스트와 이전에 만들어진 투두리스트를 합쳐서 투두리스트를 갱신하는 작업을 할 것이다. (최종적으로 이걸 투두앱에 추가해서 화면에 보여주면 됨!)
'submit' 타입의 input 버튼이 눌리면 실행되는 onSubmit
이벤트를 만들었다.
<form onSubmit={onSubmit}>
<input
className='inputSubmit'
type='submit'
value='입력'
/>
</form>
onSubmit
되면(submit 타입의 버튼이 눌리면) onSubmit
이벤트가 발생되도록 했다. const [todoList, setTodoList] = useState([]);
useState로 todoList
라는 state를 생성하고 빈 배열로 초기화했다.
useState로 초기화할 때 빈 문자열인 ''가 아니라 빈 배열[]로 초기화한 것은 todoList
가 객체 배열이기 때문이다. 뒤에서 설명하겠지만 todoList
는 id, text, checked가 담긴 객체'들'을 담는 배열이기 때문에 초기값도 같은 타입인 배열로 선언해 준다.
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}/>
)
setTodoList(nextTodoList);
setText('');
e.preventDefault();
preventDefault()
는 현재 이벤트의 default 동작을 중단한다는 뜻이다.
preventDefault()
는 submit의 default 동작인 페이지 새로고침을 막아줄 수 있는데, submit은 작동하되 새로운 페이지로 이동하고 싶지 않을 때 사용된다.
만약에 e.preventDefault();
가 없다면 아래와 같이 페이지가 새로고침되어 기존 정보가 유지되지 않을 것이다.
마지막으로, 투두리스트 값들을 담은 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>
체크박스-입력한문자열-x버튼
형식의 리스트가 todoList의 길이만큼(todoList의 개수만큼) 화면에 그려지게 된다.이 부분에서 삽질한 두 가지 경우를 소개해 보려고 한다.
아래의 코드는 초기에 작성한 코드인데, 잘못된 부분이 두 가지 있었다.
<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>
todoList.map
부분을 중괄호 {}
로 묶어줬어야 했다. 오류가 발생해서 찾아보니, 리액트에서 js 코드를 쓰려면 중괄호로 묶어서 사용해야 한다고 한다. 따라서 {todoList.map(()=>)}
와 같이 중괄호로 묶어줘야 한다.
todoList.map
의 리턴하는 부분에서 수정이 필요한데, 세 개의 태그(input, span, button)를 감싸는 태그를 추가해야 한다. JSX 표현식에서는 하나의 루트 컴포넌트가 필요하기 때문이다.
button
의 type
에는 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
에는 타입을 꼭 써주자!
드디어 ... 완성했다 투두리스트 추가기능!!!
비록 검색할 수 있었기에 완성할 수 있었지만, 그래도 처음으로 리액트로 기능을 구현해 봤다!! 🥳🥳🥳🥳
포스팅 목적이 검색과 나의 지식만으로 삽질하면서 만들어보자는 거였기 때문에, 포스팅하는 동안에는 이전에 배운 코드를 보지 못했었다.
하지만 이제는 성공했으니 이전에 배운 내용을 다시 보고 비교해 보기로 했다.
// 강의에서 쓴 코드
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
라는 것을 사용하는 것 같은데 아직 안 배웠으므로 자세히 보지는 않았다.
그런데 여기서 궁금한 점이 생겼다.
prev를 사전에 따로 선언하지 않았는데도 어떻게 과거의 값이라는 것을 알까? (바보같은 질문 같은데 이해가 안 간다)
👉🏻 커뮤니티에 질문했고 많은 답변을 받았는데도.. 이해가 잘 안 간다.. 이후에 이해하게 되면 정리해서 이곳에 추가해야겠다.
setTodoData(prev => [...prev, newTodo])
는 setTodoData([...todoData, newTodo])
와 같은 의미일까?? 이렇게 바꿔도 동작할까?
👉 바꿔서 실행시켜봤는데 정상적으로 동작한다!! 같은 의미인 것 같다.
드디어 ToDo앱 추가 기능 만들기가 끝났다! 모르는 게 너무 많아서 막힐 때마다 공부해가면서 하느라 시간이 정말 많이 걸린 것 같다..
그래도 혼자 만들어보는 과정은 꼭 필요한 것 같다. 다음 포스팅에서는 완료 표시하기, 항목 삭제 기능을 구현해 볼 것이다!
모르시는분이지만 잘보고 가요!!
(이꽉물 모름)