개인프로젝트 - todoit 3회차

Seuling·2022년 7월 24일
1

개인프로젝트

목록 보기
3/4
post-thumbnail

220724

  • AxiosTodoService 에서 api 구현
  • delete, isDone 기능 구현
    • API 호출과 렌더링 작업 분리
  • dayjs 활용 header 기능 구현
  • <Text/> <IconBtn> 컴포넌트 재사용 가능하도록 분리

AxiosTodoService 에서 api 구현

api
 ┣ axios
 ┃ ┗ AxiosTodoService.ts
 ┣ types
 ┃ ┗ todoItem.d.ts
 ┣ TodoService.ts
 ┗ index.ts

TodoService Interface 구현

//TodoService 
import { TodoItemType } from "./types/todoItem";

export default interface TodoService {
  getTodoItems(): Promise<Array<TodoItemType>>;
  changeTodoItem(item: TodoItemType): Promise<void>;
  deleteTodoItem(id: number): Promise<void>;
  changeDoneTodoItem(id: number, isDone: boolean): Promise<void>;
  createTodoItem(item: TodoItemType): Promise<TodoItemType>;
}

AxiosTodoService

import axios from "axios";

import TodoService from "../TodoService";
import { TodoItemType } from "../types/todoItem";
import dayjs from "dayjs";

const todoAxios = axios.create({
  baseURL: "http://localhost:8000",
});

export class AxiosTodoService implements TodoService {
  async createTodoItem(item: TodoItemType): Promise<TodoItemType> {
    item.id = dayjs().valueOf();
    const response = await todoAxios.post(`/todos`, item);
    return response.data;
  }
  async deleteTodoItem(id: number): Promise<void> {
    await todoAxios.delete(`/todos/${id}`);
  }
  async changeDoneTodoItem(id: number, isDone: boolean): Promise<void> {
    await todoAxios.patch(`/todos/${id}`, { isDone });
  }
  async changeTodoItem(item: TodoItemType): Promise<void> {
    await todoAxios.put(`/todos/${item.id}`, item);
  }
  async getTodoItems(): Promise<Array<TodoItemType>> {
    const response = await todoAxios.get("/todos");
    return response.data;
  }
}

index.ts

import { AxiosTodoService } from "./axios/AxiosTodoService";
import TodoService from "./TodoService";

export const todoService: TodoService = new AxiosTodoService();

todolist view를 todo 만큼 해보기

//TodoListView
const TodoListView = (props: Props) => {
  return (
    <TodoListContainer>
      {/* items의 값이 있으면 ? * 배열의 갯수만큼 보여주기*/}
      {props.items.map((item) => (
        <TodoItem
          item={item}
          key={item.id}
        />
      ))}
    </TodoListContainer>
  );
};

여기서 고민해봐야하는 지점은?
어떤 item을 받아야할까 ?
원래의 database.json 형식은 이랬다!

"todos": [
    {
      "id": 1,
      "hi": "hihihi",
      "number": "9447"
    }
  ]

하지만 기능구현을 생각하며 아래와 같이 변경하였다.

 "todos": [
    {
      "id": 1,
      "todoContent": "hihi",
      "isDone": false,
      "priority": 1,
      "dateTimes": {
        "createdDateTime": "2022-07-23T17:40:00+09:00",
        "doneDateTime": "2022-07-23T18:45:00+09:00"
      },
      "estimatedMins": 30
    },
   ]

그럼에 따라 또 고민해봤던 지점은, 시간은 어떤 포맷으로 받지??
ISO 에 맞춰 받기로 하였다.

ISO
가장 기본적인 형식(날짜와 시간)은 아래와 같습니다.
2017-03-16T17:40:00+09:00
• 날짜 : 년-월-일의 형태로 나와있습니다.
• T : 날짜 뒤에 시간이 오는것을 표시해주는 문자입니다.
• 시간 : 시:분:초의 형태로 나와있으며 프로그래밍 언어에 따라서 초 뒤에 소수점 형태로 milliseconds가 표시되기도 합니다.
• Timezone Offset : 시간 뒤에 ±시간:분의 형태로 나와있으며 UTC기준 시로부터 얼마만큼 차이가 있는지를 나타냅니다. 현재 위의 예시는 한국시간을 나타내며 UTC기준 시로부터 9시간 +된 시간임을 나타냅니다
• Z or +00:00 : UTC기준시를 나타내는 표시이며 “+00:00”으로 나타내기도 합니다.

