React JS 마스터클래스(ToDoList 2 - recoil )

짜스의 하루 ·2024년 6월 10일

Recoil 의 다양한 hook


강의를 듣던 도중, Recoil에서 사용하는 다양한 함수들이 있어 정리하러 왔다.
useRecoilValue -> atom으로부터 값을 불러올 때 사용
useSetRecoilState -> atom으로 부터 불러온 값을 수정할 때 사용
useRecoilState -> 값을 불러오거나 수정할 때, 사용할 수 있음

const [value, modifn] = useRecoilState() 으로 사용하며 [ ] 괄호 안에, 사용할 value 값과 값을 수정할 수 있는 modifn 함수가 들어간다.

--> 값을 불러만 오거나, 수정만 할 때에는 useRecoilValue, useSetRecoilState를 사용하면 되지만, 두개 동시에 할 때에는 useRecoilState를 사용하면 될 것 같다!


Recoil를 사용해서 화면에 todoList 띄워보기

자, 먼저 이전에 한번 설명하긴 했지만, react-hook-form을 사용해서 form을 만들고 검증까지 해보자


interface IForm : 이 인터페이스는 할 일 목록의 입력 폼에서 사용할 데이터 구조를 정의한다. 여기서는 toDo라는 문자열 형태의 필드가 포함되어 있다.

useForm<IForm>() : react-hook-form 라이브러리의 useForm 훅을 사용하여 폼 데이터와 관련된 상태 및 메서드를 가져온다. 여기서는 IForm 인터페이스에 정의된 데이터 구조를 사용한다.

--> useForm 훅에서는 register(저장할 키 값 지정) , handleSubmit(유효성 검사 후 실행될 함수), setValue(값을 변경할 때 사용) 등의 변수들이 사용이 된다.

handleValid : 입력 폼이 유효하게 처리되었을 때 호출되는 함수
--> 여기서는 setValue 메서드를 사용하여 toDo 필드를 빈 문자열로 설정하여 입력 폼을 초기화한다.

<form onSubmit={handleSubmit(handleValid)}> : 입력 폼을 렌더링한다. handleSubmit 함수는 입력 폼이 제출될 때 호출되는 함수를 반환하며, 여기서는 handleValid 함수를 전달하여 폼이 유효하게 처리되었을 때 실행된다.

<input {...register('toDO', { required: 'please Write a To Do' })} /> : 입력 필드를 렌더링한다.
register 함수를 사용하여 입력 필드를 toDo 필드와 연결하고, 필드 유효성 검사 규칙을 설정한다.
여기서는 필수 입력 필드임을 나타내는 required 규칙을 설정하고, 필드가 비어있을 때 표시될 오류 메시지를 전달한다.

이제 recoil를 사용해서 toDo를 화면에 뿌려보자

import React, { useState } from 'react';
import { useForm } from 'react-hook-form';
import { atom, useRecoilState } from 'recoil';

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

interface ITodo {
  text: string;
  category: 'TO_DO' | 'DOING' | 'DONE';
  id: number;
}

interface IForm {
  toDo: string;
}

const ToDoList = () => {
  const [toDos, setTodos] = useRecoilState(toDoState);
  const { register, handleSubmit, setValue } = useForm<IForm>();

  const handleValid = ({ toDo }: IForm) => {
    setTodos((oldToDos) => [
      { text: toDo, id: Date.now(), category: 'TO_DO' },
      ...oldToDos,
    ]);
    setValue('toDo', '');
  };

  return (
    <>
      <div>
        <h1>To Dos</h1>
        <hr />
        <form onSubmit={handleSubmit(handleValid)}>
          <input
            {...register('toDo', {
              required: 'Please Write a To Do',
            })}
            placeholder="write a to do"
          />
          <button>Add</button>
        </form>
      </div>
      <ul>
        {toDos.map((toDo) => (
          <li key={toDo.id}>{toDo.text}</li>
        ))}
      </ul>
    </>
  );
};

export default ToDoList;

