TIL_63_Recoil

JIEUN·2021년 4월 14일
0
post-thumbnail

Recoil: 왕위를 계승하는 중입니다 (새로운 React 상태 관리 라이브러리)

유진님이 공유해주신 영상을 보며 정리한 내용을 여기에 기록해보고자 한다.

  • 리코일
    새로운 react 상태 관리 라이브러리.

  • 리액트 상태 관리의 역사.
    한동안은 기본적인 context, prop, state가
    유일한 상태를 관리하는 방법이었다.

  • Flux Architecture
    액션 -> 디스패쳐 -> 스토어 -> 뷰 -> 액션 -> 디스패쳐

  • 리덕스가 등장 2015
    플럭스 아키텍쳐를 만족시키면서도 이해하기 쉬움.
    부족한 점: 기능이 단순, 기본적인 것만 가능. 비동기서버 통신
    그럼에도 리덕스 사가와 같은 다양한 써드파티의 도움을 받아 왕위를 유지하고 있었음.

  • 경쟁자 몹액스 등장
    데이터의 흐름이 일방통행이라는 건 같지만,
    상태 클래스가 중심.
    상태 클래스와 액션도 실행. 값의 변경을 통제.

리덕스 몹액스 둘 다 리액트에 종속된 것이 아닌 독립된 상태관리 라이브러리.

  • 리코일의 탄생 2020
    다른 상태 관리 라이브러리는 범용 라이브러리인 반면에 리코일은 리액트를 기반으로 작성된 전용 상태관리 라이브러리.

Context API는 데이터마다 Context를 만들고 Provider를 제공해야 함.
사용해야할 데이터가 많으면 둘 중 하나가 되어야 함.
모든 전역상태를 한 컨택스트에 몰아서 사용하게 되면 관련된 컴퍼넌트가 너무 자주 랜더링이 되기 때문에 무거워짐. 데이터마다 컨택스트를 따로따로 만들어주게 되면 너무 많은 프로바이더가 컴퍼넌트 트리에 나타나게 됨. 프로바이더를 계속 중첩해서 공급하게 되는 것도 굉장히 번거로운 일임.

(Data-flow Graph)

예제1.

아톰: 리코일에서 상태 데이터 조각
셀렉터: 1. 아톰에서 파생된 데이터 조각.
2. 데이터를 반환하는 순수 함수.

아톰과는 다르게 셀렉터를 선언할 때는 기본값이 없다. 파생데이터기 때문.

App.jsx 파일에 를 추가.

  import { RecoilRoot } from "recoil";

//하위에 있는 모든 리코일 데이터의 루트가 됨.
//내부적으로는 리액트 컨택스트 api를 사용하고 있고 스토어를 선언해서 컨택스트로 내려주는 역할.
//스토어를 직접 만들어주지 않아도 됨. RecoilRoot를 선언해주는 것만으로 스토어가 자동 생성.
//리코일 루트는 중첩해서 사용할 수 있으며, 중첩하게되면 아톰과 셀렉터는 가장 가까운 리코일 루트에 선언된 스토어를 사용한다.
//데이터를 저장하고 여러 컴퍼넌트에서 공유하려면 리코일 루트로 감싸줘야 한다.

Counter.jsx 파일을 만들고 빈 Counter 컴퍼넌트를 작성
App.jsx 파일에 Counter 컴퍼넌트를 추가.

//Counter.jsx
import React from "react";
export default Function Counter() {
  return (
    <div className="counter">
    </div>
   );
}
//App.jsx
import Counter from "./Counter"

Counter.jsx 에 count 상태 아톰을 선언.

//Counter.jsx
import React from "react";
import { atom } from "recoil";

const countState = atom({
  key: 'countState',
  default: 0
});

export default function Counter() {
  return (
    <div className="counter">
    </div>
  );
 }

count 아톰을 사용하기 위해 useRecoilState 훅을 호출.

//Counter.jsx
import React from "react";
import { atom, useRecoilState } from "recoil";

const countState = atom({
  key: 'countState',
  default: 0
});

export default function Counter() {
  const [count, setCount ] = useRecoilState(countState);

  return (
  <div className="counter">
  </div>
 );
}

//훅 실행의 반환값을 배열로 받으면, 첫번째 값은 상태값, 두번째 값은 상태값을 설정하는 함수가 됨.

화면에 카운터를 표시하고 카운터를 증가/감소하는 버튼을 추가.

//Counter.jsx
import React from 'react';
import { atom, useRecoilState } from 'recoil';

const countState = atom({
  key: 'countState',
  default: 0
 });

export default function Counter() {
  const [count, setCount] = useRecoilState(countState);

  return (
    <div className="counter">
      Count: {count}
      <br />
      <button onClick={() => setCount(count - 1)}>1 감소</button>
      <button onClick={() => setCount (count + 1)}>1 증가</button>
     </div>
   );
  }