Epoch??
유닉스 시간(영어: Unix time)은 시각을 나타내는 방식이다. POSIX 시간이나 Epoch 시간이라고 부르기도 한다. 1970년 1월 1일 00:00:00 협정 세계시(UTC) 부터의 경과 시간을 초로 환산하여 정수로 나타낸 것이다

이 프로젝트에서는 이러한 시간 format을 해주는 dayjs라는 라이브러리를 사용해보기로 하였다.

여기서, data의 내용이 확정되었으니 database의 type을 지정해주자!

api
 ┣ axios
 ┃ ┗ AxiosTodoService.ts
 ┣ types
 ┃ ┗ todoItem.d.ts
 ┣ TodoService.ts
 ┗ index.ts

api 폴더 내에 types라는 폴더를 생성하여 todoItem의 타입을 지정해주었다.

export type TodoItemType = {
  id?: number;
  todoContent: string;
  isDone: boolean;
  priority: number;
  dateTimes: {
    createdDateTime: string;
    doneDateTime?: string;
  };
  estimatedMins: number;
};

<Text/> <IconBtn> 컴포넌트 재사용 가능하도록 분리

//TodoItemView
type Props = {
  id: number;
  title: string;
  done: boolean;
  mouseOn: boolean;
  handleMouseEnter: () => void;
  handleMouseOut: () => void;
  handleClickDeleteBtn: () => void;
  handleClickCheckBtn: () => void;
};

const TodoItemView = ({
  id,
  title,
  done,
  mouseOn,
  handleMouseEnter,
  handleMouseOut,
  handleClickDeleteBtn,
  handleClickCheckBtn,
}: Props) => {
  return (
    <TodoItemContainer
      onMouseEnter={handleMouseEnter}
      onMouseLeave={handleMouseOut}
    >
      <IconBtn handleOnClick={handleClickCheckBtn}>{done ? <MdCheckBox /> : <MdCheckBoxOutlineBlank />}</IconBtn>
      <Text contents={title} />
      {mouseOn ? (
        <IconBtn handleOnClick={handleClickDeleteBtn}>
          <MdDelete />
        </IconBtn>
      ) : (
        <div />
      )}
    </TodoItemContainer>
  );
};

export default TodoItemView;

여기서 보면 IconBtnhandleOnClickhandleClickDeleteBtnhandleClickCheckBtn 로 두개의 함수가 모두 각각 사용되고있다.
IconBtn으로 들어가보면 주의해줘야하는 부분이있다!
type Props 부분에서 handleOnClick: () => void; 이렇게 작성되면, IconBtn 중에서 onClick이벤트가 안붙을 경우도 있기에 아래와 같이 ? 를 같이 작성해줘야 에러가 나오지 않는다!
참고자료 : Typescript optional function truthy check

type Props = {
  children: React.ReactNode;
  handleOnClick?: () => void;
};

const IconBtn = ({ children, handleOnClick }: Props) => {
  return <IconBtnStyle onClick={handleOnClick}>{children}</IconBtnStyle>;
};

export default IconBtn;

dayjs 활용 header 기능 구현

//TodoHeader
import React from "react";
import TodoHeaderView from "./TodoHeaderView";
import dayjs from "dayjs"; //dayjs
import "dayjs/locale/ko"; //한국형식!

type Props = {};

const TodoHeader = (props: Props) => {
  dayjs.locale("ko");
  const today = dayjs().format("YYYY년 M월 D일");
  const day = dayjs().format("dddd");
  return <TodoHeaderView date={today} day={day} remainingCount={1} />; 
  //remainingCount는 상태관리 하기 전까지 하드코딩 이후 변경예정
};

export default TodoHeader;
type Props = {
  date: string;
  day: string;
  remainingCount: number;
};

const TodoHeaderView = ({ date, day, remainingCount }: Props) => {
  return (
    <TodoHeaderContainer>
      <h1>{date}</h1>
      <div className="day">{day}</div>
      <div className="tasts-left">{remainingCount}개 남음</div> 
    </TodoHeaderContainer>
  );
};

