✍️ 독학하면서 투두리스트는 항상 JS강의 커리큘럼에 있어서 따라 제작해 보았던 것 같다.
하지만 이후 이제까지 팀프로젝트나 자체프로젝트에서는 어떤 리스트를 추가하거나 삭제, 수정, 필터링하는 아주 기본적인 기능은 쏙 빼고(?) 다른 기능들만 담당했는데..😅
오랜만에 React복습할겸 (내가 재미있고 싶어서) 기록 하게 되었다.✨
웹소켓(socket.IO) 채팅 통신도 구현했는데 투두리스트쯤이야! (하고 생각했다가 필터링에서 약간의 삽질을 했다..)
😌 하고나니 바닐라JS보다 엄청 간단하구나 느꼈다..
create react app
yarn add react-icons
yarn add styled-components
😅 시작하기에 앞서 대략적으로 짠(휘갈긴) 구조...💦🔥
얼른 현업경험을 많이 쌓아서 효율적인 구조를 더 많이 접하고 싶다..
Header
1) 마우스호버
<Wrapper>
<RiMoonClearLine
size={23}
/>
</Wrapper>
const Wrapper = styled.div`
&:hover {
cursor: pointer;
}
`;
❓ 풀어야 할 것
<RiMoonClearLine
size={23}
onMouseOver={({ target }) => (target.style.color = 'white')}
onMouseOut={({ target }) => (target.style.color = 'black')}
/>
🤔 이렇게 할 수는 있지만 선위에 있을때만 작동한다.
찾아보니 svg형식이어서 그런 것 같은데, 이전 프로젝트를 할때는 따로 svg 코드를 만져서 해결했는데, 혹시 다른 방식으로 해결해 볼 순 없을지 고민중! 🧐
Filter
1) 호버 애니메이션
const FilterBtn = styled.button`
margin: 1.2rem 0rem;
height: 2.2rem;
font-size: 0.9rem;
color: #999999;
background: none;
font-weight: 400;
transition: color 250ms ease-in-out;
&:first-child {
...
}
&:after {
display: block;
content: '';
transform: translate(5rem, 0);
border-bottom: solid 2px #fff;
transform: scaleX(0);
transition: transform 250ms ease-in-out;
}
&:hover {
color: white;
cursor: pointer;
}
&:hover:after {
transform: scaleX(1);
}
`;
List
1) 체크박스 커스텀
참고한 블로그
const CheckBox = styled.input`
margin: 1rem 0.8rem 0 0;
appearance: none;
width: 1.5rem;
height: 1.5rem;
border: 1.5px solid gainsboro;
border-radius: 0.35rem;
&:checked {
border-color: transparent;
background-image: url("...");
background-size: 100% 100%;
background-position: 50%;
background-repeat: no-repeat;
background-color: #9eb3da;
}
`;
AddForm
1) Form과 input
form
태그 내부에 input
태그와 button
태그를 넣고 추후 함수를 만들면서 onChange
와 onSubmit
, 버튼의 onClick
을 컨트롤 할 수 있도록 만들었다.1) 구조 생각해보기
AddForm에서 input
값과 Submit
값을 App컴포넌트로 올린 후 list컴포넌트에 내려주는 것으로 작성
2) useRef 사용하기
돔 요소를 조작하기 위함이지만 우리는 그저 아이디값을 부여하므로 input 태그에 굳이 넣지 않아도 된다는 점!
→ 그래서 그냥 ref를 사용해야하는 App 컴포넌트에 정의하고 사용했다.
3) 에러 해결과정
오랜만이라서 map을 어디에 넣어서 돌려줘야하는지 헷갈렸다. 😂
→ 결과적으로 List 컴포넌트에서 map을 돌린 후 ListBox로 taskList 정보를 넘겨주고 ListBox컴포넌트에서 그것을 렌더링 하도록 작성했다. ▼
taskList.map(()=>{}) (x)
taskList.map(()=>()) (o)
//
const List = ({ taskList }) => {
return (
<ListContainer>
{taskList.map((task) => (
<ListBox task={task} key={task.id} />
))}
</ListContainer>
);
};
4) 고유 아이디 부여하기
📌 다른방법
1. new Data()
2. uuid 라이브러리
const deleteTask = (e) => { // 1)
const deleteId = e.currentTarget.id;
setTaskList(
taskList.filter((task) => {
return task.id !== parseInt(deleteId);
})
);
};
1) 에러 해결하기
currentTarget 은 내가 그 버튼을 눌렀을 때, svg나 path에 따라서 target이 설정되는 것이 아니고, 그 evnet가 달린 컴포넌트가 선택되기 때문이다.
→ 따라서 e.target.id
(x) / e.currentTarget.id
(o)
이전 비슷한 경험을 한 적이 있어서 자문을 구한적이 있었는데 그때 기억을 떠올렸다.😄
👉 내가 해결했던 이전 경험
1) 아이템 체크박스 속성 이용하기
// List > ListBox
<CheckBox
type='checkbox'
checked={taskList.isChecked}
onChange={(e) => {
handleChecked(id, e); // taskList.map을 돌려서 얻어낸 각각의 task별 id
}}
></CheckBox>
// App
const handleChecked = (id, e) => {
setTaskList(
taskList.map((task) => {
if (task.id === id) {
return { ...task, isChecked: e.target.checked };
} else {
return task;
}
})
);
};
onChange
이벤트를 통해서 쉽게 해결할 수 있다.{ ...task, isChecked: e.target.checked }
맵을 통해서 받아오고있는 각각의 task 객체를 스프레드 연산자로 복사해준뒤, 맵을 통해 해체된 각각의 task객체 내의 isChecked라는 프로퍼티만 이벤트에서 제공하는 checked(true,false)를 이용해서 새로 갈아끼워 넣어주면 된다 🥲.. ❗️✔ ❓어떻게 작성하면 좋을까? 🤔 먼저 구상해보기 ▼
1) All, Active, Completed 가 담길 State를 추가한다.
const filtering = (filter) => {
setFilterType(filter);
};
...
return ( ...
<Filter filtering={filtering} />
이때 기본값은 ‘All’ 탭이 되도록 지정한다.
const [filterType, setFilterType] = useState('All');
2) 각 task의 객체 속성에 type 을 추가하여 All, Active, Completed가 담길 수 있도록 한다.
이때 기본값은 ‘Active’로 지정한다.
const addTask = (e) => {
e.preventDefault();
if (task === '') return;
setTaskList([
...taskList,
{ id: taskId.current, task, isChecked: false, type: 'Active' },
]);
taskId.current++;
setTask('');
};
2-1) ❗️ 여기서 중요한 점은 각 task.type 이 체크된 유무 맞는 필터값을 가지고 있어야한다는 것이다.
✍️ 이 부분의 코드가 잘 작성되어야 3번 UI렌더링까지 가능하다.
const handleChecked = (id, e) => {
setTaskList(
taskList.map((task) => {
// 내가 선택한 task의 id가 일치해야하고 && 체크가 true일때 'Completed'로 설정
if (task.id === id && e.target.checked) {
return { ...task, isChecked: e.target.checked, type: 'Completed' };
}
// 체크가 true가 아닌 false일때 이 문으로 넘어옴 && 내가 선택한 task의 id가 일치할때
else if (task.id === id) {
return {
...task,
isChecked: e.target.checked,
type: 'Active',
};
}
// 체크가 true든 false든 내가 선택한 값이 아닐때 변동주지 않기
else return task;
})
);
};
초기값은 Active이다. All이 아님을 주의하자.
체크를 할 경우 해당 체크 task만 Completed로 변경됨에 집중하자.
3) 현재 state의 filterType
과 task의 필터타입 task.type
을 비교해서 All일 경우와 Active, Completed일 경우를 구분하여 UI렌더링을 위한 return
을 한다.
const List = ({ taskList, deleteTask, handleChecked, filterType }) => {
return (
<ListContainer>
{taskList.map((task) => { // 이때 filter가 아닌 map을 써야 오류x
if (filterType === 'All') { // 초기값도 All이며, 내가 클릭한 필터타입이 All
return (
<ListBox
task={task}
deleteTask={deleteTask}
id={task.id}
handleChecked={handleChecked}
key={task.id}
/>
); // 내가 클릭한 필터타입 === task가 가지고 있는 type의 이름이 일치할 경우
} else if (filterType === task.type) {
return (
<ListBox
task={task}
deleteTask={deleteTask}
id={task.id}
handleChecked={handleChecked}
key={task.id}
/>
);
}
})}
</ListContainer>
);
};
+) 추가 UI 수정
const Task = styled.div`
margin-top: 0.2rem;
font-size: 1rem;
line-height: 3rem;
text-decoration: ${(props) =>
props.checked ? 'line-through rgba(187, 200, 222, 0.7) 2px' : 'none'};
color: ${(props) => (props.checked ? '#aaadb1' : 'none')};
`;
+) 체크된 Task UI 적용하기
const Task = styled.div`
margin-top: 0.2rem;
font-size: 1rem;
line-height: 3rem;
text-decoration: ${(props) =>
props.checked ? 'line-through rgba(187, 200, 222, 0.7) 2px' : 'none'};
color: ${(props) => (props.checked ? '#aaadb1' : 'none')};
`;
1) useContext 사용
<Task checked={task.isChecked} darkMode={isDarkMode}> ...
color: ${(props) => {
if (props.checked && props.darkMode) {
return '#aaadb1';
} else if (props.darkMode) {
return 'white';
}
}};
✍️ 이전 바닐라JS 에서 활용했던 방법을 적용해 보았다. 다 하고나니 이부분이 특히 바닐라JS와 리액트의 차이가 더 느껴졌다..🤔
1) 먼저 localStorage에 아이템을 저장하자
JSON.stringify
useEffect(() => {
localStorage.setItem('tasks', JSON.stringify(taskList));
}, [taskList]);
2) localStorage에서 아이템을 가져오자
JSON.parse
function taskListFromLocalStorage() {
const taskLists = localStorage.getItem('tasks');
return taskLists ? JSON.parse(taskLists) : [];
}
3) State 초기 상태 설정하기
[]
이므로 빈배열을 setStorage 해서 스토리지에도 []이 담기고 UI도 아무것도 남지 않게 된다.const [taskList, setTaskList] = useState(() => taskListFromLocalStorage());
function taskListFromLocalStorage() {
const taskLists = localStorage.getItem('tasks');
return taskLists ? JSON.parse(taskLists) : [];
}
// 함수 선언식으로 해야 위치가 아래여도 함수호이스팅에 의해서 제대로 작동한다. (호이스팅이론은 내추측)
❗️ 이때 주의할점
- 만약 일반 함수호출로 전달하면 계속해서 해당 함수가 호출된다!!
(블로그참고)
✍️ 내가 이해한 것
1) 어짜피 task를 추가할때마다 useEffect에 의해 계속해서 정상적인 렌더링을 하게되고, 로컬스토리지에도 동기적으로 잘 저장이 되므로 UI단에서도 문제가 없다! (이부분이 JS로 투두를 만들때보다 간단해서 헷갈렸다.)
2) 새로고침시 날라가지 않도록 설정하기위해 초기값으로 getStorage를 설정한건 이해가 된다. 여기서 그냥 함수를 호출할 경우 매 렌더링이 이루어지므로 성능이 좋지 않다.
→ 따라서 콜백형태로 State의 초기값을 넣어줌으로써 초기 딱 한번만 실행되도록 한다. 이때 초기라는것은 새로고침을 눌렀을 때에만 스토리지에서 데이터를 불러와 UI렌더링을 해준다는 것 !! 🧐
1) trim()
활용하여 빈문자열 제어하기
const addTask = (e) => {
e.preventDefault();
if (task.trim().length === 0) return; // trim() 메서드 활용
setTaskList([
...taskList,
{ id: taskId.current, task, isChecked: false, type: 'Active' },
]);
taskId.current++;
setTask('');
};
2) label 적용하기
<Task
checked={task.isChecked}
darkMode={isDarkMode}
htmlFor={task.id}
>
const Task = styled.label`
...
`
📌 추가 CSS tip!
1. Css파트 보더값 두군데만 변경하고싶으면 단축속성명이있다.
ex)border-bottom-right-radius: 8px;
2. filter: brightness(130%) -> 기존색에서 명도가 밝아진다.
이제 가장 궁금하고 하고싶었던 과정만 남았다.✨
Solution과 비교하며 내가짠 코드와 같이 구조 비교하며 분석해보기 😆✍️✨