--리덕스와 비교했을 때 코드의 양이 다르다.
--리덕스는 기본적으로 액션타입을 상수로 선언하고 액션 생성자도 만들어줘야하고 리듀서도 작성해줘야 해서 코드의 양이 많을 수 밖에 없음.
--리코일이 좀 더 직관적.
--리덕스는 상태값을 설정할 때도 디스패쳐와 액션 생성자를 따로따로 사용해줘야해서 덜 직관적.

예제2.

-심플 카운터 + 홀짝 셀렉터
-셀렉터에 대해 복습
1. 아톰에서 파생된 데이터 조각
2. 데이터를 반환하는 순수 함수

oddEven 셀렉터를 countState 아톰 아래에 작성.

//Counter.jsx
import React from 'react';
import { atom, selector, useRecoilState } from 'recoil';

const countState = atom({
  key: 'counstState',
  default: 0
 )}

const oddEvenState = selector({
  key: 'oddEvenState',
  get: ({get}) => {
    const count = get(countState);
    return count % 2 ? '홀 : '짝';
  }
 });

//아톰과는 다르게 셀렉터는 상태 키와 베타 함수를 전달해서 사용.
//만약에 선택자가 값을 설정하는 역할도 한다면,
setter 역할을 하는 set함수도 전달할 수 있다.
//위 예제에서는 데이터를 셀렉해서 데이터를 읽기만 할 것 이므로 셋 함수는 따로 전달하지 않음.

Read-only 데이터에 사용하는 Hook이 따로 있다.
atom 과 selector는 구분없이 동일한 Hooks를 사용해서 다룬다.

import React from 'react';
import { atom, selector, useRecoilState, useRecoilValue } from 'recoil';
export default function Counter() {
  const [count, setCount ] = useRecoilState(countState);
  const oddEven = useRecoilValue(oddEvenState);

  return (
    <div className="counter">
      Count: {count} / 홀짝: {oddEven}
      <br />
      <button onClick={() => setCount(count - 1)}>1 감소</button>
      <button onClick={() => setCount(count + 1)}>1 증가</button>
     </div>
    );
  }

//oddEven은 읽기만 하는 데이터이므로 읽기전용 데이터를 사용하는 useRecoilValue라는 다른 훅을 사용.
//위의 훅은 아톰의 값을 읽기만 할 때도 사용할 수 있음.
//리코일에서 아톰과 셀렉터는 구분없이 동일한 훅을 사용해서 다룬다.
//덕분에 코드가 더 단순해진다.
//파생된 데이터를 만들어내는 것도 굉장히 간단.

예제3.

-서버 API 연동
-fetch() 와 React.Suspense 기능을 사용.
-비동기 데이터를 다루는 것 만큼은 리코일의 강점!

<RecoilRoot><App>에 추가.
state.js 파일을 만들고 randomCat이라는 selector를 만들었다.

//state.js
import { selector } from 'recoil';

export const randomCat = selector({
  key: 'randomCat',
  get: async () => {
    const response = await fecth('http://aws.random.cat/meow');
    const data = await response.json();

    return data.file;
   }
  });

//get 함수가 아까와는 다르게 async 함수가 쓰였음.
//randomCat은 외부 서버에서 데이터를 가져온 후 반환하는 async 함수를 getter로 사용.
//RandomCat 컴포넌트는 셀렉터를 통해 이미지 주소를 불러와서 표시. -> 에러가 나는게 정상임.

//리엑트 서스펜스는 컴퍼넌트를 완전히 렌더링이 할 수 있게 되기까지 렌더링을 멈춰두는 기능.

//리코일은 리액트만을 위해서 작성됨.
//그래서 리액트 전용이라서 리액트 프로젝트에 사용하기에 간단하고 좋음!

에러를 해결해보자.
RandomCat 컴포넌트를 React.Suspense로 감싸준다.
fallback prop에는 기다리는 동안 보여줄 컴퍼넌트를 전달.

export default function App() {
  return (
    <div className="App">
      <h1>Random Cat</h1>
      <p>페이지를 새고 고침할 때마다 랜덤한 고양이 사진을 보여줍니다!</p>
      <RecoilRoot>
        <React.Suspense fallback={null}>
          <RandomCat />
         </React.Suspense>
       </RcoilRoot>
     </div>
  );
 }     

//리액트 서스펜스는 컴퍼넌트가 완전히 렌더링이 될 수 있을 때까지 기다리는 역할. 랜더링을 하지 않고 기다림.
//셀렉터에서 값을 가져오는 동안 화면에 폴백 프롭에 전달된 컴퍼넌트를 보여줌.
//간단하게 비동기상태 처리가 가능.
//동기식 데이터나 비동기식 데이터나 거의 같은 방식으로 다룰 수 있다.

리액트 서스펜스 없이 에러를 해결해보자.
위의 코드에서 리액트 서스펜스 부분을 지운다.

