본문 바로가기

Language & Framework/React.js

React로 LocalStorage에 저장되는 TodoList 만들기 ( react-redux, persist-redux )

이런 거 만들어야지~

설명은 거의 없습니다.

단순한 코드라 그냥 보면서 만들고 싶은 분한테는 도움이 되지 않을까 합니다.

 

준비물(라이브러리)

- 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를 변경해서 자동으로 드롭 박스가 닫히도록 변경해야겠다.

아무튼 이런 식으로 하면 끝이다. 단순한 투두 리스트 만드는 건 바닐라 자바스크립트가 훨씬 편한 것 같다.