현재 수강 중인 '리액트 완벽 가이드'라는 강의에 나오는 이 투두리스트를 만들고 싶어졌다.
일단 하루를 잡고 할 수 있는 만큼만 해보기로 했다.
👉 한나절 정도 집중해서 한 것 같은데 추가 기능이랑 빈칸 검증 기능만 최종적으로 완성되었다😂
그리고 하다가 막히는 것은 ChatGPT에 물어보면서 해보기로 했다.
ChatGPT란? 개발을 포함한 다양한 지식 분야에서 상세한 응답과 정교한 답변으로 인해 집중 받고 있는 대화형 인공지능 챗봇. 잘못된 답변을 하기도 한다!
이곳에서 ChatGPT를 사용하여 질문하는 법을 배웠다.
👉 https://www.housecode.org/chat-gpt-guide
일단 결과부터 쓰자면 완성된 화면은 아래와 같다.
일단 가장 상위에 있는 App 컴포넌트를 입력하는 곳과 투두리스트들이 쌓이는 곳으로 나눴다.
// App.js
const App = () => {
return (
<div className="App">
<Input />
<Goals />
</div>
);
}
입력폼인 Input 컴포넌트를 작성해보았다.
// Input.js
const Input = (props) => {
const [enteredGoal, setEnteredGoal] = useState('');
const goalChangeHandler = (e) => {
setEnteredGoal(e.target.value)
}
const submitHandler = (e) => {
e.preventDefault();
props.onSaveGoal(enteredGoal)
setEnteredGoal('');
}
return (
<form onSubmit={submitHandler}>
<label>수업 목표</label>
<input type='text' value={enteredGoal} onChange={goalChangeHandler} />
<button type='submit'>목표 추가하기</button>
</form>
);
};
input 창에 입력되는 입력값인 enteredGoal와 이를 업데이트하는 함수 setEnteredGoal을 만들었다.
const [enteredGoal, setEnteredGoal] = useState('');
입력값이 변화하면 goalChangehandler
라는 함수를 실행시키고, 이 함수는 setEnteredGoal(e.target.value)
로 입력값 enteredGoal을 업데이트시켰다.
<input type='text' value={enteredGoal} onChange={goalChangeHandler} />
const goalChangeHandler = (e) => {
setEnteredGoal(e.target.value)
}
'목표 추가하기' 버튼을 누르면 type='submit'
으로 인해 전송되며 버튼을 감싸는 form 태그에서 onSubmit이 실행되어 submitHandler
함수가 실행되게 된다.
<form onSubmit={submitHandler}>
<button type='submit'>목표 추가하기</button>
</form>
(1) submitHandler
함수는 먼저 e.preventDefault();
를 통해 페이지가 새로고침되어 데이터가 날아가지 않도록 한다.
(2) props.onSaveGoal(enteredGoal)
를 통해 props로 받은 onSaveGoal 함수에 enteredGoal을 넣어서 실행시키는데, onSaveGoal은 이를 부른 상위의 컴포넌트인 App.js에 있다. 입력값을 App.js로 끌어올려 그곳에서 사용하기 위해 props.onSaveGoal(enteredGoal)
를 썼다.
(3) setEnteredGoal('');
을 통해 '목표 추가하기' 버튼을 누르면 입력값이 남아있는 input태그 안의 값을 빈 문자열로 초기화시킨다. 투두리스트 추가를 한 뒤에도 추가한 내용이 입력창에 계속 남아있으면 불편할 것이다. 그래서 추가를 하면 입력창에 있던 문자열이 없어지게 했다.
const submitHandler = (e) => {
e.preventDefault();
props.onSaveGoal(enteredGoal)
setEnteredGoal('');
}
입력값이 전송될 때마다 입력된 값들이 누적되는 배열 displayInputs
를 만들었다.
const App = () => {
const [displayInputs, setDisplayInputs] = useState([]);
const onSaveGoal = (goal) => {
setDisplayInputs([...displayInputs, goal]);
}
return (
<div className="App">
<Input onSaveGoal={onSaveGoal}/>
<Goals input={displayInputs}/>
</div>
);
}
const [displayInputs, setDisplayInputs] = useState([]);
submitHandler
함수가 실행되었고, 이 함수는 onSaveGoal
함수를 실행하였다. onSaveGoal
함수에서는 입력값을 받아 이전에 받은 입력값들에 새롭게 받은 입력값을 추가하여 state로 저장한다.const onSaveGoal = (goal) => {
setDisplayInputs([...displayInputs, goal]);
}
입력값들을 모으는 코드를 어떻게 작성해야 할지 몰라 ChatGPT에게 물어봤다. 덕분에 displayInputs
라는 state를 만들 수 있었다.
하지만 이를 적용하는 과정에서 오류가 있었다. 그래서 콘솔을 찍어봤는데, 1을 입력하고 전송 버튼을 누르면 빈 배열이 콘솔에 찍힌다. 그리고 2를 입력하고 전송하면 그제서야 1이 콘솔에 찍힌다. 그리고 2를 누르면 [1, 2]가 콘솔에 출력된다.
이렇게 동기화가 제대로 되지 않는 오류가 있어 이번에도 ChatGPT에 질문했다. 답변은 input의 value를 state로 관리하고 onChange 이벤트에서 state 값을 업데이트하라고 했다.
기존에도 이렇게 하고있긴 했지만 답변을 읽고 state 부분에서 뭔가 잘못했다는 생각이 들었고, state 부분을 고치게 되었다. 지금은 enteredGoal을 Input 컴포넌트에서 선언하고 관리하고 있지만, 이전에는 App 컴포넌트에서 선언하고 사용하려고 했던 것 같다. 해결 과정을 기록하지 않아서 오류가 났던 코드를 정확히는 모르겠지만, 어쨌든 state 부분을 수정하여 해결했다.
+클래스형 컴포넌트가 아니라 함수형 컴포넌트로 보고 싶어서 이렇게 요청했다. 그리고 정확한 답변을 받을 수 있었다.
App.js에서 displayInputs를 input이라는 props로 받아 Goals 컴포넌트에서 map()을 이용하여 하나씩 화면에 출력해주었다.
index를 key로 쓰면 안되지만, 간단하게 하기 위해 여기서는 이렇게 쓰기로 했다.
// App.js
<Goals input={displayInputs}/>
// Goals.js
const Goals = (props) => {
return (
<div>
{props.input.map((goal, index)=> (
<div key={index}>{goal}</div>
))}
</div>
);
};
값을 입력하고 추가하면 아래에 값이 쌓이게 하는 것에 성공했다!
입력칸에 아무것도 입력하지 않고 추가하기 버튼을 누르면 입력칸과 label의 글자색이 변하도록 해보자.
// Input.js
import './Input.css'
import { useState } from 'react';
const Input = (props) => {
const [isValid, setIsValid] = useState(true);
const [enteredGoal, setEnteredGoal] = useState('');
const goalChangeHandler = (e) => {
setEnteredGoal(e.target.value);
}
const submitHandler = (e) => {
e.preventDefault();
if (enteredGoal === '') {
setIsValid(false);
return;
}
setIsValid(true);
props.onSaveGoal(enteredGoal)
setEnteredGoal('');
}
return (
<form onSubmit={submitHandler}>
<label className={!isValid ? 'invalid' : ''}>수업 목표</label>
<input className={!isValid ? 'invalid' : ''} type='text' value={enteredGoal} onChange={goalChangeHandler} />
<button type='submit'>목표 추가하기</button>
</form>
);
};
export default Input;
// Input.css
input:focus {
background-color: beige;
}
label.invalid {
color: red;
}
input.invalid {
background-color: red;
}
그런데!! 이렇게 하면 의도대로 되지 않는다. 빈칸을 추가하고 다시 커서를 입력창 안에 두고 한 글자라도 입력을 하면 오류 표시가 사라지는 것을 원했는데 그렇게 되지 않는다. 한번 색이 변하면 계속 빨간 창 내에서 글자를 입력하게 된다.
이건 아래처럼 일단 입력을 하면(onChange, 즉 goalChangeHandler가 실행되면) isValid를 true로 바꿔주는 코드를 작성하여 해결했다!
const goalChangeHandler = (e) => {
setEnteredGoal(e.target.value);
setIsValid(true); // 이 부분 추가
}
성공!
추가한 투두들이 Goals 컴포넌트에 쌓이고 있다. 그 중에서 한 투두를 클릭하면 그 투두가 삭제되게 해보자.
그런데..! 내가 예전에 쓴 포스팅 [리액트] 투두리스트 만들기(4) - 삭제 편이나 다른 강의를 들어보면 모두 id를 써서 삭제 기능을 구현하고 있다.
chatGPT에게 물어봐도 id가 있어야 하는 것 같다.
id 없이 하는 방법으로는 index를 사용하는 방법이 있다는데, 어쨌든 고유한 식별번호가 있어야 한다는 것 같다.
그럼 아예 입력값 state인 enteredGoal
가 문자열이 아니라 {id: 1, text:text}와 같은 객체여야 한다는 것이다.
👉 몇시간 삽질해보았는데,, 결국 안돼서 나중에 해보고 삭제 기능에 관한 포스팅을 꼭..해보기로 했다. 이전에 투두리스트 삭제에 관한 포스팅 [리액트] 투두리스트 만들기(4) - 삭제 편을 쓴 적이 있지만, 여기서는 하드코딩된 배열을 삭제한 것이었고 지금은 사용자가 직접 입력폼에 입력한 투두리스트를 삭제한다는 차이가 있다. 꼭.. 해볼 것이다..
이번 것은 그렇게 어렵지 않았다... 삭제 기능을 만들려고 하기 전까지는...^_^
꼭 여기서 삭제하는 방법을 알아낼 것이다.. 그리고 포스팅 할 것이다..
얼마 전에 처음 배운 데이터 끌어올리기를 다양하게 적용해본 것 같다. 하지만 아직 state를 어디에서 끌어올리고 끌어올린 state를 어디에 써야 하는지는 잘 모르겠다😂
그리고 css를 어느정도 해서 좀 더 예쁘게 만들고 싶었지만 기력이 다한 관계로 만들지 못했다. 이 폴더는 이번이 끝이 아니라 앞으로 보완해나갈 예정이다. 일단 css로 어느정도 꾸미고, 삭제하고, 수정하는 기능 등을 만들고 싶다. 그리고 언젠가는 Drag and Drop 기능도 추가해보고 싶다!