Recoil이 사용된 부분만 설명해보도록 하겠다

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

toDoState라는 Recoil 아톰을 정의한다.
이 아톰은 ITodo 배열을 상태로 가지며, 초기값은 빈 배열([])이다.
이 아톰은 key 속성을 통해 고유한 식별자를 가지며, 해당 식별자를 사용하여 다른 곳에서 이 아톰에 접근할 수 있다.

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

위 코드는 useRecoilState 훅을 사용하여 toDoState 아톰을 읽고 쓰는 데 사용된다.
이 훅을 사용하면 toDos 상태와 이 상태를 변경할 수 있는 setTodos 함수를 사용할 수 있다.

setTodos((oldToDos) => [
  { text: toDo, id: Date.now(), category: 'TO_DO' },
  ...oldToDos,
]);

setTodos 함수를 사용하여 toDos 상태를 업데이트하고 있다. setTodos 함수는 이전 상태(oldToDos)를 받아와서 새로운 상태를 계산하여 설정한다.

새로운 상태는 기존의 할 일 목록(oldToDos) 에 새로운 할 일을 추가한 배열이다.
새로운 할 일은 객체로 표현되어 있으며, 이 객체는 text, id, category 등의 속성을 가지고 있다.
--> 이 정보들은 ITodo 인터페이스에 정의된 타입 정보를 따르며, 새로운 할 일에 대한 정보를 나타낸다.

따라서 이 코드를 통해 toDos 상태가 업데이트되고, 새로운 할 일이 이전 할 일 목록의 맨 위에 추가되는 것이다.

코드를 실행시켜보면 이렇게 화면에 toDos가 출력되는 것을 확인할 수 있다.


코드 리팩토링

하나의 파일에 있던 코드를 이제 좀 여러 갈래로 나눠보자


form 태그 중심 -> CreateToDos.tsx
ul 태그 중심 -> ToDo.tsx
atom -> atoms.tsx

atoms.tsx

toDoState와 이의 타입을 지정해둔 ITodo를 함께 atoms.tsx 파일로 저장해 두었다.

CreateToDo.tsx

form 태그를 중심으로 코드를 옮겨보았다.

ToDo.tsx

ToDo에서는 추가한 ToDo를 관리하는 폴더이다.
여기서는 text -> 추가한 ToDo와 3가지 버튼을 가지고 있다.
이 버튼은 category에서 정의한 카테고리에 이동할 수 있는 버튼이다.

text를 기본 TodoList 파일에서 props로 받아오고, 이 text의 타입을 지정해 두었다.
여기서 중요하게 생각해야 할 부분은 text는 ITodo의 인터페이스에서 정의를 해두었기 때문에,
{ text } : ITodo 를 통해 text가 어떤 타입인지 알려주어야 한다.

ToDoList.tsx

toDos.map을 통해 전달된 toDo를 화면에 뿌리는 역할을 ToDo.tsx가 하고 있는 것이다.
--> <ToDo/> 를 불러서 사용할 때, 고유한 key 값을 전달해 주어야 하고, props를 전달 해주어야 하지만, 여기서 toDo 는 toDos에서 받아온 값이고, toDos는 toDoState에서 전달된 값이다.

toDoState는 atoms.tsx에서 이미 ITodo 인터페이스를 통해서 타입이 지정되어 있기 때문에, {...toDo}로 ToDo.tsx에서 사용할 값들을 전달해주면 된다.

원래는 text = {toDo.text} id = {toDo.id} category = {toDo.category}이런식으로 ToDo.tsx에 보내줘야 하지만, 위 내용은 이미 toDos -> toDoState에 ITodo 인터페이스를 받고 있기 때문에, {...toDo} 로 정의하면 된다.

😎😎😎😎😎😎😎😎😎😎이해하기 어렵지만 이해하면 된다 😎😎😎😎😎😎😎😎😎😎


Category 버튼 활성화 해보기

먼저 알고 넘어가야 할 부분은 우선 기본값을 'TO_DO'로 주었기 때문에, 버튼이 생성되었을 때, 'TO_DO' 버튼이 생성이 안될 것이다.