export default function App() {
  return (
    <div className="App">
      <h1>Random Cat</h1>
      <p>페이지를 새고 고침할 때마다 랜덤한 고양이 사진을 보여줍니다!</p>
      <RecoilRoot>
       <RandomCat />
       </RcoilRoot>
     </div>
  );
 }     
  • useRecoilValue을 useRecoilValueLoadable로 바꾼다.
  • use...Loadable 훅은 Loadable 객체를 반환.
  • Loadable 객체는 ...
    .state 프로퍼티를 통해 상태를 확인
    .contents 프로퍼티를 통해 실제 콘텐츠를 가져올 수 있다.
  • Loadable.state는 "hasValue", "loading", "hasError" 셋 중 하나 문자열 값

서버 API 연동

export default function RandomCat() {
  const photoUrlLoadable = useRecoilValueLoadable(randomCat);
  let content = null;

  switch (photoUrlLoadable.state) {
    case 'hasValue':
      content = <img src={photoUrlLoadable.contents} alt="random cat" />;
      break;
    case 'hasError':
      content = "데이터를 불러오는 중 에러가 발생했습니다.';
      break;
    case 'loading':
    default:
      content = '...';
   }
  return <div className="random-cat">{content}</div>;
}

//서스펜스를 사용할 때는 에러를 처리하려면 에러 바운더리를 사용하거나 셀렉터에서 아예 에러가 발생하지 않도록 해줬어야 하는데 로더블을 사용한 덕분에 에러도 함께 한꺼번에 처리할 수 있다.
//리덕스에서는 비동기 처리를 하려면 아예 써드파티 라이브러리가 필요하다.

예제 4.

살짝 더 복잡한 서버 API 연동
-검색어를 입력하면 해당 애니메이션의 목록을 표시하는 애플리케이션

  • 검색어에 따른 결과를 캐시
  • 컴퍼넌트와 스테이트의 폴더를 분리

//스테이트의 종류가 많아진다면, 여러개의 파일로도 구성할 수 있다. 혹은 파일로 구성하는 것도 많아진다면, 따로 폴더를 둬서 사용자 관련 데이터는 유저라는 폴더에 둘 수도 있을 것이고 결제 관련 데이터는 페이먼트 쪽 폴더에 둘 수도 있을 것이다.

export default function AnimeList() {
  const list = useRecoilValue(animeList);

  return (
    <div className="anime-list">
      {list.map((item) => (
        <AnimeItem key={item.mal_id} {...item} />
    ))}
     </div>
    );
  }

//<AnimeList>는 비동기 데이터를 가져오는 animeList셀렉터를 사용.
//개별 아이템은 stateless 컴퍼넌트인 <AnimeItem>이 렌더링.

//App.js
export default function App() {
  return (
    <div className="App">
      <h1>애니메이션 검색</h1>
      <p>영어 키워드를 입력한 후 엔터를 누르세요</p>
      <RecoilRoot>
        <SearchBox />
        <Suspense fallback={<div>Loading...</div>}>
        <AnimeList />
       </Suspense>
      </RecoilRoot>
    </div>
  );
 }

//에러를 방지하기 위해서 서스펜스로 아니메리스트를 감싸준 것을 알 수 있다.
//데이터를 로딩하는 동안에는 코드에서 설정한 Loading... 메세지가 화면에 보일 것.
//서치박스라는 컴퍼넌트는 검색어를 입력받아서 검색어 아톰을 설정하는 역할을 함.

//state/index.js
import {atom, selector} from 'recoil';

export const keywordState = atom({
  key: 'keywordState',
  default: ''
 });

export const animeList = selector({
  key: 'animeList',
  get: async ({ get }) => {
    const keyword = get(keywordState);
    if (!keyword || keyword.length < 3) {
    return [];
   }

const response = await fecth(
  `http://api.jikan.moe/v3/search/anime?q=${keyword}&rated=pg13&page=1`
  };
  const data = await response.json();

  return data.results;
  }
 });

//아톰과 셀렉터를 각각 하나씩 선언했다.
//키워드스테이트 아톰은 검색어를 의미하는 데이터.
//애니메이션 목록을 가져오는 것은 그 아래에 있는 셀렉터가 담당하는데 키워드의 값을 가져와서 서버의 api에 요청. 그리고 서버에서 받은 데이터를 적당히 가공해서 반환하고 있다.
//이미 이 상태만으로도 캐시가 됨.

get(keywordState)

//사용한 아톰에 자동으로 의존성이 걸림.
//두 가지 효과:
1. 키워드가 변경될 때마다 자동으로 파생된 셀렉터들의 값도 변하게 됨.
2. 의존성에 걸린 값이 모두 같으면 캐시된 동일한 결과를 반환하게 됨.
(한번 검색한 결과는 서버에 다시 요청하지 않고 캐시에서 바로 가지고와서 더 빠르게 검색이 되는 것)

리코일의 장점
-리액트와 굉장히 잘 연계가 된다.
-사용법이 직관적이고 단순.

리코일의 단점
-Hooks를 통해서만 사용할 수 있다.
-프로덕션 레벨에서 사용하기엔 아직 약간의 부담이 있다.
-현재는 디버깅 도구의 지원이 미미.

0개의 댓글