TS+REACT+TailwindCSS로 Todo 구현해보기(2)

hatban·2023년 4월 16일
0

TODO 추가 EnterEvent

예전에는 keyPressEnter가 있었는데 deprecated됐다고 한다..그래서 keydown 이벤트를 추가하는 방법을 사용하면 된다고 한다. 그리고 keyCode도 지금은 안된다!

  const handleKeypress = (e: React.KeyboardEvent<HTMLInputElement>) =>{
    let key = e.key || e.keyCode;

    if (key === 'Enter' || key === 13) {
      addTodoList();
    }
  }



TODO 삭제하는 Event구현

갑자기 toolkit을 사용해보고 싶어서 리덕스구조를 다시 만들었다.

toolkit 사용 전 파일구조

│  App.tsx
│  index.css
│  index.tsx
│
├─components
│      TodoInput.tsx
│      TodoList.tsx
│      TodoStateBtns.tsx
│
└─Redux
    │  types.ts
    │
    ├─Actions
    │      todolist_action.ts
    │
    └─Reducers
            index.ts
            todoReducer.ts

toolkit 사용 후 파일구조

│  App.tsx
│  index.css
│  index.tsx
│
├─components
│      TodoInput.tsx
│      TodoList.tsx
│      TodoStateBtns.tsx
│
└─store
        index.tsx
        todoSlice.tsx

우선 Action과 reducer, type을 위한 폴더를 따로 구성하지 않고 하나의 Slice파일로 만들어서 복잡하지 않은게 최대 장점이다!


// todoSlice.tsx
import { createSlice } from "@reduxjs/toolkit";



export type TodoType = {
    todoID: string;
    todoContent: string;
    checked: boolean;
}

type InitialStateType = TodoType[];

const initialState : InitialStateType = [];

const todoSlice = createSlice({
    name : "todo",
    initialState,
    reducers : {
        addTodo : (state, action) =>{
            return [...state, action.payload];
        },
        removeTodo : (state,action) =>{
            return state?.filter((todo) => todo.todoID !== action.payload);
        },
        setTodo : (state, action) =>{
            const newState = [...state, ...action.payload]
            return newState;
        }
    }
})

export default todoSlice;
export const {addTodo, removeTodo, setTodo} = todoSlice.actions;

createSlice를 통해서 이름과 초기상태, 리듀서를 한번에 정의할 수 있고 리듀서 선언도 간단해졌다.


  const deleteList = ()=>{
    let localStorageArr : Array<TodoType> = JSON.parse(localStorage.getItem('todos')!);
    let idx = localStorageArr.filter((todo) => todo.todoID !== todoID);
    dispatch(removeTodo(todoID));
    localStorage.setItem('todos', JSON.stringify(idx));
  }
  • 삭제 버튼 클릭시 localStorage에서 값들을 가져온다음, todoID가 같지 않은 것(삭제하지 않을 값들)을 idx라는 배열로 담은다음 로컬스토리지를 다시 set하고, dispatch로 상태를 관리한다.



TODO Check Event구현

  const handleCheckList = () =>{
     let localStorageArr : Array<TodoType> = JSON.parse(localStorage.getItem('todos')!);
     localStorageArr.forEach((list)=>{
      if(list.todoID === todoID) {
        let idx = !list.checked;
        list.checked = idx;
        return;
      }
     })
     dispatch(setTodo(localStorageArr));
     localStorage.setItem('todos', JSON.stringify(localStorageArr));
  }
  • forEach문을 통해서 ID값이 일치하면 직접 배열 요소의 checked 속성을 변경시킨다음 dispatch로 상태 관리를 해주고, localStorage에 다시 저장한다.



Doing/Done 버튼 구현

기본 상태는 Doing으로 지금 하는 중인 투두를 나타내고, Done을 클릭하면 이미 완료한 투두를 보여준다

// App.tsx
  const [btnState, setBtnState] = useState<string>('Doing');
 