export default TodoHeaderView;

delete, isDone 기능 구현

API 호출과 렌더링 작업 분리

isDone 구현

//TodoItem
type Props = {
  item: TodoItemType;
};

const TodoItem = ({ item }: Props) => {
  const [todoItem, setTodoItem] = useState(item);
  const handleClickCheckBtn = () => {
    const temp = { ...todoItem };
    todoService.changeDoneTodoItem(item.id!, !todoItem.isDone).catch(() => {
      setTodoItem(temp);
    });
    setTodoItem({ ...todoItem, isDone: !todoItem.isDone });
  };

  return (
    <TodoItemView
      id={todoItem.id!}
      title={todoItem.todoContent}
      done={todoItem.isDone}
      mouseOn={mouseOn}
      handleClickCheckBtn={handleClickCheckBtn}
    />
  );
};

export default TodoItem;
//TodoItemView

type Props = {
  id: number;
  title: string;
  done: boolean;
  handleClickCheckBtn: () => void;
};

const TodoItemView = ({
  id,
  title,
  done,
  handleClickCheckBtn,
}: Props) => {
  return (
    <TodoItemContainer
      onMouseEnter={handleMouseEnter}
      onMouseLeave={handleMouseOut}
    >
      <IconBtn handleOnClick={handleClickCheckBtn}>{done ? <MdCheckBox /> : <MdCheckBoxOutlineBlank />}</IconBtn>
      <Text contents={title} />
      {mouseOn ? (
        <IconBtn handleOnClick={handleClickDeleteBtn}>
          <MdDelete />
        </IconBtn>
      ) : (
        <div />
      )}
    </TodoItemContainer>
  );
};

export default TodoItemView;

handleClickCheckBtn 클릭 시 changeDoneTodoItem 으로 item.id 로 !todoItem.isDone 값을 보내줘야하는데, 고민해봐야하는것은! 여기서 렌더링 시점과 api 호출의 시점을 고민해보았다.

  • 먼저 api에 보내주고 다시 그려주려면 다시 데이터를 get해와서 그려주면 비효율이지 않을까 ??
  • api를 보내주고 원래 데이터에서 바뀐부분만 다시 그려주면 되지 않을까 ??
  • 그럼 catch 후에 에러가 없을 때 데이터를 렌더링 해주면 되지 않을까 ?
  • 네트워크탭에서 slow3G환경에서 테스트해보니 catch 후에 렌더링 해주는 것을 확인하니 매~~~우 느렸다!
  • api를 보내고, 렌더링 되는 것 처럼 먼저 보여주고 catch로 에러가 잡혔을 때 원래의 변경 전 값으로 그려주면 좀 더 효율적으로 사용자 경험을 만들 수 있지않을까 ?
const handleClickCheckBtn = () => {
    const temp = { ...todoItem }; //원래 item 값 임시저장
    todoService.changeDoneTodoItem(item.id!, !todoItem.isDone).catch(() => {
      setTodoItem(temp); //catch로 에러가 잡혔을 때 temp를 보여줌
    });
    setTodoItem({ ...todoItem, isDone: !todoItem.isDone }); 
  // 정상적으로 서버 작동 했을 때 체크 여부만 변경하여 보여줌!
  };

item.id! 로 해준 이유는 ?
id 값이 number만 받아야하는데 undefined가 될 수 있어서 강제로 number만 해준다! 라는 의미!!

delete 구현

delete도 똑같이 구현해주면 되지만, 과연 todoitem에서 지워주는 행위를 하는게 맞을까 ?
todolist에서 지워줘야 하지않을까 ?
그렇다면, props으로 전달해주는 방법을 바꿔야겠다!

//TodoItemView
type Props = {
  handleClickDeleteBtn: () => void;
};

const TodoItemView = ({
  handleClickDeleteBtn,
}: Props) => {
  return (
    
        <IconBtn handleOnClick={handleClickDeleteBtn}>
          <MdDelete />
        </IconBtn>
  
  );
};
//TodoItem
type Props = {
  item: TodoItemType;
  handleClickDeleteBtn: (id: number) => void;
};

