23/12/28 typescript , thunk 사용한 todolist

2023. 12. 29. 08:49카테고리 없음

//전반적으로 어제 작성한 react-query버전과 전체적인 틀은 똑같다.

<Nav.tsx>  

import React, { useState } from "react";
import styled from "styled-components";
import { useDispatch } from "react-redux";    //thunk도 유즈 디스패치 사용하니까 임포트
import { __addTodo } from "../redux/mo/modules/todoSlice";   //thunk사용할때 필요한 엑스트라 리듀서인 __addTodo
import { nanoid } from "nanoid";
import { AppDispatch } from "../redux/mo/store/configstore";   //앱 디스패치라고 해당 타입을 해당 폴더에서 만든건데
                                                                                                //일단 임포트
export default function Nav() {
  type T = { id: string; title: string; content: string; isDone: boolean };
  const dispatch = useDispatch<AppDispatch>();                                  //그 임포트한것이 바로 유즈 디스패치의타입
  const JSON_SERVER_BASE_URL = "http://localhost:4000/todos";      //제네릭안에 넣어준다.
  const initialForm = {
    id: "",
    title: "",
    content: "",
    isDone: false,
  };
  const [formState, setFormState] = useState<T>(initialForm);          

  const OnchangeHandler = (e: React.ChangeEvent<HTMLInputElement>) => {
    const { name, value } = e.target;
    setFormState((prev: T) => ({ ...prev, [name]: value }));
  };
  const OnSubmitHandler = async (e: React.FormEvent<HTMLFormElement>) => {
    const newTodo = {
      id: nanoid(),
      title: formState.title,
      content: formState.content,
      isDone: false,
    };
    e.preventDefault();
    setFormState(initialForm);
    dispatch(__addTodo(newTodo));   // 디스패치 안에 리듀서 넣어주고 인자넣어주고
  };

  // const getTodos = async () => {
  //   const { data } = await axios.get(JSON_SERVER_BASE_URL);
  //   const { id, title, content, isDone } = data;
  //   console.log("첫 데이터", data);
  //   dispatch(setTodos(data));
  // };

  return (
    <Header onSubmit={OnSubmitHandler}>
      <span>제목</span>{" "}
      <input
        name="title"
        value={formState.title}
        onChange={OnchangeHandler}
      ></input>
      <span>내용</span>{" "}
      <input
        name="content"
        value={formState.content}
        onChange={OnchangeHandler}
      ></input>
      <br />
      <button disabled={!formState.title || !formState.content}> //인풋에 정보없으면 작불가
        추가하기
      </button>
    </Header>
  );
}
const Header = styled.form`
  background-color: lightcyan;
  height: 150px;
  display: flex;
  justify-content: center;
  align-items: center;
  flex-direction: column;
`;

 

<TodosList.tsx> 

import React, { useEffect } from "react";
import { useDispatch, useSelector } from "react-redux";  //thunk도 RTK이기때문에 스테이트 변경을 위한 
import styled from "styled-components";                   //디스패치와 스테이트를 읽어오긴위한 유즈샐렉터
import {
  __deleteTodo,
  __editTodo,                                //엑스트라 리듀서들
  __getTodos,
} from "../redux/mo/modules/todoSlice";
import { AppDispatch } from "../redux/mo/store/configstore";   //역시나 유즈디스패치의 타입을 위해 임포트