const onClick = (event: React.MouseEvent<HTMLButtonElement>) => { ... } : 버튼이 클릭될 때 실행될 이벤트 핸들러 함수
--> event.currentTarget.name을 통해 클릭된 버튼의 name 속성을 가져온다.
이벤트 함수를 정의할 때, 타입 스크립트에게 어떠한 타입의 함수인지 알려주어야 하기 때문에
event : React.MouseEvent<HtmlButtonElement> 임을 알려주었다.

각 버튼의 onClick 속성에는 onClick 함수가 바인딩되어 있다. 이전 코드와 마찬가지로 각 버튼에는 클릭 시 해당 버튼이 나타내는 상태를 나타내는 name(DOING, TO_DO, DONE) 을 name 속성으로 지정한다.

클릭된 버튼의 이름을 로그에 출력하기 위해 onClick 함수를 이벤트 핸들러로 사용하고,
이를 통해 각 버튼이 클릭될 때마다 해당 버튼의 이름이 콘솔에 출력된다.

{category !== 'DOING' && (
          <button name="DOING" onClick={onClick}>
            Doing
          </button>
          //인자를 넘기기 위해 익명함수 사용
        )}

해당 코드 부분은 category가 'DOING'이 아닌 경우에만 'Doing'이라는 내용을 표시하는 버튼을 렌더링한다. 이를 통해 category 값이 'DOING'이 아닐 때만 해당 버튼이 표시되도록 조건을 설정한 것이다.

JavaScript의 논리 연산자인 &&를 사용하여 이를 구현했다. 만약 category가 'DOING'이 아니라면, 뒤의 <button> 요소가 반환된다.
하지만 만약 category가 'DOING'이라면, 첫 번째 조건문이 거짓이 되어 뒤의 <button> 요소는 렌더링되지 않는다.

화면에 출력된 것을 살펴보면, 버튼을 눌렀을 때, 버튼과 이벤트 함수가 제대로 렌더링 되고 있는 것을 확인할 수 있다!


category를 변경해보자

현재 category가 to-do라고 가정해보자. 그럼 이때 Doing 버튼을 눌렀을 때, category 역시 Doing 으로 변경되어야 한다.

category를 어떻게 변경해야 할까 ? --> setXXX 를 사용해서 변경하여야 한다.
이게 무슨 말인가?


category를 변경하기 위해서는 todo.tsx 파일을 수정해야 한다.
이 코드를 살펴보면, useSetRecoilState() 를 통해서 toDoState의 상태를 업데이트하는 setTodos 함수를 가져올 수 있다.

즉, setTodos를 이용해서 category의 값을 변경할 수 있다는 것이다.

그럼 category를 변경하려면, 내가 누른 to do 가 어떤 to do 인지 알아야 한다. 이때 고유의 값인 id를 가져오면 된다.

