State Management(3)

초연2e·2022년 10월 2일
0

MUTSA_Front.archive

목록 보기
6/16

toDo 추가하기

todoState라는 atom을 만들자.

    const value = useRecoilValue(toDoState);
    const modFn = useSetRecoilState(toDoState);

우리가 앞서 recoil에 대해 공부했을 때 배웠던 것이다.
useRecoilValue를 사용하면 state값을 받아올 수 있고,
useSetRecoilState를 사용하면 state를 수정할 수 있다.


우리는 useRecoilState를 이용하여 두 코드를 한 줄로 대체할 수 있다.

   const [toDos, setToDos] = useRecoilState(toDoState);

useRecoilState 함수는 value와 modifier 함수를 반환해준다. (물론 import 필요)
리액트의 setState와 거의 똑같음,,

interface IToDo{
    text: string;
    category: "TO_DO" | "DOING" | "DONE";
}

const toDoState = atom<IToDo[]>({
    key: "toDo",
    default: [],
});

이렇게 해서 typescript에게 toDos가 IToDo 객체로 이뤄진 배열이라는 것을 알리자.
안그러면 toDos에 값을 넣으려고 하면 오류가 날 것이다..


폼이 제출되고 데이터가 모두 유효하면 state를 바꿔보자.

    const handleValid = ({toDo}: IForm) => {
      setToDos(oldToDos => [{text:toDo, category:"TO_DO"}, ...oldToDos]);
      setValue("toDo", "");
    };

이렇게 해주면 이전의 state를 oldToDos로 받아와서 배열 안의 요소를 반환한다.
그러면서 새 toDo를 받아온다.

우리가 interface로 설정해놨던 text,category를 사용하자.

우리가 setValue로 submit 후에 input을 비우도록 설정해놔서 안보이지만 "멋사 과제하기"라고 작성하고 Add를 클릭했더니 콘솔에 저렇게 찍혔다.

text와 category 모두 잘 들어간 것을 확인할 수 있음



자 이제 interface에 id:number를 추가한 후,
setToDos에도 id:Date.now()해주자.

form 아래 만든 ul태그에 map으로

  <ul>
    {toDos.map(toDo => 
     (<li key={toDo.id}>{toDo.text}</li>
  ))}
    </ul>

해주면

이렇게 여러개가 잘 출력된다.
젤 최근 작성한 투두가 젤 위에 뜬다.




toDo 카테고리 바꾸기


우리는 DONE에서 DOING, DOING에서 TO_DO, TO_DO에서 DOING, DONE으로 카테고리가 변하도록 설정해줘야한다. 그게 투두리스트 기능이니까,,

(이 사이에 추가로 코드 컴포넌트 분리해줫다,)





toDo 카테고리 넘겨주기

버튼을 클릭하면 toDo의 카테고리가 변경되게 해보겠다.

function ToDo({ text, category }: IToDo) {
    const onClick = (newCategory: IToDo["category"]) => {
        console.log("i wanna to ",newCategory);
    };
    return (
    <li>
        <span>{text}</span>
        {category !== "DOING" && 
            <button onClick={()=> onClick("DOING")}>Doing</button>}
        {category !== "TO_DO" && 
            <button onClick={()=> onClick("TO_DO")}>To Do</button>}
        {category !== "DONE" && 
            <button onClick={()=> onClick("DONE")}>Done</button>}
    </li>
    );
}

이러면 버튼을 클릭하면 해당 버튼의 카테고리 값이 onClick함수의 인자로 넘겨진다...

이런 식으로 인자가 있는 onClick event를 처리해줄 수 있다.


즉, 인자를 받는 함수를 만들고 새 익명함수를 선언해서 인자를 넘겨주었다.



이렇게 해주지 않고