export default function TodosList({ listType }: { listType: boolean }) {
  type T = { id: string; title: string; content: string; isDone: boolean };
  const { todos, isLoading } = useSelector((state: any) => state.todos);   //스테이트를 애니라고 했다 귀찮아서 ㅋ
  const dispatch = useDispatch<AppDispatch>();                                        //나중에 깨달은 건데 스테이트는
  const JSON_SERVER_BASE_URL = "http://localhost:4000/todos";      //각각의 리듀서를 품고있는 객체형인거 같다.
  // const getTodos = async () => {
  //   const { data } = await axios.get(JSON_SERVER_BASE_URL);
  //   const { id, title, content, isDone } = data;
  //   console.log("첫 데이터", data);
  //   dispatch(setTodos(data));
  // };
  useEffect(() => {
    dispatch(__getTodos());       //리액트 쿼리는 유즈이펙트 필요없지만 첫랜더링시 정보를 가져오기위한
  }, [dispatch]);                          //사용 안에는 정보를 가져올 함수

  const DeleteHandler = (id: string) => {
    const confirmedData = window.confirm("정말로 삭제할꺼에여?");
    if (confirmedData) {
      dispatch(__deleteTodo(id));         //삭제로직 인자 id 전달
    }
  };
  const UpdateHandler = (id: string, isDone: boolean) => {
    dispatch(__editTodo({ id, isDone }));                         //수정로직 인자 isDone , id 전달
    // updateTodos1(id, isDone);
    // getTodos();
  };

  const notYetTodos = todos.filter((todo: T) => {
    return todo.isDone === false;
  });
  const completedTodos = todos.filter((todo: T) => {
    return todo.isDone === true;
  });

  const changedTodos = listType ? completedTodos : notYetTodos;

  if (isLoading) {
    return <>로딩중....</>;
  }

  return (
    <>
      {" "}
      <div style={{ backgroundColor: "black", color: "white" }}>
        {listType ? "완료된투두" : "해야할투두"}
      </div>
      <Section1>
        <>
          {changedTodos?.map((todo: T) => {
            return (
              <ListWrapper key={todo.id}>
                <div>아이디 : {todo.id}</div>
                <div>제목 : {todo.title}</div>
                <div>내용 : {todo.content}</div>
                <div>상태 : {todo.isDone ? "완료" : "미완료"}</div>
                <button onClick={() => UpdateHandler(todo.id, todo.isDone)}>
                  {listType ? "취소" : "완료"}
                </button>
                <button onClick={() => DeleteHandler(todo.id)}>삭제하기</button>
              </ListWrapper>
            );
          })}
        </>
      </Section1>
    </>
  );
}
const Section1 = styled.section`
  height: 200px;
  display: flex;
  justify-content: flex-start;
  align-items: center;
  gap: 10px;
  background-color: lightblue;
`;
const ListWrapper = styled.div`
  height: 180px;
  width: 180px;
  background-color: lightgoldenrodyellow;
`;

 

<configureStore.ts>

import {
  ThunkDispatch,
  combineReducers,
  configureStore,
} from "@reduxjs/toolkit";
import todos from "../modules/todoSlice";
import { useDispatch } from "react-redux";

const rootReducer = combineReducers({});                    //이제보니 이건 왜 만들었지? 지워야겠네
const store = configureStore({
  reducer: { todos },                             //여기까지는 자바스크립트랑 같다.
});

export default store;
export type AppDispatch = typeof store.dispatch;                //유즈디스패치를 사용할떄 타입지정요청이 뜨는데
export type RootState = ReturnType<typeof rootReducer>;              //요래요래 하면 된다고 해서 해본
export const useAppDispatch: () => AppDispatch = useDispatch;             //스토어라는 객체안에 디스패치가 있나봄
// export type AppThunkDispatch = ThunkDispatch<ReducerType, any, Action<string>>;

 

<todoSlice.ts> 

import { createAsyncThunk, createSlice } from "@reduxjs/toolkit";
import axios from "axios";

const getTodos = async () => {                                                                       //계속사용할 비동기함수라서 뺴놓음
  const { data } = await axios.get<T[]>(JSON_SERVER_BASE_URL);        
  //   const { id, title, content, isDone } = data;
  console.log("첫 데이터", data);
  return data;
};

type T = { id: string; title: string; content: string; isDone: boolean };
const JSON_SERVER_BASE_URL = "http://localhost:4000/todos";
type U = {
  todos: T[];
  isLoading: boolean;                              //타입지정
  isError: boolean;
  error: any;
};

