설명은 거의 없습니다.
단순한 코드라 그냥 보면서 만들고 싶은 분한테는 도움이 되지 않을까 합니다.
준비물(라이브러리)
- redux, react-redux, persist-redux, uuid
사실 다 없어도 된다. 노가다 한다면.
컴포넌트 구성
- Head부터 input까지 전체 요소들을 담고 박스 스타일을 담당할 TodoContainer 컴포넌트.
- TodoHeader 컴포넌트.
- 중간에 들어갈 TodoItem 컴포넌트.
- TodoInput 컴포넌트.
reducer에서 관리할 todoItem의 상태
- 각 todoItem의 id
- 해당 todoItem이 완료되었는지 확인해줄 done (boolean)
- todoItem의 값 text
- DONE을 선택하면 done==true인 todoItem만 보여줄 수 있도록 visible (boolean)
1. todoReducer를 먼저 보고 갑시다.
const todoReducer = function (state = [], action) {
switch (action.type) {
// 새로운 todo item을 만들 친구
case "CREATE_TODO":
return state.concat(action.payload);
// 체크박스를 클릭할 경우 done이 (!기존값)이 된다.
case "CHECK":
return state.map((todo) =>
todo.id === action.payload.id ? { ...todo, done: !todo.done } : todo
);
// delete한 요소만 제외하고 배열 반환
case "DELETE":
return state.filter((todo) => todo.id !== action.payload.id);
// done이 false고 visible이 true일 경우 보이지 않게(!visible) 만든다.
// 만약 이것의 조건으로 !todo.done만 준다면 카테고리에서 DONE만 클릭해도 계속해서 요소들이 사라졌다가
// 생겨났다가를 반복하게 된다. (why? done이 아닌 요소들이 계속해서 !todo.visible하고 있는 것이므로 )
case "SHOW_DONE":
return state.map((todo) =>
!todo.done && todo.visible ? { ...todo, visible: !todo.visible } : todo
);
// 다 보여주는 거라 그냥 !visible일 경우 죄다 visible로 바꿔주면 된다. 간단.
case "SHOW_ALL":
return state.map((todo) =>
!todo.visible ? { ...todo, visible: !todo.visible } : todo
);
default:
return state;
}
};
실제로 만들 때는 reducer를 먼저 완성 형태로 작성하게 되지는 않지만..
어차피 단계별로 하나하나 꼼꼼히 설명하는 글은 아니기 때문에 reducer를 먼저 보고 가는 것이 이해해 도움이 될 것 같다.
참고로 "CREATE_TODO" 부분에서 나는 concat을 사용했는데 이것이 성능에 별로 좋지 않다고 한다.
https://jintelli.tistory.com/30
자세한 게 궁금하다면 위 블로그 글을 참고하면 될 것 같다.
아무튼 결론은 push와 spead 문법을 사용하는 게 좋다고 하니까 [...state].push(action.payload) 이런 식으로 만들면 되겠죠?
2. container 컴포먼트 만들기.
// app.js나 기타등등..
function 어쩌고 저쩌고 () {
return (
<TodoListContainer>
<TodoHeader />
<TodoItem />
<TodoInput />
</TodoListContainer>
)
}
완성하면 이런 형태가 될 텐데 여기에는 함정이 있다.
import로 가져온 컴포넌트 사이에 다른 컴포넌트를 삽입해도 화면에 표시되지 않는다.
function TodoListContainer({ children }) {
return <section>{children}</section>;
}
export default TodoListContainer;
그래서 이런 식으로 { 이 안에 다른 컴포넌트들을 넣을 예정입니다 ~~ } 하고 만들어줘야 한다.
이제 저 section에 투두 박스에 대한 css 스타일링만 해주면 Container는 끝.
3. TodoHeader 만들기
return (
<>
<div>
<span onClick={showDropBox}>{list}</span>
</div>
{dropBox && (
<ul>
<li onClick={showAllTodo}>TODO {todos.length}</li>
<li onClick={showOnlyDone}>DONE {done.length}</li>
</ul>
)}
</>
);
const dispatch = useDispatch();
// TODO와 DONE 중 어떤 텍스트를 띄울 지 관리
const [list, setList] = useState("TODO ▾");
// dropBox를 열고 닫는 것을 관리
const [dropBox, setDropBox] = useState(false);
const todos = useSelector((store) => store.todoReducer);
// done의 개수를 구하기 위해 전체 목록에서 완료된 요소만 추려서 done에 반환한다.
const done = todos.filter((todo) => todo.done == true);
const showDropBox = () => {
dropBox ? setDropBox(false) : setDropBox(true);
};
const showAllTodo = () => {
setList("TODO ▾");
dispatch({ type: "SHOW_ALL" });
};
const showOnlyDone = () => {
setList("DONE ▾");
dispatch({ type: "SHOW_DONE" });
};
단순히 TODO, DONE 버튼 두 개 띄워놓고 누르면 없어보이니까 dropbox를 이용해서 TODO DONE을 선택할 수 있게 만들었다.
사실 위의 이유는 뻥이고 모멘텀 클론 코딩하면서 만든 투두 리스트라서 모멘텀하고 똑같이 만들었다.
DONE, TODO 옆에는 각 항목에 포함된 요소의 개수를 표시해준다.
4. TodoInput 만들기
input에서 값을 넣어줘야 todoItem이 나오니까요
return (
<form onSubmit={todoSubmitHandler}>
<input
placeholder={"Enter a new Todo here"}
onChange={todoInputHandler}
ref={todoInput}
/>
</form>
);
const [input, setInput] = useState("");
const newTodo = { id: uuidv4(), text: input, done: false, visible: true };
// 우리가 reducer에 넣어줄 값들을 가진 객체 생성
const todoInput = useRef(null);
// submit한 이후에 input을 비워주기 위해 만들었습니다.
const dispatch = useDispatch();
const todoInputHandler = (e) => {
e.preventDefault();
setInput(e.target.value);
};
const todoSubmitHandler = (e) => {
e.preventDefault();
// 화면 새로고침 막아주기
if (!input) {
return false;
}
// input값이 없다면 return
dispatch({
type: "CREATE_TODO",
payload: newTodo,
});
setInput("");
todoInput.current.value = "";
};
엄청 간단하고 단순하군요. redux-persist를 사용하지 않을 거라면 만들면서 localStorage에 newTodo를 넣어주는 작업도 같이 해줘야겠네요.
5. todoItem 컴포넌트 만들기
return (
<div
style={props.visible ? { display: "flex" } : { display: "none" }}
>
<input
onChange={onChangeHandler}
defaultChecked={props.done}
/>
<p
style={
// 체크되어 있다면 글씨 색을 바꾸고 줄을 그어주세요
props.done
? {
textDecoration: "line-through",
color: "rgba(255,255,255,0.3)",
}
: { textDecoration: "none", color: "rgb(255,255,255)" }
}
>
{props.text}
</p>
<삭제 모양 아이콘 onClick={deleteHandler} />
</div>
);
const dispatch = useDispatch();
const onChangeHandler = () => {
dispatch({
type: "CHECK",
payload: {
id: props.id,
text: props.text,
done: props.done,
},
});
};
const deleteHandler = () => {
dispatch({
type: "DELETE",
payload: {
id: props.id,
},
});
};
제가 인라인으로 작성한 이유는 styled-components를 사용했기 때문입니다. (가독성을 위해 여기에는 다 태그로 변경해서 올림)
원래는 클래스를 탈부착해주는 게 제일 좋습니다.
6. 다 조립해주면 완성 ~
const todoState = useSelector((state) => state.todoReducer);
<TodoListContainer>
<TodoHeader />
<TodoItemsList>
{todoState.map((todo) => {
return (
<TodoItem
id={todo.id}
key={todo.id}
text={todo.text}
done={todo.done}
visible={todo.visible}
/>
);
})}
</TodoItemsList>
<TodoInput />
</TodoListContainer>
완성 아닙니다.
localStorage에 저장해줘야죠.
이대로 방치하면 새로고침 한 번 할때마다 오늘의 할 일이 새로 갱신되는 메멘토 주인공 같은 인물이 되어버립니다.
persist-redux의 기본 사용법 같은 경우는 https://7357.tistory.com/53 이 글을 참고해 주세요.
// reducer.js
import { persistReducer } from "redux-persist";
import storage from "redux-persist/lib/storage";
const persistConfig = {
key: "LOCAL_ITEMS",
// 본인 마음대로 설정하면 된다.
storage,
whitelist: ["todoReducer"],
// todoReducer에 대한 값만 저장하고 싶다면 이렇게 작성하면 됨.
};
export default persistReducer(persistConfig, todoReducer);
// 다른 reducer가 있을 경우 rootReducer에 다 집어넣고 persistReducer의 두 번째 인자로
// rootReducer를 보내주면 된다.
// index.js
import { persistStore } from "redux-persist";
import { PersistGate } from "redux-persist/integration/react";
import persistReducer from "./redux/reducer";
import { Provider } from "react-redux";
import { createStore } from "redux";
import { HashRouter } from "react-router-dom";
const store = createStore(persistReducer);
const persistor = persistStore(store);
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(
<React.StrictMode>
<HashRouter>
<Provider store={store}>
<PersistGate loading={null} persistor={persistor}>
<App />
</PersistGate>
</Provider>
</HashRouter>
</React.StrictMode>
);
이렇게 해주면 상태가 변경될 때마다 자동으로 localStorage에 저장된다.
결과물
잘 작동되는데 TODO와 DONE을 오갈 때 일일히 저 버튼을 다시 눌러줘야 하는 게 흠이다.
버튼을 누를 때 state를 변경해서 자동으로 드롭 박스가 닫히도록 변경해야겠다.
아무튼 이런 식으로 하면 끝이다. 단순한 투두 리스트 만드는 건 바닐라 자바스크립트가 훨씬 편한 것 같다.
'Language & Framework > React.js' 카테고리의 다른 글
intersection Observer로 React 무한 스크롤 구현하기 (0) | 2022.05.08 |
---|---|
리액트 로딩 시 스피너 구현하기 (react-loader-spinner) (0) | 2022.05.07 |
React)유저가 전송할 수 있는 메일 폼 만들기(EmailJS) (0) | 2022.04.29 |
redux를 손쉽게 LocalStorage에 저장하자, redux-persist (0) | 2022.04.27 |
React에서 날씨 표시하기 (feat. geolocation API를 hook으로 만들기) (0) | 2022.04.21 |