<Text/>
<IconBtn>
컴포넌트 재사용 가능하도록 분리api
┣ axios
┃ ┗ AxiosTodoService.ts
┣ types
┃ ┗ todoItem.d.ts
┣ TodoService.ts
┗ index.ts
//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>;
}
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;
}
}
import { AxiosTodoService } from "./axios/AxiosTodoService";
import TodoService from "./TodoService";
export const todoService: TodoService = new AxiosTodoService();
//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;
여기서 보면 IconBtn
의 handleOnClick
이 handleClickDeleteBtn
와 handleClickCheckBtn
로 두개의 함수가 모두 각각 사용되고있다.
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;
//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;
//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 호출의 시점을 고민해보았다.
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도 똑같이 구현해주면 되지만, 과연 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;
사용자 경험을 고민 해본 하루!