const initialState: U = {
  todos: [],
  isLoading: true,
  isError: false,
  error: null,
};

export const __getTodos = createAsyncThunk(       //thunk의 사용 방식
  "getTodos",                                                              //네이밍
  async (_, thunkAPI: any) => {                  //thunkAPI의 타입을 내가 어떻게 아냐구용~
    try {                                                                 // 어디서 알아야되는건지알려줘유
      const todos = await getTodos();
      return todos;                                   //얘가 액션 함수 였나? 엑스트라 리듀서로 보내주기위한 값 리턴
    } catch (err) {
      return thunkAPI.rejectWithValue(err);        //요게 에러일때 에러값을 리턴해 주는 로직
    }                                                                   //thunk 만든사람이 이렇게 만들었으니 군말없이 쓰도록
  }
);
export const __editTodo = createAsyncThunk(
  "editTodo",
  async ({ id, isDone }: { id: string; isDone: boolean }, thunkAPI) => {   //첫번째 인자가 액션함수 쓴곳에서받은 인자
    try {                                                                                        //두번쨰가 thunkAPI 임 근데 여기 타입안넣는데 오류 없네?
      const { data } = await axios.patch(`${JSON_SERVER_BASE_URL}/${id}`, {
        isDone: !isDone,                          //인자로 받은 아이디에 맞는 애 수정로직
      }); 

      const todos = await getTodos();         //자동 스테이트 변경해주는게 아니므로 한번데 data read 함수 호출
      return todos;
    } catch (err) {
      return thunkAPI.rejectWithValue(err);
    }
  }
);

export const __deleteTodo = createAsyncThunk(
  "deleteTodo",
  async (id: string, thunkAPI) => {
    try {
      await axios.delete(`${JSON_SERVER_BASE_URL}/${id}`);  //삭제로직 이하동문
      const todos = await getTodos();
      return todos;
    } catch (err) {
      return thunkAPI.rejectWithValue(err);
    }
  }
);

