seunghyun Note

Recoil & react-hook-from (✈️ travel list) 본문

스터디/REACT

Recoil & react-hook-from (✈️ travel list)

승숭슝현 2024. 3. 13. 01:24

Recoil & react-hook-form

recoilReact-hook-form 을 이용한 간단한(?) to-do list를 복습해보려고 한다.

기본적인 파일구조

├── App.tsx ➡️ 주요 애플리케이션 컴포넌트가 위치
├── atoms.tsx ➡️ Recoil이나 다른 상태 관리 라이브러리를 사용할 때 상태들의 정의를 담당
├── components
│   ├── CreateTravel.tsx ➡️ 여행 항목을 생성하는 데 사용
│   ├── Travel.tsx ➡️ 여행 항목을 표시하고 해당 항목에 대한 작업을 수행
│   └── TravelList.tsx  ➡️ 여행 항목들을 리스트 형태로 표시
└── index.tsx

일단 이 파일 구조를 이해하기 위해서는 원리를 그리면서 접근을 했다. (interface, atom을 일단 제외하고 component의 이동과 사용되는 부분들만 고려)

내가 이해한 파일 구조

컴포넌트를 하나씩 보기 전에 useState와는 다르게 값이 컴포넌트마다 자유롭게 이동시킬 수 있는 Atoms.tsx부터 보자


Atoms.tsx 

많이 신기한 atom의 세계

https://cojjangsh.tistory.com/149

 

Recoil

개인적으로 공부할 것들이 많은 react의 개념들을 정리해보려고 한다. 오늘은 recoil! 공식문서와 블로그를 참고했다. https://recoiljs.org/ko/docs/introduction/getting-started Recoil 시작하기 | Recoil React 애플리

cojjangsh.tistory.com

  • Atom은 상태(state)의 일부를 나타낸다. Atoms 은 어떤 컴포넌트에서나 읽고 쓸 수 있다. Atom의 값을 읽는 컴포넌트들은 암묵적으로 atom을 가져온다.
  • 기본적인 key : "고유식별자" , default 는 []의 형태로 저장한다. (list를 ITravel타입의 상태)
  • Recoil에서는 파생된 상태를 selector를 통해 관리한다.
  • get: 현재 상태에서 파생된 값을 계산하는 로직을 포함한다.
  • travelState를 가져와 이를 카테고리별로 필터링 -> TravelList.tsx에서 const [go, been, favorite] = useRecoilValue(travelSelector);에서 사용된다.
import { atom, selector } from "recoil";

export interface ITravel {
  text: string;
  id: number;
  category: "go" | "been" | "favorite";
}

export const travelState = atom<ITravel[]>({
  key: "travel",
  default: [],
});

export const travelSelector = selector({
  key: "travelSelector",
  get: ({ get }) => {
    const travels = get(travelState);
    return [
      travels.filter((travel) => travel.category === "go"),
      travels.filter((travel) => travel.category === "been"),
      travels.filter((travel) => travel.category === "favorite"),
    ];
  },
});

TravelList

- app에서 가장 먼저 연결되는 컴포넌트이다.

먼저 App.tsx를 보면 파일 내에 TravelList 밖에 없다.

//App.tsx
import TravelList from "./components/TravelList";

export default function App() {
  return (
    <main>
      <TravelList />
    </main>
  );
}
  • useRecoilValue를 통해 travelSelector(위에 atoms)에서 반환된 recoil 상태를 읽어온다.
  • travelSelector에서 가져온 것을 배열의 형태로 저장한다(이건 각자의 방식으로..)
  • CreateTravel 컴포넌트
  • travelSelector에서 가져온 값들을 mapping해서 출력한다. (spread 연산자를 통해 배열 출력)
import { useRecoilValue } from "recoil";
import { travelSelector } from "../atoms";
import Travel from "./Travel";
import CreateTravel from "./CreateTravel";
function TravelList() {
  const [go, been, favorite] = useRecoilValue(travelSelector);
  return (
    <div>
      <h2>내가 가고 싶은 나라들</h2>
      <CreateTravel />
      <ul>
        {go.map((travel) => (
          <Travel key={travel.id} {...travel} />
        ))}
      </ul>
      <h2>내가 가본 나라들</h2>
      <ul>
        {" "}
        {been.map((travel) => (
          <Travel key={travel.id} {...travel} />
        ))}
      </ul>
      <h2>내가 좋아하는 나라들</h2>
      {favorite.map((travel) => (
        <Travel key={travel.id} {...travel} />
      ))}
    </div>
  );
}
export default TravelList;

 


CreateTravel

https://cojjangsh.tistory.com/150

 

React-hook-form

기존에 사용했던 제어 컴포넌트들의 반복되는 코드들과 유효성 검증을 한다면 코드가 점점 길어질 것이다. react에서 컴포넌트 리랜더링이 발생하는 조건 중 하나는 state가 변했을 때이다. 폼에

