React 상태 관련 라이브러리 - Recoil(2)

jiny·2022년 9월 2일
0

React

목록 보기
4/9
post-thumbnail

목차

  1. Selector
  2. Selector를 이용한 비동기 처리

Selector

Selector?

A selector represents a piece of derived state

  • 파생된 상태의 일부를 나타낸다.
  • 기존의 atoms들을 가져와 새로운 값으로 변형하여 리턴할 수 있음

Selector Type

function selector<T>({
  key: string,

  get: ({
    get: GetRecoilValue
  }) => T | Promise<T> | RecoilValue<T>,

  set?: (
    {
      get: GetRecoilValue,
      set: SetRecoilState,
      reset: ResetRecoilState,
    },
    newValue: T | DefaultValue,
  ) => void,

  dangerouslyAllowMutability?: boolean,
})

1. key

  • Selector를 구분할 수 있는 유일한 id, 즉 key 값을 의미

2. get

  • derived state(파생된 상태)를 return
  • 예시에선 atom(numberState)의 state를 가져와 변형 된 값을 return

3. set

  • writeable 한 state 값을 변경할 수 있는 함수를 return
  • 만약, 자기 자신 selector를 set 하려고 하는 경우, 스스로를 해당 set 함수에서 set 하게 됨 -> 무한 루프 발생
  • 그래서 반드시 다른 Selector와 Atom을 set 하는 로직을 구성
  • Selector는 애초에 read-only한 return 값(RecoilValue)만 가지기 때문에 set으로는 writeable한 atom의 RecoilState만 설정 가능
set: ({set}, newValue) =>{ set(hourState, newValue) } // incorrect : cannot align itself
set: ({set}, newValue) =>{ set(minuteState, newValue) } // correct : can align another upstream atom that is writeable RecoilState
const [minute, setMinute] = useRecoilState(minuteState);

=> 공식문서에 나와 있는 설명으로 이해하기 힘들어서 직접 해보기로 함

Selector 활용

store.ts

import { atom, selector } from "recoil";

export const minuteState = atom({
    key : 'minuteState',
    default : 0
})

export const hourState = selector({
    key : 'hourState',
    get : ({get}) => {
        let prev = get(minuteState);
        return prev / 60
    },
    set : ({set}, newValue) => {
        const hour = newValue as number * 60;
        set(minuteState, hour);
    }
});
  • get은 기존 state를 가져와 state에서 60을 나눈 값 리턴
  • set은 기존 hour을 60으로 곱하여 minute로 변형한 값을 set

home.tsx

import React from "react";
import { useRecoilState } from "recoil"
import { hourState, minuteState } from "src/utils/store";

export default function Home () {

    const [minute, setMinute] = useRecoilState(minuteState);
    const handleSetMinute = (e:React.ChangeEvent<HTMLInputElement>) => {
        setMinute(+e.currentTarget.value);
    }
    
    const [hour, setHour] = useRecoilState(hourState);
	const handleSetHour = (e:React.ChangeEvent<HTMLInputElement>) => {
    	setHour(+e.currentTarget.value);
    }
    
    return (
        <>
            <input
                value={minute}
                onChange={handleSetMinute} 
                type="number"/>
            <input 
                value={hour}
                onChange={handleSetHour}
                type="number"/>
        </>
    )
}
  • selector는 useRecoilState나 useRecoilValue(Read-Only)를 통해 확인 가능
  • minute input에서 사용자가 입력할 경우 hour input에서 minute에 영향 받아 60으로 나눠진 값이 보여지게 됨 (get의 영향)
  • 반대로 hour input에서 사용자가 입력할 경우 60으로 곱해진 값이 minute input에서 보여지게 됨 (set의 영향)

Selector를 이용한 비동기 처리

  • selector를 통해 api 통신을 처리 후 그 selector를 구독하는 모든 컴포넌트에서 selector를 가져와 response data를 받아오는 것이 가능
  • 캐싱 기능도 제공

기존 코드

Trello.tsx

import { useEffect, useState } from "react"
import { client, refreshApi } from "../utils/api/api";
import { useRecoilState } from 'recoil';

interface IResponse {
    status: string;
    message : string;
    email : string;
}

interface IToken {
    status: string;
    message : string;
    accessToken : string;
}

export default function Trello() {
	const [loading, isLoading] = useState(true);
    const [userInfo, setUserInfo] = useRecoilState('');
    
    useEffect(() => {
            client.get("/api/refresh")
            .then(res => {
                const data = res.data as IToken;
                client.defaults.headers.common["Authorization"] = `Bearer ${data.accessToken}`
                client.get("/api/user")
                .then(res => {
                    let data = res.data as IResponse
                    setUserInfo(data.email);
                })
                .catch(err => {
                    console.log(err);
                })
            })
    },[userInfo]);

    return (
      { loading ? (<div>is Loading...</div>) : 
          (<>
          <div>{userInfo}</div>
          <button
              onClick={handleUserLogout}>logout</button>
          </>)
      }
   )
}
  • 다음과 같이 작성해도 큰 문제는 없음
  • atoms의 상태가 변할 때 마다 각 컴포넌트에서 이렇게 따로 비동기 처리를 한다면, 같은 atom의 상태가 변할 때 마다 각 컴포넌트에서 이렇게 따로 비동기 처리를 해준다면, 같은 atom을 구독하고 있던 컴포넌트들은 알아서 re-render 되기 때문
  • 하지만, selector 에서는 이러한 로직을 한번에 처리해 줄 수 있는 동시에 캐싱 기능이 있어, 이미 받아왔던 정보에 대해서는 빠른 피드백이 가능해 성능적으로 유리