setTodos((oldToDos) => {
      const targetIndex = oldToDos.findIndex((todo) => todo.id === id)
return oldToDos

};

oldToDos: oldToDos는 할 일(To-Do) 객체들의 배열을 의미한다. 즉 이미 저장되어있었던 ToDos를 불러오는 것이다.

findIndex() 메서드를 통해 index의 값을 찾을 수 있다.

여기서 todo는 oldToDos의 배열의 각 요소를 나타낸다. todo의 id가 ITodo에서 받아온 id와 같은지 판단한 후, 일치한 값을 targetIndex에 저장한다고 생각하면 된다.

--> 이렇게 id의 값을 찾았다

그럼 이제, 특정 인덱스의 기존 할 일 객체(oldToDo) 를 찾고, 이를 기반으로 새로운 할 일 객체(newToDo) 를 생성하는 코드를 작성해봐야 한다.

const oldToDo = oldToDos[targetIndex];
const newToDo = { text, id, category: name };

const oldToDo = oldToDos[targetIndex]; : oldToDos 배열에서 특정 인덱스(targetIndex)의 할 일 객체를 가져와서 oldToDo에 저장한다.

const newToDo = { text, id, category: name }; : 새로운 할 일 객체를 생성하여 newToDo에 저장한다. 이 객체는 기존의 텍스트와 ID를 유지하면서 카테고리만 새롭게 설정한다.

자 이제 console.log(oldToDo, newToDo) 를 찍어보자

text가 3인 todo 의 원래 category는 'TO_DO'이다. 내가 Doing 버튼을 누른 뒤,
text, id는 똑같고, category만 변경된 것을 확인할 수 있다.

그럼 이제, category를 변경해보자

oldTodos = [ 1,2,3,4,5,6,7,8,9] 가 있다고 가정해보자
여기서 내가 3을 '삼'으로 변경해보고 싶다 그럴 경우 어떻게 해야 할까?

그럴 경우에는

const target = 2
oldTodos = [... oldTodos.slice(0, target), '삼', ...oldTodos.slice(target+1)]

이렇게 하면
[1,2,'삼', 4,5,6,7,8,9] 로 변경된다!

그럼, 이제 category를 변경하면 어떻게 해야할까?

위의 방식과 같이 slice() 를 사용하면 된다.
우리는 이미 targetIndex를 통해서 index를 가지고 있다.
--> targetIndex 는 변경하려는 특정 할 일을 식별하기 위해 해당 할 일의 id를 찾기 위한 용도로 사용되기 때문이다!

 return [
        ...oldToDos.slice(0, targetIndex),
        newToDo,
        ...oldToDos.slice(targetIndex + 1),
      ];

return 구문은 새로운 할 일 목록을 반환 :

  • oldToDos.slice(0, targetIndex) : 변경할 할 일 이전의 모든 할 일을 가져온다.
  • newToDo : 변경할 할 일 객체를 추가한다.
  • oldToDos.slice(targetIndex + 1) : 변경할 할 일 이후의 모든 할 일을 가져온다.

이렇게 가져온 할 일들을 하나의 배열로 합쳐서 반환한다.


이렇게 text 1인 todo의 Done 버튼을 누르자, 'TO_DO'였던 카테고리가 'DONE'으로 변경 된 것을 확인할 수 있다!


Recoil - selector

Recoil의 selector는 atom을 읽고 그 값을 기반으로 새로운 상태를 생성할 수 있다.
selector는 atom을 전/후 처리하여 새로운 값을 리턴하거나 기존 atom의 값을 수정할 때 사용한다.
또한, atom의 값이 최신화되면 selector의 값 또한 최신화되어 편하게 사용할 수 있다.
--> (selector 은 state를 가져다가 원하는대로 모습을 변형시킬 수 있는 도구)

자, 어떻게 사용하는지 알아보러가자

지금 현재, category 별로 볼 수 있도록 화면을 구성하려고 하는게 현재 목표이다.

예를 들면 이렇게 화면에 띄우는 것이다!

Recoil의 selector를 활용하여 toDoState를 필터링하는 toDoSelector를 생성하는 코드이다.
이 selector는 toDoState라는 atom으로부터 투두 리스트를 가져와 이를 세 가지 카테고리(‘TO_DO’, ‘DOING’, ‘DONE’)로 나누어 반환하게 된다.

  • key: 'toDoSelector' : selector의 고유한 식별자이다. 프로젝트 내에서 각 selector는 유일한 키를 가져야 한다.

  • get: ({ get }) => { const todos = get(toDoState);... : get은 이 selector의 값을 계산하는 함수
    --> get 함수는 get 인자를 받아 다른 atom이나 selector의 값을 읽을 수 있다. 여기서는 toDoState의 현재 상태를 읽는다.

  • return [] : todos.filter는 주어진 조건에 맞는 요소들만 필터링하여 새 배열로 반환한다.

이렇게 selector를 활ㄹ용해서 atom에 정의한 toDoState를 가져온 뒤, return 값을 filter를 통해 카테고리 별로 구별할 수 있게 되었다.

자, 이제 정의한 toDoSelector를 사용하러 가보자

const [todo, doing, done] = useRecoilValue(toDoSelector) 는 toDoSelector에서 정의한 return 값을 가져오는 것인데,
useRecoilValue(toDoSelector)의 반환 값을 배열 구조 분해 할당을 통해 [todo, doing, done]으로 할당하면, todo -> todos.filter((todo) => todo.category === 'TO_DO') 를 가리키게 된다.

이렇게 배열 분해 할당으로 가져온 값들을

<h2>TO DO</h2>
 <ul>
   {todo.map((toDo) => (
     <ToDo key={toDo.id} {...toDo} />
   ))}
 </ul>
 <hr />

따로 화면에 뿌려줄 수 있다.
todo, doing, done 배열 각각을 map 함수로 순회하여 ToDo 컴포넌트를 통해 각 항목을 렌더링하면서, 위의 예시처럼 화면에 카테고리 별로 나타나는 것을 의미한다.

카테고리 별로 볼 수 있도록 코드 수정!!

내가 지정한 카테고리 별로 볼 수 있도록 수정을 해보도록 하겠다

간단하게, <select> 태그를 사용해 option을 누르면, 해당 todo를 볼 수 있도록 코드를 짜보도록 하겠다.

select 태그에서 선택한 category가 무엇인지, 알 수 있도록, categoryState atom을 하나 생성하도록 하겠다.

export const categoryState = atom({
  key: 'category',
  default: 'TO_DO',
});

다른 컴포넌트에서 categoryState의 값을 읽거나 변경하여 투두 리스트를 카테고리에 따라 필터링할 수 있게 되었다. (기본 값은 우선 'TO_DO'로 설정)

const [category, setCategory] = useRecoilState(categoryState);

useRecoilState() 메소드를 활용해서 값을 불러오고(category), 상태를 수정할 수 있는 함수(setCategory)를 불러왔다.

useRecoilValue(toDoSelector) 를 사용하여 toDoSelector로부터 필터링된 투두 항목을 가져온다.
useRecoilState(categoryState) 를 사용하여 categoryState의 현재 값과 값을 설정하는 함수를 가져온다.
onInput 함수를 사용하여 <select> 태그에서 선택된 카테고리 값을 받아와서 setCategory 함수를 통해 categoryState의 값을 업데이트한다.

이렇게 함으로써 categoryState를 사용하여 현재 선택된 카테고리를 관리하고, 사용자가 드롭다운 메뉴에서 다른 카테고리를 선택할 때마다 이를 업데이트할 수 있다.

return문

  • 드롭다운 메뉴: 현재 선택된 category 값을 표시하고, 변경 시 onInput 함수가 호출된다.
  • CreateToDo 컴포넌트: 새로운 투두 항목을 추가할 수 있는 폼을 렌더링한다.
  • todos?.map : 현재 카테고리에 따라 필터링된 투두 항목들을 ToDo 컴포넌트로 렌더링합니다. 각 항목은 id를 key로 사용하여 고유성을 보장한다.

Enum

자바 공부를 할 때에도 enum을 본 적이 있다.

enum(열거형) 은 다른 언어에서와 마찬가지로 연관된 상수들의 집합을 정의하는 TypeScript의 enum과 유사한 개념이다.
--> 열거형을 사용하면 일련의 연속된 값을 가지는 상수들을 정의할 수 있다.


export enum Categoryies {}로 Category들을 정의해 두었다.
이를 다른 곳에서 사용할 수 있도록 export해두었고,

ITodo(toDoState의 타입을 지정해둔 interface)의 category에, categoryState의 타입에도 각각 가져와서 사용할 수 있으므로
--> 코드의 깔끔함, 오류를 방지할 수 있게 된다.

이를 어디서 사용할 수 있을까?


이렇게 사용하면서, 문자열의 오타를 막을 수 있다.

profile
2024. 01. 02 ~ 백앤드 공부 시작, 2024. 04.01 ~ 프론트 공부 시작

0개의 댓글