function ToDo({ text, category, id }: IToDo) {
    const onClick = (event: React.MouseEvent<HTMLButtonElement>) => {
      const {
        currentTarget: { name },
      } = event;
    };
    return (
    <li>
        <span>{text}</span>
        {category !== "DOING" && (
        <button name="DOING" onClick={onClick}>
            Doing
        </button>
        )}
        {category !== "TO_DO" && (
        <button name="TO_DO" onClick={onClick}>
            To Do
        </button>
        )}
        {category !== "DONE" && (
        <button name="DONE" onClick={onClick}>
            Done
        </button>
        )}
    </li>
    );
}

이렇게 해줘도 똑같이 작동한다.


    const setToDos = useSetRecoilState(toDoState);

를 추가해주면 toDo를 수정할 수 있는 조건이 완성되었다.




카테고리 수정하고 싶은 toDo의 index 넘겨주기


이렇게 toDo를 다섯개 만들어보자.
우리는 3이라고 쓰여진 toDo의 카테고리를 수정해주고싶다.
(인덱스 2겠지?)

  1. 우리는 이 toDo들이 어디에 있는지 모른다. 그래서 id로 toDo를 찾아야함!
  2. array안에 있는 object의 index를 찾아야함
  setToDos((oldToDos) => {
    const targetIndex = oldToDos.findIndex(toDo => toDo.id === id)
    return oldToDos;
  });

index를 찾기 위해 이렇게 해준다.

즉, 우리는 setToDos로 oldToDos의 array를 받아오고 이 array에서 toDo의 인덱스를 찾기 위해 toDo의 id가 props의 id와 같은지 비교한다.

이제 3의 버튼을 클릭 후 targetIndex를 콘솔창에 찍어보면

인덱스 2가 잘 찍힌다. target의 경로를 잘 찾은 것이다.




버튼 클릭 시 카테고리 변경된 새 toDo 만들기