비동기 코드 분리 -> Selector

client.ts

import axios from "axios";

export const client = axios.create({
    baseURL : "http://localhost:4000",
    withCredentials : true,
})

store.ts

import { atom, selector } from "recoil";
import { client } from "./api/api";

interface IToken {
    status: string;
    message : string;
    accessToken : string;
}

interface IResponse {
    status: string;
    message : string;
    email : string;
}

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

export const refreshSelector = selector({
    key : 'refresh/user',
    get : async ({get}) => {
        let user = get(userInfo);
        await client.get('/api/refresh')
        .then( async (res) => {
            const data = res.data as IToken;
            client.defaults.headers.common["Authorization"] = `Bearer ${data.accessToken}`
            await client.get("/api/user")
            .then(res => {
                let data = res.data as IResponse
                user = data["email"];
            })
            .catch(err => {
                console.log(err);
            })
        });
        return user
    }
})
  • 비동기 처리하는 로직을 selector로 분리
  • 기존 userInfo를 가져와 api logic을 수행 후 userInfo에 api response를 변경 후 return

Selector로 분리할 경우 발생하는 문제

Trello.tsx (Selector로 개선)

import { useRecoilValue } from 'recoil';

const Trello = () => {

const userInfo = useRecoilValue(refreshSelector);

return (
  <>
   <div>{userInfo}</div>
  </>
});

export default Trello;

  • 하지만 비동기 처리를 selector로 할 경우 error가 발생
  • 이는 비동기를 처리하고 있을 경우(= loading state인 경우) 보여줄 fallback UI가 없다는 뜻의 에러 메시지

해결 1 : React -> Suspense

import React, { Suspense } from 'react';
import ReactDOM from 'react-dom/client';
import { RecoilRoot } from 'recoil';
import App from './App';

const root = ReactDOM.createRoot(
  document.getElementById('root') as HTMLElement
);
root.render(
  <React.StrictMode>
    <RecoilRoot>
      <Suspense fallback={<div>Loading ...</div>}>
        <App />
      </Suspense>
    </RecoilRoot>
  </React.StrictMode>
);
  • 상위 컴포넌트를 Suspense로 감싼 후 fallback prop에 loading UI를 작성 시 문제 해결이 가능

해결 2 : Recoil -> useRecoilValueLoadable

Trello.tsx

import { useRecoilValueLoadable } from "recoil";
import { client, refreshApi } from "../utils/api/api";
import { refreshSelector } from "../utils/store";

export default function Trello() {
    const handleUserLogout = () => {
        client.get("/api/logout")
        .then(res => {
            window.location.replace('/');
        })
    }

    const refreshLoadable = useRecoilValueLoadable(refreshSelector);
  
    switch(refreshLoadable.state) {
        case 'hasValue' :
            return (
                <>
                    <div>{refreshLoadable.contents}</div>
                    <button
                        onClick={handleUserLogout}
                    >
                    logout
                    </button>
                </>
            );
        case 'loading' : 
            return <div>is Loading...</div>
        case 'hasError' : 
            throw refreshLoadable.contents  
    }
}

Loadable

  • atom 이나 selector의 현재 상태를 나타내는 객체

ValueLoadable - 비동기 처리가 완료되었을 경우의 객체 -> value 리턴
LoadingLoadable - 비동기 처리 중 일경우의 객체 -> promise 리턴
ErrorLoadable - 비동기 처리 중 에러가 발생했을 경우의 객체 -> error 리턴

const refreshLoadable = useRecoilValueLoadable(refreshSelector);

와 같이 접근할 수 있음

  • Loadable은 statecontents 라는 프로퍼티를 가짐

Loadable 객체

  • state - hasValue(ValueLoadable), hasError(ErrorLoadable), loading(LoadingLoadable) atom 이나 selector의 상태를 의미하며, 앞의 3가지의 상태를 가짐
  • contents - atoms이나 contents의 값을 나타냄, hasValue일 경우 value를, hasError 일 경우 Error 객체, loading일 땐 Promise를 가짐

실행 결과

  • 데이터를 잘 가져온 것을 확인

레퍼런스

https://recoiljs.org/

juno7803 - Recoil 200% 활용하기

https://velog.io/@juno7803/Recoil-Recoil-200-%ED%99%9C%EC%9A%A9%ED%95%98%EA%B8%B0#-reactsuspense%EC%9D%98-%EC%A7%80%EC%9B%90-%EB%B9%84%EB%8F%99%EA%B8%B0-%EC%83%81%ED%83%9C-%EC%B2%98%EB%A6%AC

0개의 댓글