히치키치

Recoil : selector에서 비동기 로직 처리 본문

FE

Recoil : selector에서 비동기 로직 처리

히치키치 2022. 7. 8. 03:23

다음의 글을 참고하며 공부한 내용

https://velog.io/@juno7803/Recoil-Recoil-200-%ED%99%9C%EC%9A%A9%ED%95%98%EA%B8%B0#4%EF%B8%8F%E2%83%A3---selector-%EC%99%80-%EB%B9%84%EB%8F%99%EA%B8%B0-%EC%B2%98%EB%A6%ACsuspense-loadable

 

[Recoil] Recoil 200% 활용하기

아무리 구글링해도 Recoil 기본 예제밖에 나오지 않아 직접 작성한 Recoil 200% 활용법 🙃

velog.io

 

다음의 api 사용함

https://www.themoviedb.org/documentation/api

 

API Overview — The Movie Database (TMDB)

API Overview Our API is available for everyone to use. A TMDB user account is required to request an API key. Professional users are approved on a per application basis. As always, you must attribute TMDB as the source of your data. Please be sure to read

www.themoviedb.org

💙 비동기 로직에서 주목해야 할 Selector의 특성

1. selector는 순수함수이다

순수함수

: same Input -> same Output
: give Function as Output according to Input

2. selector는 RecoilValueReadOnly 객체임

RecoilValueReadOnly 객체

: readOnly 하여 return 값만 가짐
: 값을 set 할 수 없음

3. selector의 Type 둘러보기

key 프로퍼티

: string
: selector를 구분하는 Id

get 프로퍼티

인자 : {get}

  • get : GetRecoilValue

반환

  • T
  • Promise<T>
  • RecoilValue<T>

비동기적 역할

  • api call로 받아온 data 반환

set 프로퍼티 (optional)

첫번째 인자 : {get | set | reset}

  • get : GetRecoilValue
  • set : SetRecoilState
  • reset : ResetRecoilState

두번째 인자 : newValue

  • newValue : T 또는 Default Value

반환

  • void

비동기적 역할

  • wrtiable한 state 값을 변경(set)할 수 있는 함수 반환
  • 자기 자신 selector를 set하지 않음 : 무한 루프 발생
  • 다른 writable한 atom을 set함 : 에러 발생 없음
  • 애초에 selector는 read-only한 RecoilValue만 가짐

dangerouslyAllowMutability 프로퍼티 (optional)

: boolean

정리하기 : 예제 코드 분석

/state.js
import { atom, selector } from "recoil";
import { serverAxios } from "./apis";

export const movieState = atom({
  key: "movieState",
  default: [],
});

export const getTrending = selector({
  key: "trending/get",
  get: async ({ get }) => {
    try {
      const {
        data: { results },
      } = await serverAxios.get(
        `/trending/all/week?api_key=${process.env.REACT_APP_API_KEY}&language=en-US`
      );
      return results;
    } catch (err) {
      throw err;
    }
  },
  set: ({ set }, newValue) => {
    set(movieState, newValue);
  },
});

movieState : atom

  • writable한 state 가짐

getTrending : selector

  • set은 selector의 값 수정하는 함수 정의 X, 다른 atom의 writable한 state (movieState) 수정하는 함수 정의 O
/movie.js
import { getTrending } from "./state";
import { useRecoilState } from "recoil";

const Movie = () => {
  const [movies, setMovies] = useRecoilState(getTrending);
  console.log(movies);

  return (
    <>
      {movies.map((movie, idx) => (
        <div key={idx}>
          <h1>{movie.original_title}</h1>
          <p>{movie.overview}</p>
        </div>
      ))}
    </>
  );
};

export default Movie;

useRecoilState(getTrending)

  • get 프로퍼티로 api call의 반환 결과가 movies에 대입됨
  • set 프로퍼티인 set(movieState, newValue)movieState atom의 state가 get 프로퍼티로 받아온 새로운 값으로 write됨

정리

selector의 역할
get : 다른 selector/atom 값 가져오기
set : get한 값을 바탕으로 다른 atom의 state를 modify함

💙 비동기 상태 (load, success, fail)에 대한 처리

비동기 상태에 따른 처리를 하지 않아 발생한 에러

그러나 막상 비동기 api로 받아온 값을 콘솔에 출력하면 다음의 에러가 발생함. 비동기 작업은 컴포넌트가 unmount되어도 실행됨.즉 컴포넌트가 unmount되면 setState 함수가 실행되지 않도록 해야함. 이를 비동기 처리 중 (= 컴포넌트 unmount됨 / 랜더링 할 데이터 도착 전 / Loading ) 보여줄 UI를 따로 보여주며 에러 해결하도록 하자

1. React의 Suspense의 fallback props 이용

Suspense 컴포넌트로 랜더링할 컴포넌트인 <Movies/>를 감싸고 fallback이란 props에 loading 시 보여줄 UI를 부여함

/App.js
import React, { Suspense } from "react";
import { RecoilRoot } from "recoil";
import Movie from "./movie";

