💻 github Repo : https://github.com/minngki/todo-app
회사에서 메타데이터 관리 운영을 위해 어드민 페이지를 제작하라는 거대한 임무를 받고 도전하는 도중,
처음으로 회사 사수 분께서 "혼자서 다 해봐라~"하고 던져주신 과제..
Front-end, React는 완전 처음이라 맨땅에 헤딩 식으로 시작해서 todolist만 2주라는 긴 시간이 걸렸고, 그만큼 상태관리의 개념이 좀 잡히기 시작했던 것 같다.
내가 참고했던 코드는 redux를 사용해서 할 일들의 데이터를 저장했는데, 컴포넌트 간 연결 구조와 상태관리 부분을 이해하고자 리스트를 props로 넘기는 방식으로 코드를 짰다. 사실 redux마저도 공부하기 벅찼다..
먼저 컴포넌트 구조를 짜보자. 나는 총 5개의 컴포넌트로 레이아웃을 구성했다.
<TodoTemplate> // 컨테이너 박스
<TodoTitle /> // 리스트의 상단
<TodoCreate> // 할 일 입력창
<TodoList> // 할 일 목록
<TodoItem /> // 할 일
</TodoList>
</TodoCreate>
</TodoTemplate>
물론 App의 구성이랑은 다르지만 컴포넌트 간 연결의 이해를 돕고자 tsx 방식으로 표현해보았다.
컴포넌트 구성할 때도 생각보다 어려웠는데, 원하는 action(event)에 초점을 두고 어떤 컴포넌트에 해당될지 생각하니까 비교적 수월했다.
import TodoCreate from './components/TodoCreate'
import TodoTemplate from './components/TodoTemplate'
import TodoTitle from './components/TodoTitle'
const App = () => {
return (
<TodoTemplate>
<TodoTitle />
<TodoCreate />
</TodoTemplate>
)
}
export default App
컴포넌트를 연결하다보니, 다른 사람들처럼 깔끔하게 모든 컴포넌트가 App에서 확인될 수 있게끔 코드를 짜진 못 했다.
TodoCreate에서 할 일의 input 값을 받기 때문에 핵심 내용인 리스트 관련 컴포넌트 중 가장 상위 컴포넌트로서 TodoList, TodoItem 컴포넌트에 차례로 props를 전달한다.
실제 코드를 짤 때는 각 컴포넌트를 생성할 때 마다 하나의 css파일에 추가하는 방식으로 진행했고, 모듈css를 사용했다.
tailwind는 코드가 좀 지저분한 감이 있어서 선호하지 않는 편이고 각 컴포넌트에 css 내용을 둬도 지저분한 느낌이 들어서 모듈css를 선호하는 편이다. 쉽기도 하고..
CSS는 아직 부족한 게 많아서 출처에 남긴 코드를 입맛에 맞게 가져왔다.
본격적으로 컴포넌트를 분해해보자!
import todo from './Todo.module.css'
const TodoTemplate = ({ children }: any) => {
return <div className={todo.template_box}>{children}</div>
}
export default TodoTemplate
템플릿은 배경이 되는 박스에 해당된다!
처음엔 props에 아무 내용을 작성하지 않았는데,
'{ children: Element; }' 유형에 'IntrinsicAttributes' 유형과 공통적인 속성이 없습니다.ts(2559)
이런 오류가 떴다. ㅎㅎ
상위 컴포넌트에서 children을 props로 두면서 안의 내용을 전달 받아야 한다. children이라는 개념을 처음엔 몰랐는데 알고 나니까 너무 당연하기도 했다..
Point
✅ 하위 컴포넌트의 내용을 보여주기 위해서는 children을 사용한다 !
import todo from './Todo.module.css'
const TodoTitle = () => {
const today = new Date()
const dateString = today.toLocaleString('ko-KR', {
year: 'numeric',
month: 'long',
day: 'numeric',
})
const dayName = today.toLocaleString('ko-KR', { weekday: 'long' })
return (
<>
<div className={todo.title}>To Do List</div>
<div className={todo.head}>
{dateString}
<div className={todo.day}>{dayName}</div>
</div>
</>
)
}
export default TodoTitle
TodoTitle에선 말 그대로 제목이 포함되어 있고 오늘 날짜가 나타나도록 한다.
생성자로 호출하여 Date
객체를 today
변수로서 선언하고, 날짜와 요일을 따로 보여주기 위해 dateString
, dayName
로 각각 선언한다.
import { useRef, useState } from 'react'
import { MdAdd } from 'react-icons/md'
import todo from './Todo.module.css'
import TodoList from './TodoList'
export interface IList {
id: number
text: string | null
done: boolean
}
우선 input받을 컴포넌트이므로 list들의 type을 정의한다.
할 일들을 삭제하거나, 완료한 할 일을 관리하기 위해 id
, done
을 추가한다.
const TodoCreate = () => {
const [open, setOpen] = useState<boolean>(false)
const [item, setItem] = useState<string>('')
const [inputList, setInputList] = useState<IList[]>([])
const listId = useRef<number>(0)
const onPlusToggle = () => {
setOpen(!open)
}
const handleChange = (e: any) => {
setItem(e.target.value)
}
const onSubmit = (e: any) => {
e.preventDefault()
if (!item) {
alert('입력해주세요.')
} else {
setInputList([
...inputList,
{
id: listId.current,
text: item,
done: false,
},
])
listId.current += 1
setItem('')
}
}
return (
<>
{inputList.length > 0 && (
<TodoList inputList={inputList} setInputList={setInputList} />
)}
<div className="absolute inset-x-0 bottom-0">
{open ? (
<>
<form className={todo.inputForm} onSubmit={onSubmit}>
<input
autoFocus
className={todo.inputField}
onChange={handleChange}
value={item}
placeholder="Write what to do, and Press the Enter"
/>
</form>
<button className={todo.xBtn} onClick={onPlusToggle}>
<MdAdd />
</button>
</>
) : (
<button className={todo.plusBtn} onClick={onPlusToggle}>
<MdAdd />
</button>
)}
</div>
</>
)
}
export default TodoCreate
open
: 토글 클릭 시 input 창이 보이게끔 하기 위해 boolean
타입의 상태값을 정의한다.
item
: 할 일의 내용을 담을 string
타입의 상태값을 정의한다.
inputList
: 앞서 정의한 input값에 따른 id와 done의 내용을 쌓을 array
타입의 상태값을 정의한다.
listId
: id
가 렌더링되면서 초기화되는 것을 방지하기 위해 useRef를 이용하여 값을 관리한다.
useRef의 내용 정리는 이 곳을 참고해주세요.
input value가 변경되는 되는 것을 감지하기 위해 event.target.value
를 사용하면서 useState를 이용해 상태 관리를 해주어야 하고, 받은 input value를 기반으로 onSubmit
함수를 통해 inputList를 생성한다.
open
이 true 일 때 input창을 보기 위해 렌더링 관련 return 값에서 open
값에 따른 조건문을 작성한다.
이제 와서 보이는 거지만, redux로 데이터를 관리하는 것은 아니기 때문에 form 태그의 onSubmit
함수로 굳이 submit할 필욘 없다. ㅎㅎ 이렇게 보면 고쳐볼 만한 것들이 많네유..
Point
✅onSubmit
함수에서e.preventDefault()
함수를 추가한 이유는, 이벤트가 발생할 때 마다 브라우저를 렌더링하기 때문에 막아주는 함수가 필요하다!
여기서 또 헤맸던 부분은, 내가 참고한 to do list는 button을 눌러서 할 일의 내용을 추가하는 게 아니라 enter key를 누를 시에 추가되는 로직이었기 때문에 괜히 이상한 고집으로 button을 만들고 싶지 않았다.
지금에서야 input 태그의 onKeyPress 함수로 enter key에 관한 설정을 하면 정상 작동할 것 같은데 난 왜 추가를 안 했는데도 잘 돌아가는지 아직 의문이다.. 일단은 돌아가니까 냅두기..
이제 받아둔 input list를 하위 컴포넌트인 inputList로 상태변환 set함수와 함께 props로 보내준다!
이제부터 정말정말 본격적으로 컴포넌트 간 props를 넘기기 시작하면서 애먹었던 부분..이다. 할 일 목록의 체크, 삭제 와 같은 작동 방식의 내용이다.
import { Dispatch, SetStateAction, useState } from 'react'
import todo from './Todo.module.css'
import { IList } from './TodoCreate'
import TodoItem from './TodoItem'
//상태변경 컴포넌트
const TodoList = (props: {
inputList: IList[]
setInputList: Dispatch<SetStateAction<IList[]>>
}) => {
const { inputList, setInputList } = props
const [toggle, setToggle] = useState<boolean>(false)
const onToggle = (id: number) => {
//click시 해당 할일 상태 변경
setInputList(
inputList.map((item) =>
item.id === id ? { ...item, done: !item.done } : item
)
)
setToggle(!toggle)
}
const onRemove = (id: number) => {
// click시 해당 할일 제거
setInputList(inputList.filter((item) => item.id !== id))
}
return (
<div className={todo.listBlock}>
<div className={todo.task}>
할 일 {inputList.filter((item) => !item.done).length}개 남음
</div>
{inputList.map((item) => (
<TodoItem
todos={item}
key={item.id}
onRemove={onRemove}
onToggle={onToggle}
/>
))}
</div>
)
}
export default TodoList
Point
✅ TodoCreate(부모)에서 만든 input 값들을 TodoList(자식)에서 toggle과 remove 기능에 따라 inputList 상태값을 변경하는 게 핵심
- 자식 → 부모 데이터 전송
- 자식에서 부모는 props를 전달할 수 없다.
- 따라서 자식에서 만든 값을 부모에게 전달해야한다면, 부모에서 useState의 변수를 선언하고 자식컴포넌트의 props로 보내면 된다.
- 자식은 받은 props로 상태값을 변경하는 기능들을 구현
- 부모 → 자식 데이터 전송
- props 전달이 가능하므로 부모에서 기능 구현을 하고 자식으로 보내기만 하면 된다.
onToggle
: toggle click 시 toggle UI 변경과 할 일 완료한 것들의 상태값 변경하는 함수
onRemove
: 쓰레기통 icon click 시 할 일을 삭제하며 상태값 변경하는 함수
각 항목(item)에 대하여 각 아이콘을 생성할 것이기 때문에 inputList들을 map을 통해 TodoItem에 props로 하나씩 보낸다고 생각하면 된다!
할 일 목록의 icon, text를 뿌려주는 UI적 내용만 담긴 비교적 간단한 컴포넌트이다.
import { MdDelete, MdDone } from 'react-icons/md'
import todo from './Todo.module.css'
import { IList } from './TodoCreate'
const TodoItem = (props: { todos: IList; onRemove: any; onToggle: any }) => {
const { id, text, done } = props.todos
const { onRemove, onToggle } = props
return (
<div className={todo.itemBlock}>
{done ? (
<>
<div className={todo.checkBtn} onClick={() => onToggle(id)}>
{done && <MdDone />}
</div>
<div className={todo.chkedText}>{text}</div>
</>
) : (
<>
<div className={todo.nonCheckBtn} onClick={() => onToggle(id)}></div>
<div className={todo.nonText}>{text}</div>
</>
)}
<div className={todo.remove} onClick={() => onRemove(id)}>
<MdDelete />
</div>
</div>
)
}
export default TodoItem
props로 보낸 todos
를 id
, text
, done
에 구조 분해하여 할당한다.
즉, todos.id
= id
, todos.text
= text
, todos.done
= done
인 것이다.
함수 props인 onRemove
, onToggle
은 그대로 받고 각 icon의 onClick
함수의 명령어가 된다.
할 일을 완료했는지 아닌지 여부에 따라 toggle 및 text의 UI가 다르므로 이 또한 done
에 따라 다르게 나타나도록 조건문을 return문에 작성한다.
처음에는 set함수를 보내는 것까지 생각을 못 해서 input value들만 TodoCreate에서 받기만 하고 TodoList에서 value의 리스트를 생성 및 관리하려고 했다.
onToggle
, onRemove
함수를 사용하면서 리스트의 상태를 또 관리해야하기 때문에 여기서 useState를 사용해서 또 정의해야하나.. 했지만 비효율적인 것 같고.. 어떻게 props를 건네주고 받을지 그 당시 나에겐 엄청난 숙제였다.. 그렇지만 set함수와 보내면 된다는 아주 간단한 흐름..
사실 더 처음에는 상태 관리 개념이 많이 부족해서 값만 보냈다가 이상하게 값이 들어와서 혼자 피식했던 기억..
주로 string
의 input을 받을 때는 값이 입력될 때 마다 event가 발생되는 비효율성 때문에 다 입력되면 값을 가져오기 위해 document.getElementById
를 사용해서 값을 가져왔는데, event.target
부분을 공부해보고 싶어서 이 방식으로 string
을 가져와봤다. 구체적으로 event 자체의 타입은 물론이고 원리나 기능을 몰라서 정말 많이 헤맸다.
그래서 아직까지 event 관련 type 정의는 모두 any
로 되어있다..
오히려 페이지를 직접 만들 때는 사수 분이 하시던 코드를 베껴 짜는 방식이어서 '왜?' 라는 질문 없이 하기 급급했다.
front-end를 배운다면 한 번쯤은 다 해봤다는 todolist..를 하고 나니 React의 생태계와 상태관리가 무진장 복잡하다는 걸 느꼈다..
모든 태그, 함수들의 기능을 다 알고 한 게 아니였고, 지금 보더라도 '엥 왜 이렇게 했지..?' 싶은 것들이 많다.
여유가 있을 때 마다 upgrade 해야지...!