const TodoItem = ({ item, handleClickDeleteBtn }: Props) => {
  return (
    <TodoItemView
      id={todoItem.id!}
      title={todoItem.todoContent}
      done={todoItem.isDone}
      mouseOn={mouseOn}
      handleMouseEnter={handleMouseEnter}
      handleMouseOut={handleMouseOut}
      handleClickDeleteBtn={() => {
        handleClickDeleteBtn(todoItem.id!);
      }}
      handleClickCheckBtn={handleClickCheckBtn}
    />
  );
};

export default TodoItem;

todolistview와 todolist가 있으면 함수니까 todolist에서 만들어서 todolistview에 넘겨줘야겠군!

//todolist
const TodoList = (props: Props) => {
  const [items, setItems] = useState<TodoItemType[]>([]);
  const deleteTodoItem = async (id: number) => {
    const tempItems = [...items];
    todoService.deleteTodoItem(id).catch(() => {
      setItems(tempItems);
    });
    setItems([...items].filter((item) => item.id! !== id));
  };
  useEffect(() => {
    todoService.getTodoItems().then((data: TodoItemType[]) => {
      setItems(data);
    });
  }, []);

  return <TodoListView items={items} handleClickDeleteBtn={deleteTodoItem} />;
};

export default TodoList;

useState에서 type 지정??
useState<number>()

state의 type을 지정할 때에는 위와 같이 Generics안에 타입을 지정해주면 된다. 그런데 사실 초기값을 지정해주면 알아서 타입을 유추하기 때문에 굳이 지정해주지 않아도 무방하다.
그리고, 배열임을 알려주기위해 [] 도 넣어주자!

usestate에서 object인경우 어떻게하지 ??
https://stackoverflow.com/questions/54150783/react-hooks-usestate-with-object

 setExampleState({...exampleState,  masterField2: {
        fieldOne: "a",
        fieldTwo: {
           fieldTwoOne: "b",
           fieldTwoTwo: "c"
           }
        },
   })

이런식으로 하는구나를 보고 이렇게 구현!

    setItems([...items].filter((item) => item.id! !== id));

todolist에서는 getTodoItems()을 통해서 data를 items에 setItems를 해준다. 하지만 delete해줄 id값을 모르기에, delete 함수에 id를 파라미터로 넣어준다!
또한 이전의 isDone의 방식처럼 api호출과 렌더링 작업을 분리해주었다.
여기서 TodoListView로 넘겨주고,

// TodoListView로
type Props = {
  items: TodoItemType[];
  handleClickDeleteBtn: (id: number) => void;
};

const TodoListView = (props: Props) => {
  return (
    <TodoListContainer>
      {/* items의 값이 있으면 ? * 배열의 갯수만큼 보여주기*/}
      {props.items.map((item) => (
        <TodoItem
          item={item}
          key={item.id}
          handleClickDeleteBtn={props.handleClickDeleteBtn}
        />
      ))}
    </TodoListContainer>
  );
};

export default TodoListView;
//TodoItem
type Props = {
  item: TodoItemType;
  handleClickDeleteBtn: (id: number) => void;
};

const TodoItem = ({ item, handleClickDeleteBtn }: Props) => {
 
 return (
    <TodoItemView
      id={todoItem.id!}
      title={todoItem.todoContent}
      done={todoItem.isDone}
      mouseOn={mouseOn}
      handleMouseEnter={handleMouseEnter}
      handleMouseOut={handleMouseOut}
      handleClickDeleteBtn={() => {
        handleClickDeleteBtn(todoItem.id!);
      }}
      handleClickCheckBtn={handleClickCheckBtn}
    />
  );
};

export default TodoItem;

여기 까지 handleClickDeleteBtn 의 id를 파라미터로 같이 넘겨주고, 이후부터는 id값을 알기에 같이 안넘겨도됨!
다시 TodoItemView를 보자!


type Props = {
  handleClickDeleteBtn: () => void;
};

const TodoItemView = ({handleClickDeleteBtn}: Props) => {
  return (
      
        <IconBtn handleOnClick={handleClickDeleteBtn}>
          <MdDelete />
        </IconBtn>
    
export default TodoItemView;

사용자 경험을 고민 해본 하루!

profile
프론트엔드 개발자 항상 뭘 하고있는 슬링

0개의 댓글