function App() {
  return (
    <RecoilRoot>
      <Suspense fallback={<div>Loading...</div>}>
        <Movie />
      </Suspense>
    </RecoilRoot>
  );
}

export default App;

2. Recoil의 Loadable 이용

Recoil의 Loadable인 useRecoilValueLoadable 사용

Loadable 이란?

  • atom/selector의 현재 상태를 나타내는 객체로 statecontents 프로퍼티를 가짐
  • state : hasValue, hasError, loading 상태 가짐
  • contents : state 값에 따라 각각 value, Error 객체, Promise 가짐
/movie.js
import { getTrending } from "./state";
import { useRecoilValueLoadable } from "recoil";

const Movie = () => {
  const movieLoadble = useRecoilValueLoadable(getTrending);
  console.log(movieLoadble);

  return <></>;
};

export default Movie;

useRecoilValueLoadable 객체 반환 결과

따라서 useRecoilValueLoadable 객체의 state 상태에 따라 비동기 처리하는 UI 코드를 추가해보자

/movie.js
import { getTrending } from "./state";
import { useRecoilValueLoadable } from "recoil";

const Movie = () => {
  const movieLoadble = useRecoilValueLoadable(getTrending);
  //console.log(movieLoadble);

  switch (movieLoadble.state) {
    case "hasValue":
      return (
        <>
          {movieLoadble.contents.map((movie, idx) => (
            <div key={idx}>
              <h1>{movie.name}</h1>
              <p>{movie.overview}</p>
            </div>
          ))}
        </>
      );
    case "Loading":
      return <div>Loading...</div>;
    case "hasError":
      throw movieLoadble.contents;
  }
};

export default Movie;

💙 selectorFamily : 동적인 라우터 URL Query 따른 api 호출

영화 목록을 보일 Movie 컴포넌트와 id에 따른 특정 영화 세부 정보를 보일 MovieDetail 컴포넌트를 연결하는 라우터 작성

 

브라우저 라우터 설정하기

/index.js
import React from "react";
import ReactDOM from "react-dom/client";
import App from "./App";
import { BrowserRouter } from "react-router-dom";

const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(
  <BrowserRouter>
    <App />
  </BrowserRouter>
);

라우터 작성

/App.js
import React from "react";
import { RecoilRoot } from "recoil";
import Movie from "./movie";
import MovieDetail from "./movieDetail";
import { Routes, Route } from "react-router-dom";

const App = () => {
  return (
    <RecoilRoot>
      <Routes>
        <Route exact path="/" element={<Movie />} />
        <Route path="/:id" element={<MovieDetail />} />
      </Routes>
    </RecoilRoot>
  );
};

export default App;

영화 목록에서 특정 영화 클릭 해당 영화 id와 함께 세부 정보 페이지로 넘어갈 Link 설정

/movie.js
import { getTrending } from "./state";
import { useRecoilValueLoadable } from "recoil";
import { Link } from "react-router-dom";

const Movie = () => {
  const movieLoadble = useRecoilValueLoadable(getTrending);
  console.log(movieLoadble);

  switch (movieLoadble.state) {
    case "hasValue":
      return (
        <>
          {movieLoadble.contents.map((movie, idx) => (
            <Link to={`/${movie.id}`}>
              <div key={idx}>
                <h1>{movie.original_title}</h1>
                <p>{movie.overview}</p>
              </div>
            </Link>
          ))}
        </>
      );
    case "Loading":
      return <div>Loading...</div>;
    case "hasError":
      throw movieLoadble.contents;
  }
};

export default Movie;

동적으로 바뀌는 영화 id로 api를 호출해 영화 세부정보 받아오는 SelectorFamily 생성

  • key 프로퍼티 : string, selecotrFamily를 구분하는 unique Id
  • get 프로퍼티 : 인자로 받아온 값(id)을 기반으로 api를 호출하고 그 결과를 반환함
/state.js
import { atom, selector, selectorFamily } from "recoil";
import { serverAxios } from "./apis";

(..생략..)


export const getMovieDetail = selectorFamily({
  key: "movieDetail/get",
  get: (movieId) => async () => {
    if (!movieId) return "";
    const { data } = await serverAxios.get(
      `/movie/${movieId}?api_key=${process.env.REACT_APP_API_KEY}`
    );
    console.log(data);
    return data;
  },
});

SelectorFamily의 Value를 get하여 특정 영화 세부 정보 페이지 생성

/movieDetail.js
import React from "react";
import { useRecoilValue } from "recoil";
import { useParams, useNavigate } from "react-router-dom";
import { getMovieDetail } from "./state";

const MovieDetail = () => {
  const { id } = useParams();
  const navigate = useNavigate();
  const movie = useRecoilValue(getMovieDetail(id));
  //console.log(movie);
  const _handleBackBtn = () => {
    navigate("/");
  };
  return (
    <div>
      <h1>{movie.original_title}</h1>
      <p>{movie.release_date}</p>
      <p>run time : {movie.runtime}</p>
      <p>{movie.overview}</p>
      <button onClick={_handleBackBtn}>back</button>
    </div>
  );
};

export default MovieDetail;

 

Comments