Todolist는 정말 많이도 만들어봤다. 근데 매번 다른 방법으로 만들 때 마다 코드의 수가 줄어들고
더 간단해지는 게 신기하다. 늘 Todolist는 신세계다.. 이번 Todolist는
useform, Recoil, TS 등을 활용하여 만들어 보았다.
강의를 듣기 전 먼저 내가 Todolist를 만들어 보았다. 만든 코드는 다음과 같다.
import { useState } from "react";
import { useForm } from "react-hook-form";
interface ITodo {
todo?: string;
}
const TodolistTraining = () => {
const { register, watch, handleSubmit, reset } = useForm();
const [todo, setTodo] = useState<ITodo[]>([]);
const Pass = () => {
setTodo((current: any) => [watch("Todo"), ...current]);
};
return (
<div>
<form onSubmit={handleSubmit(Pass)}>
//handleSubmit은 Validation을 당담한다. 또, preventDefault를 해준다.
<input {...register("Todo")} placeholder="todolist" />
//register를 이용해서 "Todo" 라는 name의 value를 감시할 수 있게됐다.
<button>제출</button>
</form>
<div>
<ul>
{todo?.map((item: any, index) => (
<li key={index}>{`🔥 ${item}`}</li>
//any로 타입을 정리한 것이 매우 아쉽다.. 아직 TS는 어려움이 많다.
))}
</ul>
</div>
</div>
);
};
위 코드를 보면 register로 input을 감시하고 그 input에서의 변화값을 watch를 통해 가져와
useState에 state값으로 넣어주었다. 물론 setState()를 이용하여 최근의 배열에 추가로 Todo를
넣어주는 방법을 활용하였다.
그런데 문제가 있었다. 위 코드는 다른 기능은 정상작동하지만 form을 submit 한 다음 input의 value를
"" 로 즉 공백으로 만드는데 어려움을 겪었다. 다양한 방법을 찾아보았지만 결과적으로 찾지못했다.
그래도 찾았던 useForm에서 제공하는 reset이라는 함수는 공식문서를 읽어도 사용방법을 이해하지 못해서
예습하며 애를 썼다.
이것보다 Recoil(전역상태관리)을 활용하는 것이 input에 기능을 추가하고 컴포넌트를 나누어 관리하는데
많은 도움이 된다. 다음은 강의를 학습 후 새롭게 짜본 Todolist이다.
//Todo.tsx
const Todo = () => {
const toDos = useRecoilValue(toDoState);
/*
const value = useRecoilValue(toDoState);
const modFn = useSetRecoilState(toDoState);
*/
console.log(toDos);
return (
<div>
<h1>To Dos</h1>
<hr></hr>
<CreateToDo />
<ToDoPaint />
</div>
);
};
//ToDoPaint.tsx
const ToDoPaint = () => {
interface IToDo {
text: string;
id?: number;
category: "TO_DO" | "DOING" | "DONE";
//의도적으로 type을 통해 해당 하는 recoil의 value를 제한함
// 즉 , 해당 atom의 value인 toDos를
}
const setToDos = useSetRecoilState(toDoState);
const toDos = useRecoilValue<IToDo[]>(toDoState);
const onClick = (newCategory: IToDo["category"]) => {
console.log("i wanna go to", newCategory);
};
return (
<ul>
{toDos.map((toDo) => (
<li key={toDo.id}>
<span> {toDo.text}</span>
{toDo.category !== "DOING" && (
<button onClick={() => onClick("DOING")}>Doing</button>
)}
{toDo.category !== "TO_DO" && (
<button onClick={() => onClick("TO_DO")}>To Do</button>
)}
{toDo.category !== "DONE" && (
<button onClick={() => onClick("DONE")}>DONE</button>
)}
</li>
))}
</ul>
);
};
//CreateToDo.tsx
const CreateToDo = () => {
interface IForm {
toDo: string;
}
const { handleSubmit, register, setValue } = useForm<IForm>();
const setToDos = useSetRecoilState(toDoState);
const onSubmit = (data: IForm) => {
console.log(data);
console.log("add to do", data.toDo);
setValue("toDo", "");
setToDos((current) => [
{ text: data.toDo, id: Date.now(), category: "TO_DO" },
...current,
]);
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<input
{...register("toDo", {
required: "Please wirte a To Do",
})}
placeholder="Write a to do"
/>
<button>Add</button>
</form>
);
};
위 코드가 전체 코드이고 하나씩 몰랐던 부분을 정리해보겠다.
그 전에 필자가 예습을 하며 헷갈려했던 setValue와 reset에 대해 정리를 먼저 해보면
useform을 쓰다보면 해당 register된 input을 reset 하고싶은 생각이 들 수있다. 먼저 input의 reset이 없는 코드는
const App =()=>{
const {register, handleSubmit} = useform()
const Pass=(data)=>{
//여기에서 useState나 Recoil을 이용해서 데이터를 업데이트시켜준다.
}
return
(<form onSubmit={handleSubmit(Pass,Fail)}>
<input {...register("toDo")}/>
</form>)
}
이렇게 되면 Input이 제출이 되고도 Input에 user가 적은 흔적이 남게된다.
먼저 setValue!
const Pass =(data)=>{
setValue("toDo","")
// register된 Input의 Name과 그 input에 넣을 값을 파라메터로 받는다.
}
setValue는 바로 input이나 select에 접근한다.
reset
const Pass=(data)=>{
reset({toDo:""})
}
//reset은 form안의 필드값과 에러값을 조정한다.
필자가 이해한 둘 의 차이는 먼저 두 가지가 있다.
여기서, 바인딩이란 실제 값이나, 프로퍼티를 결정짓는 행위를 말한다
HandleSubmit은 기본적으로 useform 라이브러리에서 제공하는 validation 메서드이다.
즉, 타당성 검사를 담당한다.
form이 제출 될 때 input에 넣은 값들을 검증해주는 역할을 한다.
게다가 preventDefault를 해주는 역할까지해 새로고침을 위해 event 파라메터를 쓰지 않아도 된다.
handleSubmit은 Pass했을 때 input의 key값과 value를 object형식으로 반환해준다.
이를 필자는 잘 이해를 못했었다.. 그래서 아래의 코드를 이해하지 못했던 거 같다.
const Pass=(data)=>{
여기서 data는 뭐지??????
// => data는 {todo: "helloWorld"}
}
return <div>
<form>
<input {...register("todo")} placeholder="Write a memo"/>
</form>
<ul>
{//여기에 이제 state로 저장된 배열을 가지고와 map으로 <li></li> Return }
</ul>
</div>
저 data를 이해하지 못해서 type을 결정해주지 못했다.
useRecoilValue는 Recoil에서 사용된 atom(bubble)을 꺼내 올 때 사용한다.
즉, 값만 가져올 때 위 useRecoilValue를 사용한다.
그리고 이 atom의 값을 변경하고 싶을 때는 useSEtRecoilState를 사용했다.
근데 이 두개를 한 컴포넌트에서 사용하고 싶다면 useRecoilState! 를사용하면된다.
useRecoilState(atom의이름) 은 [data,setData]의 역할을한다.
즉, useState()와 거의 비슷하다!! 대신 전역으로 상태를 관리할때 쓴다는 점이 다르다
위 메서드는 앞으로도 유용하게 사용될 거 같다.
Todolist를 만들면서 가장 어려웠던 점은 변수들의 타입을 정해주는 일이었다.
변수들의 타입을 interface로 정해주는 일을 정확하게 이해하지 못했던 거 같다.
그리고 Recoil value의 타입을 정리하는 것에 익숙치 않았었다.
변수의 타입을 정확하게 적어주는것이 중요하다 Todolist의 경우에는 배열을 많이 사용하는데 배열을 매개변수로 받았을 때
타입을 정리해보겠다.
const toDoState = atom({
key:"toDos" ,
default:[],
})
const data = useRecoilValue(toDoState)
여기서 중요한 건 toDoState라는 atom은 앞으로 무엇을 담을 것인가!! 이게 핵심이다.
사실 그냥 string[]를 담는다면
const toDoState:string[] = atom({
key:"toDos",
default:[],
})
이렇게 사용할 수 있겠지만 위 atom에 무엇을 담을지 미리 interface를 이용해 정할 수 있다.
interface ITodo{
text:string,
id:number,
category:"TO_DO",|"DOING"|"DONE"
//위와같이 특정 string으로 값을 정할 수도있다.
const toDoState = atom<ITodo[]>({
key:"toDos" ,
default:[],
})
}
위와 같이 설정을 해주면 toDoState의 값은 [{text:,id:~,category:~~}]
의 형태로 설정이 된다.
그렇게 handleSubmit의 성공함수 인 onSubmit은
const onSubmit = (data: IForm) => {
//여기서 IForm은 data의 타입을 설명해주는데 data는 input에서 받아온 key: value형태의 object임으로
//interface{toDo:string } 이렇게 설정된다.
setToDos((current) => [
{ text: data.toDo, id: Date.now(), category: "TO_DO" },
...current,
]);
//current는 자동으로 [text:,id:,category:]형태의 배열이 된다.
};
Todolist를 만들면서 never타입을 만나 오류를 겪었다 이 역시, 타입에 대한 설정을 이해하지 못해서 발생했는데
강의에서 니코선생님은 never타입이니 interface를 활용하여 타입을 설정해 주어야 한다고 했다.
무슨 말인지 몰랐다..
never 타입이란 공집합이다. 즉, 숫자체계에서 아무것도 없는 0이 있듯이 문자체계에도 불가능을 나타내는타입이 필요하다. 그것이 바로 never이다.
위 표현은 never타입 정리 Toast 사이트에서 발췌했다.
나는 그렇다면 왜 never 타입을 마주했었을까??
const toDoState = atom({
key:"toDos",
default:[],
})
위 코드에서 default는 []로 설정을 하였다. 그렇다 보니 useRecoilValue를 했을 때 그 타입은
never가 되어있었다.[]를 default값으로 주었기 때문데 never 타입으로 인식이된다.
반면 default값을 string으로 정하니 기본 타입이 string이 되었다.
즉 빈 배열은 never 타입으로 설정이 된다!!