cojjangsh.tistory.com

  • useForm을 사용한다~! (useForm 은 비교적 재밌고 실용적인듯..)
  • 주석을 한번 보자
import { useForm } from "react-hook-form";
import { useSetRecoilState } from "recoil";
import { travelState } from "../atoms";

interface IForm {
  travel: string;
}

export default function CreateTravel() {
  const setTravels = useSetRecoilState(travelState);
  const {
    register, //register : register function은 많은 요청들을 해결해주고 handler 를 대신해준다.
    handleSubmit, //기존 form에서는 event.preventDefault()를 이용해 form의 제출을 사용했다.
					//useForm()에서는 handleSubmit을 사용한다.
    setValue,		//입력 필드의 값을 변경 -> 여기서는 비운다.
    formState: { errors },	//formState : 전체 양식 상태에 대한 정보가 포함되어 있다. 다양한 옵션들이 있지만 결국 error를 찾기에 적합하다.
  } = useForm<IForm>();
  
  const handleValid = ({ travel }: IForm) => {	//IForm 타입 설정(string)
    setTravels((oldTravels) => [
      { text: travel, id: Date.now(), category: "go" },
      ...oldTravels,
    ]);
    setValue("travel", "");	//필드 값을 지운당
  };

  return (
    <form onSubmit={handleSubmit(handleValid)}>
      <input
        {...register("travel", {
          required: "😠 required!",
        })}
        placeholder="이름"
        type="text"
      />
      <h4>{errors?.travel?.message}</h4>	<!--위에서 formState를 사용해야죠~! error 출력-->
      <button>가자</button>
    </form>
  );
}

Travel.tsx

사실 여기가 난관이였다.

내맘대로 지울수 없고... 버튼이 1개만 나와야하는데 헷갈렸다.  ( 이 부분을 간단하게... 하려면 어떻게 해야할까?? 😅)

 <li>
      <span>{text}</span>
      {category !== "favorite" && (
        <button name="favorite" onClick={onClick}>
          {category === "been" ? "👍" : "✅"}
        </button>
      )}
      {category !== "been" && category !== "go" && (
        <button name="been" onClick={onClick}>
          👎🏻
        </button>
      )}
      {category !== "go" && category !== "favorite" && (
        <button name="go" onClick={onClick}>
          ❌
        </button>
      )}
      {category === "go" && (
        <button name="delete" onClick={onClick}>
          🗑️
        </button>
      )}
    </li>

 

그리고 atoms.. 고맙다 (useState였으면 계속 새롭게 할당하거나 한 컴포넌트에서 끝내야할텐데.. atoms 제대로 사용하고 있는거 같다.)

  • Travel에서 props 구조로 나눠서 위에서 Travel 컴포넌트를 사용했다. 
  • useSetRecoilState를 사용한다(나는 계속 useRecoilState를 사용해서 오류가 나왔다..)
    • travelState 아톰의 상태를 업데이트하는 함수인 setTravel을 가져온다
  • 목록을 지울 delete를 추가해서 id가 다른 것들만 filtering 해준다. ( 그 외는 카테고리에 업데이트)
import React from "react";
import { useSetRecoilState } from "recoil";
import { ITravel, travelState } from "../atoms";

function Travel({ text, category, id }: ITravel) {
  const setTravel = useSetRecoilState(travelState);

  const onClick = (event: React.MouseEvent<HTMLButtonElement>) => {
    const {
      currentTarget: { name },
    } = event;

    if (name === "delete") {
      setTravel((oldTravels) =>
        oldTravels.filter((travel) => travel.id !== id)
      );
    } else {
      setTravel((oldTravels) => {
        const targetIndex = oldTravels.findIndex((travel) => travel.id === id);
        const newTravel = { text, id, category: name as any };
        return [
          ...oldTravels.slice(0, targetIndex),
          newTravel,
          ...oldTravels.slice(targetIndex + 1),
        ];
      });
    }
  };

  return (
    <li>
      <span>{text}</span>
      {category !== "favorite" && (
        <button name="favorite" onClick={onClick}>
          {category === "been" ? "👍" : "✅"}
        </button>
      )}
      {category !== "been" && category !== "go" && (
        <button name="been" onClick={onClick}>
          👎🏻
        </button>
      )}
      {category !== "go" && category !== "favorite" && (
        <button name="go" onClick={onClick}>
          ❌
        </button>
      )}
      {category === "go" && (
        <button name="delete" onClick={onClick}>
          🗑️
        </button>
      )}
    </li>
  );
}

export default Travel;

 

아직 해결은 되지 않아서.. 코드 리뷰를 받고 싶다. 

728x90

'스터디 > REACT' 카테고리의 다른 글

React-hook-form  (2) 2024.03.12
Recoil  (0) 2024.03.11
React 데이터 관리 및 라우팅  (1) 2024.02.24