🐱개요
배운 내용을 연습하기 위한 토이 프로젝트 입니다. 리액트, 리덕스 툴킷, styled-components 를 활용하여 만들었습니다.
LINK
https://hyejj19.github.io/react-todo/
GITHUB
🐱기능
- 할 일 추가 및 삭제
- 반응형 디자인
🐱결과물
🐱프로젝트 구조
폴더 구조
컴포넌트 구조
FigJam 을 사용해 컴포넌트 구조와 각 컴포넌트별 필요한 props와 state를 간단하게 정리해보았다.
초반에는 useState를 사용해서 전체 할 일 목록을 todoList 라는 state로 관리하고, 함수를 props로 전달해 하위 컴포넌트에서 상태를 끌어올려서 변경하도록 만들었다. 이후에 리덕스 툴킷을 적용하면서 수정했다.
🐱상세 기능
GlobalStyle 적용
styled-componensts 로 글로벌 스타일을 적용해 전체 색상과 html 기본 효과를 제거했다.
미디어 쿼리 반응형
최상위 컴포넌트 TodoApp 에서는 전체 컨테이너의 스타일을 아래와 같이 정의했다. 미디어쿼리로 반응형 디자인을 적용했다.
//TodoApp.js
const TodoContainer = styled.div`
width: 500px;
height: 800px;
border-radius: 20px;
background-color: var(--main-color);
padding: 50px;
transition: all 0.2s ease-in-out;
> h1 {
margin-top: 0;
color: white;
font-size: 3rem;
margin-bottom: 45px;
}
@media screen and (max-width: 992px) {
width: 100vw;
height: 100vh;
border-radius: 0px;
transition: all 0.2s ease-in-out;
padding: 50px 30px;
}
`;
//...생략
스크롤 바 디자인 변경
투두 목록을 출력하는 아이템 컨테이너에 아래와 같이 스타일을 적용하고, 스크롤바 스타일을 변경했다.
// TodoItems.js
const TodoItemsContainer = styled.div`
margin-top: 20px;
max-height: 75%;
overflow-y: scroll;
overflow-x: hidden;
&::-webkit-scrollbar {
width: 8px;
background-color: var(--main-color);
}
&::-webkit-scrollbar-thumb {
background-color: var(--point-color);
border-radius: 10px;
}
`;
//...생략
React-icons 적용
할 일 목록에 hover 상태에서 나타나는 아이콘은 react-icons 를 적용해보았다. react-icons로 불러온 아이콘은 색상을 직접 변경할 수가 없어서 부득이하게 inline-style 을 사용했는데 다음에는 사용법을 더 숙지해서 보완해야겠다. 아래 코드에서는 Remove 라는 이름의 div에 아이콘을 넣은 뒤 ItemContainer 에서 hover 상태일 때 display 속성을 변경시켰다.
//TodoItem.js
const TodoItemContainer = styled.div`
padding: 20px 0px 20px 10px;
display: flex;
align-items: center;
border-radius: 10px;
&:hover {
background-color: var(--hover-color);
transition: all 0.2s ease-in-out;
cursor: pointer;
${Remove} {
display: initial;
}
}
> .todo__checkbox {
margin-right: 1rem;
min-width: 1.1rem;
min-height: 1.1rem;
}
> span {
font-size: 1.1rem;
width: 100%;
&.checked {
color: gray;
text-decoration: line-through;
}
}
`;
function TodoItem({id}) {
//... 중략
return (
<TodoItemContainer>
<input className={'todo__checkbox'} type="checkbox" checked={isCheck} onChange={onChangeCheckbox} />
<span className={classNameCheck}>{todoItem.contents}</span>
<Remove onClick={deleteTodoHandler}>
<FaTrash style={{fill: '#ff6b6b'}} />
</Remove>
</TodoItemContainer>
);
}
Redux-toolkit 적용
초반 코드에서는 useState 훅을 사용했는데, 리덕스를 학습한 뒤 튜토리얼을 참고해 리덕스 툴킷을 적용해 보았다.
컴포넌트가 4개, 상태가 todoList와 체크 값을 확인하는 isCheck 밖에 없었는데도 입력된 내용으로 상태를 업데이트하고 또 체크 상태에 따라 todoList 를 업데이트 하기 위해서 todo 라는 상태를 추가로 만드는 등 ... 기능이 제대로 동작은 했지만 추후에 새로운 기능을 만들어 확장하려면 많은 수정이 필요할 것 같았다. 확장성과 유지보수성을 고려해 리덕스 툴킷을 적용해 코드를 리팩토링했다.
아래 코드는 대부분 리덕스 툴킷 튜토리얼을 참고하여 작성했다.
리덕스 툴킷의 configureStore 메서드로 store를 생성했다. 일단 여러개의 reducer를 별도의 함수 없이 전달할 수 있어서 편리한 것 같고 reducer 이외에도 여러 설정을 한 번에 해줄 수 있다고 하는데 다른 기능은 아직은 잘 모르겠다.
// store.js
import {configureStore} from '@reduxjs/toolkit';
import todoReducer from '../slice/todoSlice';
export const store = configureStore({
reducer: {
todoList: todoReducer,
},
});
todoSlice는 create, remove, toggleCheck 라는 세가지 액션을 갖는다. todoList에 새 todo가 create 될 때마다 고유한 id를 부여하기 위해 react-uuid 를 사용했다. 이 아이디로 컴포넌트에 key를 부여하고 삭제와 체크값 변경이 가능해진다.
초기 상태값은 로컬스토리지에 저장된 데이터 혹은 이게 없을 때 빈 배열이 된다. 가장 상위 컴포넌트에서 todoList 가 업데이트 될 때마다 로컬 스토리지에 갱신하여 저장하도록 코드를 짰다.
//todoSlice.js
import {createSlice, current} from '@reduxjs/toolkit';
import uuid from 'react-uuid';
// todoList 초기값 : localStorage에 저장된 내용이 없을 경우 빈 배열을 초기값으로 한다.
const initialState = JSON.parse(localStorage.getItem('todoList')) || [];
export const todoSlice = createSlice({
name: 'todoList',
initialState,
reducers: {
create: (state, action) => {
state.push({
id: uuid(),
contents: action.payload,
isCheck: false,
});
},
remove: (state, action) =>
state.filter(todo => {
return todo.id !== action.payload;
}),
toggleCheck: (state, action) =>
state.map(todo => {
return todo.id === action.payload.id ? {...todo, isCheck: action.payload.isCheck} : todo;
}),
},
});
export const {create, remove, toggleCheck} = todoSlice.actions;
export default todoSlice.reducer;
slice 를 작성하면서 state 를 콘솔로 확인하려고 했는데, Proxy 객체가 나와서 당황했다. 이유를 찾아보니 원인은 immer.js 였다. 여기서 콘솔에서 현재 값을 확인하려면 current 메서드를 추가로 불러와 current(state) 와 같이 사용해야한다.
그리고 filter나 map 메서드처럼 새 배열을 리턴하는 메서드를 사용할 때는 기존처럼 return을 해주어야하는데 화살표 함수에서 중괄호를 사용하면서 return을 안하는 실수를 해서 약간 헤맸다..;
할 일 추가
TodoInput 컴포넌트에서 입력된 내용을 토대로 todoList 에 업데이트된다. 여기서 dispatch 함수를 가지고 create 액션을 불러와 내용을 전달하면, 이 내용을 토대로 todoSlice의 reducer 가 새 객체를 생성해 상태를 업데이트하여 화면에 출력한다.
//TodoInput.js
function TodoInput() {
const dispatch = useDispatch();
const [inputValue, setInputValue] = useState('');
const onChangeInput = e => {
setInputValue(e.target.value);
};
const onKeyUpEnter = e => {
if (e.key === 'Enter' && inputValue) {
dispatch(create(inputValue));
setInputValue('');
} else if (!inputValue) {
alert('내용을 입력해 주세요!');
}
};
return (
<>
<InputEl type="text" name="inputValue" onChange={onChangeInput} value={inputValue} onKeyUp={onKeyUpEnter} />
<Label htmlFor="inputValue">Notes...</Label>
</>
);
}
할 일 화면 출력
먼저 최상위 컴포넌트에서 useEffect를 사용해 todoList가 업데이트 될 때마다 로컬스토리지에 해당 내용을 갱신하여 저장하는 함수를 작성했다. useState를 사용했을때는 상태가 끌어올려졌을 때 실행되는 다른 함수가 많이 있었는데 툴킷을 적용하면서 엄청나게 간소화되었다.
//TodoApp.js
function TodoApp() {
// store에서 가져온 todoList state
const todoList = useSelector(state => state.todoList);
// 로컬스토리지 저장 함수
const updateLocalStorage = () => {
localStorage.setItem('todoList', JSON.stringify(todoList));
};
// 초기 마운트 될 때, todoList가 업데이트 될 때 로컬스토리지에 todoList 저장
useEffect(() => {
updateLocalStorage();
}, [todoList]);
return (
<>
<GlobalStyle />
<TodoBackground>
<TodoContainer>
<h1>To Do</h1>
<TodoInput />
<TodoItems />
</TodoContainer>
</TodoBackground>
</>
);
}
TodoItem의 상위 컴포넌트인 TodoItems 에서 map으로 각 todo 데이터를 하나씩 출력한다. 코드는 아래와 같다.
이 컴포넌트에서는 isCheck 라는 값으로 체크 여부를 추적하고 클릭이 일어났을 때 변경된 체크값과 현재 id 를 담아 toggleCheck 액션을 수행한다. delete도 비슷하게 동작한다.
//TodoItem.js
function TodoItem({id}) {
const dispatch = useDispatch();
const todoItem = useSelector(state => state.todoList).find(todo => todo.id === id);
let isCheck = todoItem.isCheck;
let classNameCheck = isCheck === true ? 'checked' : 'unchecked';
function onChangeCheckbox() {
isCheck = !isCheck;
dispatch(toggleCheck({id, isCheck}));
}
const deleteTodoHandler = () => {
dispatch(remove(id));
};
return (
<TodoItemContainer>
<input className={'todo__checkbox'} type="checkbox" checked={isCheck} onChange={onChangeCheckbox} />
<span className={classNameCheck}>{todoItem.contents}</span>
<Remove onClick={deleteTodoHandler}>
<FaTrash style={{fill: '#ff6b6b'}} />
</Remove>
</TodoItemContainer>
);
}
투두 리스트를 만들면서 가장 헷갈렸던 부분이 체크 값을 변경하는 부분이었다. 체크 값을 변경하고 전체 todoList 상태를 업데이트하면 로컬스토리지에 변경된 내용이 저장 되어야 하는데 그 부분이 잘 되지 않았다. 먼저 초반에는 useState로 복잡하게 하다보니 헷갈려서 힘들었다. 또 isCheck 도 state로 등록해서 useEffect로 업데이트되도록 코드를 짰기 때문에 불필요한 렌더링도 많이 일어났다. 툴킷으로 리팩토링하면서는 slice 에서 업데이트 하는 부분에서 return 문을 잘못써서... 시간을 많이 썼다.
🐱배포
깃허브 페이지를 통해 배포를 했다. 배포 후 흰 페이지와 콘솔에 404 에러가 떴었는데, 빌드 후 생성된 index.html 내의 경로를 수정하고 manifest 관련 코드를 주석처리하니 해결되었다.
script 태그의 경로가 절대경로로 표시되어 있었는데 잘 찾지 못해서 static 부터 상대경로로 변경하여 작성했다.
<!-- 주석처리 -->
<!-- <link rel="manifest" href="/react-todo/index.html/manifest.json" /> -->
<!-- 경로 수정 -->
<script defer="defer" src="./static/js/main.7a7a070b.js"></script>
참고링크 : https://stackoverflow.com/questions/61730152/manifest-json-404-not-found
🐱소감
처음 리액트를 배우고, 배운 내용을 적용해보려고 시작한 간단한 토이 프로젝트였다. 처음에는 styled-components 대신에 css를 import 하여 코드를 짰었는데 여러 툴을 배우게 되니 또 적용하고 싶어서 코드를 짜고 뜯어고치고 또 짜고 뜯어고치고를 반복하느라 거의 한 달 정도 걸렸다. 계속 뜯어 고치면서 기초적인 문법과 에러 핸들링에도 쬐금 익숙해진 것 같다.
여기에 추가하고 싶은 여러 기능들이 많이 있는데, 우선 다른 것들을 공부하여 그 내용을 토대로 새롭게 적용해보려고 한다. 끝 ~
'회고' 카테고리의 다른 글
[Project] 🔥모락모락 프로젝트 최종 회고🔥 (2) | 2022.12.07 |
---|---|
[회고] 코드스테이츠 FE 섹션 4 회고 (0) | 2022.10.19 |
[회고] 코드스테이츠 FE 섹션 3 회고 (0) | 2022.09.19 |
[회고] 코드스테이츠 FE 섹션 2 회고 (0) | 2022.08.18 |
[회고] 코드스테이츠 FE 섹션 1 회고 (0) | 2022.07.20 |