function onSetList(btn : string){
    let localStorageArr : Array<TodoType> = JSON.parse(localStorage.getItem('todos')!); //1. 로컬에서 가져오기
    if ( localStorageArr!= null && btn === "Doing") {
      let listArr = localStorageArr.filter((list)=> list.checked === false);
      dispatch(setTodo(listArr)); 
    }
    else if(localStorageArr!= null && btn === "Done"){
      let listArr = localStorageArr.filter((list)=> list.checked !== false);
      dispatch(setTodo(listArr)); 
    }
  }

  useEffect(()=>{
    onSetList(btnState);
  },[btnState]);
  • App.tsx에 btnState라는 상태값이 있고 기본적으로 Doing이며, 값에 따라서 다른 listArr을 만들어 리덕스에 상태를 관리해준다.

  • btnState상태값은 TodoList와 TodoStateBtns라는 컴포넌트들에 props로 전달된다


//TodoStateBtns.tsx
  const doingBtnRef = useRef<HTMLButtonElement>(null);
  const doneBtnRef = useRef<HTMLButtonElement>(null);

  useEffect(()=>{
    if(btnState === "Doing"){
      doingBtnRef.current?.classList.add('bg-yellow-200');
      doneBtnRef.current?.classList.remove('bg-yellow-200');
    }else{
      doneBtnRef.current?.classList.add('bg-yellow-200');
      doingBtnRef.current?.classList.remove('bg-yellow-200');
    }
  },[btnState])


  const handleBtnClick = (e : React.MouseEvent<HTMLElement>)=>{
    const {name}= e.target as HTMLButtonElement;
    if(name === 'doingBtn'){
      doingBtnRef.current?.classList.add('bg-yellow-200');
      doneBtnRef.current?.classList.remove('bg-yellow-200');
      setBtnState('Doing');
    }else{
      doneBtnRef.current?.classList.add('bg-yellow-200');
      doingBtnRef.current?.classList.remove('bg-yellow-200');
      setBtnState('Done');
    }
  }
  • 버튼 상태값은 TodoList 컴포넌트에 의해서도 바뀌기 때문에 useEffect로도 값을 주시하고 있다가 그에 따른 버튼 클래스를 이용해 tailwindCss로 처리하고, 버튼을 클릭하는 이벤트 리스너를 붙여서 상태를 set한다.
// TodoList.tsx

  const handleCheckList = () =>{
     let localStorageArr : Array<TodoType> = JSON.parse(localStorage.getItem('todos')!);
     localStorageArr.forEach((list)=>{
      if(list.todoID === todoID) {
        let idx = !list.checked;
        list.checked = idx;
        if(idx === true){ //true로 바뀌면 Done으로 바꿔주기
          setBtnState("Done");
        }else{
          setBtnState("Doing");
        }
        return;
      }
     })
     dispatch(setTodo(localStorageArr));
     localStorage.setItem('todos', JSON.stringify(localStorageArr));
  }
  • TodoList 컴포넌트도 마찬가지로 check하면 setBtnState를 통해서 Btn의 상태를 관리한다.

후기

간단한 todolist를 React+Redux(toolkit)+Typescript+TailwindCSS로 만들어 봤다. 확실히 css파일이 따로 없으니까 깔끔하고 부트스트랩보다 커스텀이 자유로워서 좋았다. 자주 사용할 듯 싶다(특히 그리드나 레이아웃 잡을때 편할 것 같다).
그리고 toolkit도 중간에 갑자기 사용하게 되었는데 action이나 reducer같은 폴더를 따로 안만들고 createSlice로 모든 설정이 가능한게 편리해서 앞으로는 꼭 toolkit을 사용할 것 같다.
Typescript는 사용하는데 익숙치가 않아서 아직 나에게는 에러를 많이 발생시키는 아이..지만 이제 더 복잡한 로직을 짜게된다면 분명 이점이 있을것이라 생각하고 더 공부해야겠다.

0개의 댓글