이제는 새로운 toDo를 만들어서 원래 toDo를 업데이트하고, 새로운 toDo를 다른 카테고리로 만들 수 있어야한다.

    const onClick = (event: React.MouseEvent<HTMLButtonElement>) => {
      const {
        currentTarget: { name },
      } = event;
      setToDos(oldToDos => {
        const targetIndex = oldToDos.findIndex(toDo => toDo.id === id);
        const oldToDo = oldToDos[targetIndex];
        const newToDo = {text, id, category:name};
        return oldToDos;
      });

새로운 toDo를 만들어주자. props에서 이미 새롭게 만들어지는 toDo들의 text, id를 넘겨주기 때문에 그대로 가지고 오면 됨. 그러나 category는 우리가 클릭한 버튼값을 가져와야하기 때문에 name으로 설정해주어야한다! (oldToDo는 비교를 위해 만들어줌)

이제 이 oldToDo와 newToDo를 콘솔에 찍어보자.

이번엔 2라고 쓰여진 투두에서 Doing버튼을 눌러보았다.

콘솔엔 이렇게 찍힌다.
원래는 TO_DO 카테고리이던 투두가 DOING 카테고리로 변경된 것을 확인할 수 있다.

근데 이건 투두의 카테고리가 "교체"된 상태가 아니라 oldToDo는 또따로 존재하고 추가로 newToDo라는 카테고리만 바뀐 새 투두가 만들어진 상태와 같다. 두개의 투두가 존재하는,,,,,,




이제는 진짜로 oldToDos의 array에서 oldToDo 자체의 카테고리를 새롭게 바꿔주자..

현재 우리는 oldToDo가 어디있는지도 알고, newToDo도 가지고 있는 상태이다.
그러면 targetIndex에 있는 toDo를 newToDo로 바꿔주면 되겠지?





버튼 클릭 시 새 toDo로 아예 "교체"시키기


이걸 위해 배열의 특정 원소를 어떻게 교체하는지에 대해 배워보자.

    const onClick = (event: React.MouseEvent<HTMLButtonElement>) => {
      const {
        currentTarget: { name },
      } = event;
      setToDos(oldToDos => {
        const targetIndex = oldToDos.findIndex(toDo => toDo.id === id);
        const oldToDo = oldToDos[targetIndex];
        const newToDo = {text, id, category:name as any};
        return [
            ...oldToDos.slice(0, targetIndex),
            newToDo,
            ...oldToDos.slice(targetIndex+1),
        ];
      });

결론부터 말하면 이 코드처럼 해주면 된다.

return 부분을 잘 봐보자..
oldToDos에서 0부터 targetIndex-1까지 잘라주고 그 사이에 newToDo를 넣고 다시 oldToDos의 targetIndex+1부터 끝까지 붙여준다.
이렇게 해주면 우리가 원하는 targetIndex의 toDo만 쏙 newToDo로 변경되겠지?

아 근데 이렇게만 해주면 에러가 남.
이유는 우리 카테고리는 interface 설정 상 TO_DO, DOING, DONE 세개 중 하나여야만 하는데 우리의 newToDo의 카테고리는 그냥 string이기 때문이다..
그래서 코드에 category:name as any; 를 해주어 타입스크립트에게 신경쓰지말라고 전달한 것이다.



이러면 toDo 카테고리 수정이 가능하다!
toDo를 만들고 버튼을 클릭해주면 원래 카테고리의 투두는 사라지고 그 버튼에 해당하는 category값을 가진 새 toDo만 남게된다.




Recoil Selectors


Selector

파생된 state(derived state)의 일부.

즉, 기존 state를 가져와서, 기존 state를 이용해 새로운 state를 만들어서 반환할 수 있고, 기존 state를 이용만할 뿐 변형시키지 X.
derived state는 다른 데이터에 의존하는 동적인 데이터를 만들 수 있기 때문에 강력한 개념.


현재는 카테고리에 상관없이 모든 투두가 전부 같은 state에 저장되고 있다.

Selector를 이용해서 이 투두들을 카테고리별로 분류해줄 것이다.

Selector를 이용하면 state를 가져와서 원하는대로 변형할 수 있기 때문에 매우 유용하다. atom을 가져다가 output을 변형할 수 있다. (import 필요)

<atoms.tsx>
  
export const toDoSelector = selector({
  key:"toDoSelector",
  get: ({get})=> {
    return "hello";
  }
});

selector는 이런식으로 사용하면 된다.
일단 key와 get함수를 필요로 하는데 이 get에서 return 하는 값이 바로 toDoSelector의 value가 된다.

이렇게 해주고 이 selector가 필요한 컴포넌트에서 import 후 useRecoilValue로 value값을 받아 콘솔에 찍어보면 저 hello가 찍힐 것이다.
(useRecoilValueatom의 output, selector의 output 모두 얻을 수 있다.)

카테고리 별로 나눠진 배열 만들기

우리는 get으로 모든 todo를 받아올거다.

<atoms.tsx>
  
export const toDoState = atom<IToDo[]>({
  key: "toDo",
  default: [],
});

export const toDoSelector = selector({
  key:"toDoSelector",
  get: ({get})=> {
    const toDos = get(toDoState);
    return toDos.length;
  },
});

이렇게 해주면.. 보다시피 get에서 toDoState라는 우리의 atom으로 모든 toDo를 받아온다. 이러면 이제 atom이 변할 시 우리 selector도 변하는 것이다.

새 투두를 추가하면 콘솔에 찍히는 숫자가 늘어날 것이다.



쨌든 이런식으로 작동한다..
이제 카테고리별로 분류해서 배열을 만들어보자.

자바스크립트의 filter를 사용한다.

<atoms.tsx>
  
export const toDoSelector = selector({
  key:"toDoSelector",
  get: ({get})=> {
    const toDos = get(toDoState);
    return [
      toDos.filter(toDo => toDo.category === "TO_DO"), 
      toDos.filter(toDo=>toDo.category==="DOING"), 
      toDos.filter(toDo=>toDo.category==="DONE"),
    ];
  },
});

filter로 카테고리별로 나누어진 투두들을 원소로 갖는 3개의 서로다른 배열을 만들어냈다.

이런식으로,, 내가 카테고리를 변경하는대로 콘솔에 찍힌다.

우리는 selector를 이용해서 state값을 변형했다.
그러나 주의할 점은 우리가 state 자체를 바꾼 것이 아니라
그 output만 원하는대로 바꾸고 있다는 것이다.


<ToDoList.tsx>

import { useRecoilValue } from "recoil";
import { toDoSelector } from "../atoms";
import CreateToDo from "./CreateToDo";
import ToDo from "./ToDo";


function ToDoList(){
    const [toDo, doing, done] = useRecoilValue(toDoSelector);
    return (
    <div>
        <h1>To Dos</h1>
        <hr/>
        <CreateToDo/>
        <h2>To Do</h2>
        <ul>
            {toDo.map((toDo) => 
                <ToDo key={toDo.id} {...toDo} />
            )}
        </ul>
        <hr/>
        <h2>Doing</h2>
        <ul>
            {doing.map((toDo) => 
                <ToDo key={toDo.id} {...toDo} />
            )}
        </ul>
        <hr/>
        <h2>Done</h2>
        <ul>
            {done.map((toDo) => 
                <ToDo key={toDo.id} {...toDo} />
            )}
        </ul>
        <hr/>
    </div>
    )
}
export default ToDoList;

자 이제 이렇게 해주면
3개의 배열을 각각 todo, doing, done에 따로 넣어주는게 된다.
그리고 각각의 배열을 map으로 돌려주면 우리가 원하는대로 카테고리별로 render되어 화면에 출력된다.

이렇게!ㅇㅇ👍👍👍👍





toDO 작성 시 카테고리 선택할 수 있게


<toDoList.tsx>
  
  ...

    const onInput = (event:React.FormEvent<HTMLSelectElement>) => {
        console.log(event.currentTarget.value);
    }
    return (
    <div>
        <h1>To Dos</h1>
        <hr/>
        <select onInput={onInput}>
            <option value="TO_DO">To Do</option>
            <option value="DOING">Doing</option>
            <option value="DONE">Done</option>
        </select>
        <CreateToDo/>
    </div>
    );

 ...

이렇게 해주면,,

옵션 선택하는대로 콘솔에 value가 찍힌다.

이제 이 value를 categoryState의 atom과 연결시키면 됨
(categoryState를 추가로 만들어주었다.)





선택된 카테고리 atom과 연결


현재의 값과 값을 수정하는 훅을 사용하자.
useRecoilState

function ToDoList(){
    const [toDo, doing, done] = useRecoilValue(toDoSelector);
    const [category, setCategory] = useRecoilState(categoryState);
    const onInput = (event:React.FormEvent<HTMLSelectElement>) => {
        setCategory(event.currentTarget.value);
    };
    return (
    <div>
        <h1>To Dos</h1>
        <hr/>
        <select value={category} onInput={onInput}>
            <option value="TO_DO">To Do</option>
            <option value="DOING">Doing</option>
            <option value="DONE">Done</option>
        </select>
        <CreateToDo/>
    </div>
    );
}
export default ToDoList;

category가 받아와진다.
value와 categoryState의 atom이 연결된 상태





선택된 카테고리만 render해서 보여주기


selector의 get 함수를 사용하자.

<atoms.tsx>
  
export const toDoSelector = selector({
  key:"toDoSelector",
  get: ({get})=> {
    const toDos = get(toDoState);
    const category = get(categoryState);
    return toDos.filter((toDo)=>toDo.category === category);
  },
});
<ToDoList.tsx>
  
function ToDoList(){
    const toDos= useRecoilValue(toDoSelector);
    const [category, setCategory] = useRecoilState(categoryState);
    const onInput = (event:React.FormEvent<HTMLSelectElement>) => {
        setCategory(event.currentTarget.value);
    };
    return (
    <div>
        <h1>To Dos</h1>
        <hr/>
        <select value={category} onInput={onInput}>
            <option value="TO_DO">To Do</option>
            <option value="DOING">Doing</option>
            <option value="DONE">Done</option>
        </select>
        <CreateToDo/>
        {toDos?.map(toDo => <ToDo key={toDo.id} {...toDo}/>)}
    </div>
    );
}
export default ToDoList;





toDo 추가 시 지금 선택된 category에 바로 넣을 수 있게 만들기

지금은 Done 카테고리를 선택하고 Add해도 자동으로 toDo로 들어가기 때문에 바로 보이지 않는다.
이 점을 개선해주자.

toDo의 카테고리가 categoryState에 따라 추가되게 해보자는 말,,

<CreateToDo.tsx>
  
  ...

function CreateToDo() {
  const setToDos = useSetRecoilState(toDoState);
  const category = useRecoilValue(categoryState);
  const { register, handleSubmit, setValue } = useForm<IForm>();
  const handleValid = ({toDo}: IForm) => {
    setToDos(oldToDos => [
      {text:toDo, id:Date.now(), category:category},
      ...oldToDos]);
    setValue("toDo", "");
  };
  return (
      <form onSubmit={handleSubmit(handleValid)}>
      <input 
          {...register("toDo", {
              required: "Please write a To Do",
          })}
          placeholder="Write a to do"
      />
      <button>Add</button>
  </form>
  )
};

 ...

이렇게 해주자..

useRecoilValue로 categoryState에서 category를 받아와주고 이걸 setToDos에 넣어서 해당 카테고리로 등록되게 해주는 방식.

이러면 우리가 원하던대로 우리가 선택한 해당 카테고리에 바로 추가된다.





enum

enum은 열거형으로 이름이 있는 상수들의 집합을 정의할 수 있음. 열거형을 사용하면 의도를 문서화 하거나 구분되는 사례 집합을 더 쉽게 만들수 있다.

export enum Categories {
  "TO_DO",
  "DOING",
  "DONE",
}

이렇게 enum으로 선언해주면
우리는 모든 필요한 곳에서 저 카테고리값들을 쉽게 부를 수 있다.

예를들면

export const categoryState = atom<Categories>({
  key:"category",
  default: Categories.TO_DO,
})

이런식으로!
실수할 가능성이 낮아지겠지?

enum은 프로그래머를 돕기 위해 일련의 숫자를 문자로 표현해준다.
그래서 TO_DO는 코드상에서 사실 0이고, DOING은 1이고, DONE은 2로 작동하는 것이다.

그래서 저런식으로 수정을 해주다보면

  {category !== Categories.DOING && (
    <button name={Categories.DOING} onClick={onClick}>
    Doing
  </button>
  )}
  {category !== Categories.TO_DO && (
    <button name={Categories.TO_DO} onClick={onClick}>
    To Do
      </button>
  )}
  {category !== Categories.DONE && (
    <button name={Categories.DONE} onClick={onClick}>
    Done
  </button>
  )}

여기서 오류가 생긴다.
name에는 string이 들어가야하는데 number가 들어가는것과 마찬가지라 type error가 생길 것이다.

이 에러는 일단 뒤에 +""를 붙여서 string으로 강제해주면 해결되긴하는데 이게 해결되어도 실행해보면 문제가 있다는 걸 알 수 있다.

0,1,2 값으로 들어가서 저 !== 조건문이 작동을 하지 않는다.
그래서 등록했을 때 3개의 버튼이 모두 화면에 출력된다.



이건 어떻게 해결해야할까?

그냥 간단히

export enum Categories {
  "TO_DO" = "TO_DO",
  "DOING" = "DOING",
  "DONE" = "DONE",
}

이렇게.. 해주면 된다. 값을 강제해주는 것이다.
이러면 0,1,2로 들어가는 것이 아니라 TO_DO, DOING, DONE으로 들어가는 것이 되어 원래대로 잘 나타나게 된다.!
(물론 string이 되었기 때문에 이제 +""는 필요가 없다)





끝.

profile
프론트로 멋쟁이되기 대장정,,

0개의 댓글