export const __addTodo = createAsyncThunk(
  "addTodo",
  async (newTodo: T, thunkAPI: any) => {   //인자로 뉴투두 받아옴 쟤가 payload임
    try {
      await axios.post(JSON_SERVER_BASE_URL, newTodo);   //새 객체 추가로직
      const todos = await getTodos();
      return todos;
    } catch (err) {
      return thunkAPI.rejectWithValue(err);
    }
  }
);
const todoSlice = createSlice({
  name: "todos",
  initialState,                              //이제 투두 슬라이스 만드는 데 thunk는 reducers 키값에 할당하는게 아니라
  reducers: {
    // setTodos: (state, action) => {
    //   return action.payload;
    // },
    //
    // addTodos: (state, action) => {
    //   const newtodo = action.payload;
    //   return [newtodo, ...state];
    // },
    // deleteTodo: (state, action) => {
    //   const id = action.payload;
    //   const fiteredTodos = state.filter((todo: T) => {
    //     return todo.id !== id;
    //   });
    //   return fiteredTodos;
    // },
    // updateTodo: (state, action) => {
    //   const id = action.payload;
    //   const ChangedTodos = state.map((todo: T) => {
    //     if (id === todo.id) {
    //       return { ...todo, isDone: !todo.isDone };
    //     } else {
    //       return todo;
    //     }
    //   });
    //   return ChangedTodos;
    // },
  },

  extraReducers: (builder) => {                                                 //엑스트라 리듀서에 할당
    builder.addCase(__addTodo.pending, (state) => {                              //pending은 데이트 로딩중
      state.isLoading = true;
    });
    builder.addCase(__addTodo.fulfilled, (state, action) => {                //fulfilled는 데이터를 제대로 받았을떄
      state.isLoading = false;
      state.todos = action.payload;
      state.isError = false;
      state.error = null;
    });
    builder.addCase(__addTodo.rejected, (state, action) => {             //rejected는 데이터 못받았을떄 쯕 에러일때
      state.isLoading = false;                                                       //로 각각구분
      state.isError = true;
      state.error = action.payload;
    });
    builder.addCase(__getTodos.pending, (state) => {                       //액션함수 위에서 4개쓰므로 하나당 또 3개
      state.isLoading = true;                                                             //를 만들어야 하기때문에 builder.addCase가 12개
    });
    builder.addCase(__getTodos.fulfilled, (state, action) => {
      state.isLoading = false;
      state.todos = action.payload;
      state.isError = false;
      state.error = null;
    });
    builder.addCase(__getTodos.rejected, (state, action) => {
      state.isLoading = false;
      state.isError = true;
      state.error = action.payload;
    });
    builder.addCase(__deleteTodo.pending, (state) => {                  //pending때는 딱히 데이터가 없으므로
      state.isLoading = true;                                                            //걍  isLoading상태가 트루
    });
    builder.addCase(__deleteTodo.fulfilled, (state, action) => {    //데이터를 받았을때가 중요한데
      state.isLoading = false
      state.todos = action.payload;                                   //액션함수 4개다 그냥 페이로드가 state.todos이므로
      state.isError = false;                                          //다 갈아껴주고 끝
      state.error = null;
    });
    builder.addCase(__deleteTodo.rejected, (state, action) => {      //rejected 됐을때는 페이로드로 에러 내용을
      state.isLoading = false;                                                        //주므로 state.error에 할당
      state.isError = true;
      state.error = action.payload;
    });
    builder.addCase(__editTodo.pending, (state) => {
      state.isLoading = true;
    });
    builder.addCase(__editTodo.fulfilled, (state, action) => {   //fulfilled 되면 isLoading은 끝났으므로 false
      state.isLoading = false;
      state.todos = action.payload;
      state.isError = false;                                      //에러있니? no = false
      state.error = null;                                //에러내용 당연히 없고
    });
    builder.addCase(__editTodo.rejected, (state, action) => {
      state.isLoading = false;
      state.isError = true;
      state.error = action.payload;
    });
  },
});

export default todoSlice.reducer;
// export const { /*addTodos, deleteTodo, updateTodo,*/ setTodos } =
//   todoSlice.actions;

 

<index.ts>

import React from "react";
import ReactDOM from "react-dom/client";
import "./index.css";
import App from "./App";
import reportWebVitals from "./reportWebVitals";
import { Provider } from "react-redux";
import store from "./redux/mo/store/configstore";

const root = ReactDOM.createRoot(
  document.getElementById("root") as HTMLElement
);
root.render(
  <Provider store={store}>  // 스토어 리듀서 툴킷 썽크 등 쓸려면 요거 셋팅 해줘야한다!!
    <App />
  </Provider>
);

// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();

 

<index.ts> 이건 어제 설명 못한 react-query때 의 인덱스.ts

import React from "react";
import ReactDOM from "react-dom/client";
import "./index.css";
import App from "./App";
import reportWebVitals from "./reportWebVitals";
// import { Provider } from "react-redux";
// import store from "./redux/mo/store/configstore";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";  //두개 임포트

const queryclient = new QueryClient();    //요래요래 해줘야하고
const root = ReactDOM.createRoot(
  document.getElementById("root") as HTMLElement
);
root.render(
  <QueryClientProvider client={queryclient}>      //요래요래 묶어줘야 한다 원리는 모름 ㅋㅋ
    {/* <Provider store={store}> */}
    <App />
    {/* </Provider> */}
  </QueryClientProvider>
);

// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();

 

//thunk가 확실히 react-query 보다 귀찮은거 같다. ㅋ

//typescript는 확실히 더 연습해서 다음에 next.js랑 같이 프로젝트에 써먹어야 겠다운