<지난 포스팅>
리액트로 ToDo앱 만들기(1) - UI 편
리액트로 ToDo앱 만들기(2) - 추가 편
리액트로 ToDo앱 만들기(3) - 완료 편
리액트로 ToDo앱 만들기(4) - 삭제 편
지난 네 개의 포스팅에서는 ToDo앱 UI, 추가, 완료, 삭제 기능을 구현해보았다.
이번 포스팅에서는 하나의 컴포넌트(App.js)를 여러 개의 컴포넌트로 분리해볼 것이다.
컴포넌트를 분리하면 코드 수정이 용이하고 코드의 재사용성을 높일 수 있다는 장점이 있다!
이번 포스팅도 전과 같이 강의(따라하며 배우는 리액트 A-Z)에서 배운 코드를 보지 않고 '검색 + 혼자 힘'으로 (삽질하며) 투두앱을 만드는 것이 목표이다.
개인적으로 컴포넌트를 분리하는 과정에서 props로 컴포넌트에 값을 전달하는 부분이 어려워서 이 부분을 중점적으로 공부하려고 한다.
컴포넌트를 분리하기 전에 컴포넌트의 특징에 대해 간단히 알아보려고 한다.
리액트에서 컴포넌트의 이름을 만들 때 이름은 대문자로 시작하는 한 단어이고 한 단어 안에 여러 단어를 결합하면 중간에 시작하는 서브 단어는 대문자로 시작해야 한다. (예: ExpenseItem.js
)
리액트에 있는 컴포넌트는 단지 자바스크립트 함수이며 html코드를 반환하는 함수이다.
function ExpenseItem() {
return <h2>Expense item!</h2>
}
export default ExpenseItem;
파일 이름인 ExpenseItem
을 html처럼 사용할 수 있다. 대문자로 시작한다는 점만 html과 다르다.
<input />
)<ExpenseItem />
)기존에는 아래와 같이 컴포넌트가 App 하나였는데, App.js에 모든 코드가 있었다.
(사진에는 js가 아닌 jsx로 표시했는데, 원래는 jsx가 맞는데 js로 써도 되는 것이라고 함)
그걸 Form
과 Lists
, List
이렇게 세 개의 컴포넌트로 분리하려고 한다.
먼저 App.js을 확인해봤을 때, Form 부분은 아래에 표시해둔 부분인 것을 알 수 있다. 그래서 일단 이 부분 모두를 Form.js에 잘라넣기했다.
코드를 붙여넣기 전에 리액트 코드 템플릿을 작성해줘야 하는데, vscode에서는
Reactjs code snippets
라는 익스텐션을 설치하고rsc
(함수형 컴포넌트) 혹은rcc
(클래스형 컴포넌트) 입력 후 탭을 눌러 리액트 코드 템플릿을 손쉽게 작성할 수 있다.
return (
<div className='container'>
<div className='contents'>
<h1>할 일 목록</h1>
<form onSubmit={onSubmit}>
<div className='lists'>
{todoList.map((todoItem) =>(
<div className='list' key={todoItem.id}>
<input
onClick={()=>checkedToggle(todoItem.id)}
className="checkbox"
type='checkbox'
/>
<span className={ "listContent" + (todoItem.checked ? " checked" : '')}>{todoItem.text}</span>
<button
type='button'
className='deleteBtn'
onClick={() => onDelete(todoItem.id)}
>X</button>
</div>
))}
</div>
{/* Form 부분은 여기부터 */}
<div className='form'>
<input
onChange={onChangeInput}
className='inputText'
type='text'
placeholder='해야 할 일을 입력하세요.'
value={text}
/>
<input
className='inputSubmit'
type='submit'
value='입력'
/>
</div>
{/* 여기까지 */}
</form>
</div>
</div>
);
<Form />
써주기그리고 잘라 넣은 자리에는 Form 컴포넌트가 대신하는 것을 알려주기 위해 아래와 같이 <Form />
을 써주었다.
<Form />
App 컴포넌트에서 Form 컴포넌트를 사용해야 하므로 Form 컴포넌트를 임포트해줬다.
import Form from "./components/Form";
그러면 이런 오류가 뜬다.
onChangeInput
함수와 state인 text
가 정의되지 않았다고 한다.
그래서 일단 App.js에 있던 onChangeInput
함수를 잘라내어 Form.js에 붙여넣기 했다.
그럼 아래와 같이 useState에서 정의해주는 text
와 setText
가 정의되지 않았다고 뜬다. 그래서 단순하게 App.js에서 useState
부분을 잘라내어 Form 컴포넌트에 붙여 넣으면 되지 않을까? 하는 희망을 갖고 그렇게 해봤다.
위와 같은오류가 뜬다🥺 자세히 보니 Form.js에 text
, setText
가 정의되지 않았다고 뜬다.
그러니까 Form.js로 옮긴 text
와 setText
가 App.js, Form.js 둘 다에서 쓰이고 있는데, App.js에 있던 정의를 Form.js에서 옮겨서 App.js에서는 정의가 안 되어있는 상태인 것이다.
이때에는 props를 사용해서 App.js에 있는 데이터 text와 setText를 Form.js로 넘겨줘야 한다.
상위(부모) 컴포넌트가 하위(자식) 컴포넌트에게 데이터를 넘겨줄 때 사용한다.
읽기 전용으로, 하위 컴포넌트는 전달받은 props를 변경할 수 없다.
일단 아래와 같이 text
, setText
를 props로 Form 컴포넌트에 넘겨줬다. left={right}
일 때 left
는 props를 넘겨받은 쪽에서 쓰는 이름이고 right
는 넘겨주는 쪽에서 쓰는 이름이다.
둘의 명칭은 달라도 되지만 헷갈리지 말라고 아래와 같이 같은 이름으로 정했다.
<Form text={text} setText={setText} />
그리고 Form 함수에서 받은 데이터를 props라는 이름으로 받는다는 표시로 () 안에 props를 적어준다.
const Form = (props) => {
}
마지막으로 Form.js에서 text
대신 props.text
, setText
대신 props.setText
라고 적어준다.
Form 컴포넌트를 분리했어도 오류 없이 잘 작동한다.
일단 Form 컴포넌트를 분리한 것과 같이 Lists에 해당하는 부분을 App.js에서 잘라내어 Lists.js에 붙여넣기 했다.
<form onSubmit={onSubmit}>
<div className='lists'>
{todoList.map((todoItem) =>(
<div className='list' key={todoItem.id}>
<input
onClick={()=>checkedToggle(todoItem.id)}
className="checkbox"
type='checkbox'
/>
<span className={ "listContent" + (todoItem.checked ? " checked" : '')}>{todoItem.text}</span>
<button
type='button'
className='deleteBtn'
onClick={() => onDelete(todoItem.id)}
>X</button>
</div>
))}
</div>
<Form text={text} setText={setText} />
</form>
Lists에서 쓰는 함수로는 onDelete
, checkedtoggle
, onSubmit
이 있었다.
아래처럼 세 개의 함수를 Lists 컴포넌트로 옮겼다.
const onDelete = (id) => {
setTodoList(todoList.filter(todoItem=>
todoItem.id !== id
));
};
const onSubmit = (e) => {
e.preventDefault();
if (!text) return;
const nextTodoList = todoList.concat({
id: no.current++,
text,
checked: false,
});
setTodoList(nextTodoList);
setText('');
};
const checkedToggle = (id) => {
setTodoList(todoList.map(todoItem=>
todoItem.id===id ? {...todoItem, checked: !todoItem.checked} : todoItem
))
};
Lists.js에서는 아래의 no
, text
, setText
, todoList
, setTodoList
를 모두 사용하므로 이 다섯 개를 모두 props로 넘겨주었다.
const no = useRef(1);
const [text, setText] = useState('');
=const [todoList, setTodoList] = useState([]);
그리고 Lists 컴포넌트에 인자로 props를 받게 하고 no
, text
, setText
, todoList
, setTodoList
앞에 모두 props.
를 붙여주었다.
그런데 그렇게 했더니 아래와 같이 두개의 오류가 뜬다.
첫번째 Module not found: Error:
을 해결하려고 엄청나게 고민했는데, 결국 경로 문제였다. 경로 문제인 건 알고 있었는데 아무리 봐도 뭐가 잘못된 건지 모르다가, 누가 경로 확인해보라고 해서 다시 보니까 뭐가 잘못됐는지 보였다....!!!
경로가 왜 잘못됐냐면, App.js와 Lists.js, Form.js가 다른 폴더 안에 있기 때문이었다.
App.js와 다르게 Lists.js와 Form.js는 components라는 폴더에 있었고, 그래서 같은 Form 컴포넌트를 임포트하더라도 App.js에서 Form.js를 임포트할 때 쓰는 경로와 Lists.js에서 Form.js를 임포트할 때 쓰는 경로는 달랐는데 나는 components라는 폴더를 만든 걸 간과하고 경로를 적었다!!
두 번째 오류는 Lists.js에서 text
가 정의되지 않은 오류였다. 그래서 props.text
라고 했는데 그런데 그래도 오류가 나는 것이다...! 더 자세히 확인해봤다.
아래의 코드이다.
보니까 props.text
도 다른 것 처럼 text: props.text
라고 해야 할 것 같아서 그렇게 하니까 됐다! 아니 왜 처음에 text: text
가 아니라 text
라고만 했는데 된 거지? 그건 잘 모르겠다.
const nextTodoList = props.todoList.concat({
id: props.no.current++,
props.text,
checked: false,
});
오류 없이 Lists 컴포넌트 분리에 성공했다!
마지막으로 List 컴포넌트를 분리해보자. Lists에서 List에 해당하는 부분만 새 컴포넌트로 분리하면 된다.
아래는 Lists 컴포넌트에 있는 코드이다. 여기서 map 안에 있는 className이 list인 div 태그와 그 안에 있는 부분을 List 컴포넌트로 분리할 것이다.
return (
<form onSubmit={onSubmit}>
<div className='lists'>
{props.todoList.map((todoItem) =>(
<div className='list' key={todoItem.id}>
<input
onClick={()=>checkedToggle(todoItem.id)}
className="checkbox"
type='checkbox'
/>
<span className={ "listContent" + (todoItem.checked ? " checked" : '')}>{todoItem.text}</span>
<button
type='button'
className='deleteBtn'
onClick={() => onDelete(todoItem.id)}
>X</button>
</div>
))}
</div>
<Form text={props.text} setText={props.setText} />
</form>
);
분리하면 아래와 같은 오류가 발생한다. 해결하기 위해 checkedToggle
과 onDelte
함수를 일단 먼저 가져오려고 한다.
checkedToggle
과 onDelte
함수를 List 컴포넌트로 옮겼다.
그러면 함수 관련 오류는 사라지고 아래와 같은 오류가 생긴다. 정의되지 않았다는 todoItem을 props로 넘겨주면 될 것 같다.
다음과 같이 todoItem을 props로 넘겨줬다.
<List todoItem={todoItem} />
하지만 문제는..!!
이렇게 하니까 삭제 기능도 완료 기능도 동작하지 않는다. 왜일까??
아래의 코드에서 뭔가가 잘못된 것 같은데, 잘 모르겠다. 이건 강의에서 어떻게 했는지 보고 와야겠다...!
const onDelete = (id) => {
props.setTodoList(props.todoList.filter(todoItem=>
todoItem.id !== id
));
};
const checkedToggle = (id) => {
props.setTodoList(props.todoList.map(todoItem=>
todoItem.id===id ? {...todoItem, checked: !todoItem.checked} : todoItem
))
};
아니, 봐도 모르겠음..... 혼자 다시 코드를 짜는 과정에서 강사님과 코드가 많이 달라져서 이해를 못 하겠다.
일단 넘겨주는 props를 todoItem에서 그 안에 있는 id, text, checked를 각각 넘겨주는 것으로 변경했다.
<List
// todoItem={todoItem}
id={todoItem.id}
text={props.text}
checked={todoItem.checked}
todoList={props.todoList}
setTodoList={props.setTodoList}
/>
아니 그랬더니 아래와 같은 오류가 난다.... 😤
id부분에서 잘못된 것 같아서 id 부분을 수정해보기로 했다.
이부분에서 몇 시간 동안 헤맸는데, 결국 잘못된 부분을 찾는 데 실패했다 ^_^
아는 분께 도움을 구한 결과....!!!! List의 props 중 text={props.text}
를 text={todoItem.text}
로 바꿔야 했다. text={props.text}
라고 하면 List의 text
와 Form의 props로 주는 text
가 같아지는 불상사가 발생한다.
왜 이걸 몰랐을까... !
<List
id={todoItem.id}
checked={todoItem.checked}
// text={props.text} 이거 대신 아래 거로 바꿔야함
text={todoItem.text}
todoList={props.todoList}
setTodoList={props.setTodoList}
/>
))}
<Form text={props.text} setText={props.setText} />
드디어 완성했다!!! App 컴포넌트를 Lists, List, Form 컴포넌트로 분리하는 데 성공했다!!
처음에는 props가 익숙하지 않아서 두려움을 가지고 컴포넌트 분리를 시작했는데, 한나절 훨씬 넘게 걸리긴 했지만 결국 완성했다.
마지막에 Lists 컴포넌트 안에 List와 Form 컴포넌트가 동시에 있어서 props를 주는 부분에서 많이 헷갈렸다. 그래도 차근차근 하니까 결국 되긴 되는구나 싶었다! 앞으로는 익숙하지 않거나 잘 모르겠는 부분은 이론만 보지 말고 이렇게 일단 코드로 쳐보며 배우는게 낫겠다는 생각을 했다.
다음 포스팅에서는 edit 버튼을 만들고 항목 수정 기능을 구현하는 포스팅을 할 것이다. 강의를 들으며 수정 기능을 구현하는 게 가장 어려웠어서 걱정도 되지만, 어쨌든 만들어보자!!