[React] Recoil 전역상태관리

MINEW·2022년 7월 8일
5

1. 기본 설명 1: store는 atom, selector 사용가능

1) atom 이란?

  • atom은 상태를 정의하는 방법. atom이 set되면, 해당 atom을 구독하고 있던 '모든 컴포넌트들의 state가' 새로운 값으로 리렌더링 된다!
  • 즉, 값을 바꾸면 재할당 해줄 필요없이, 바로 적용이 된다.
  • But, atom을 사용하고 있던 함수가 자동으로 실행되지는 않는다.

2) atom 값을 get & set

  • useRecoilState -> atom을 넣고 -> get -> atom 값 가져오기

  • useRecoilState -> atom을 넣고 -> set -> atom 값 바꾸기

  • selector -> get -> atom 값 가져오기 (+ atom 값 가져와서, 계산한 결과를 return 하기) // 원본훼손X

  • selector -> set -> atom 값 바꾸기 // 원본훼손O

  • useRecoilState -> selector를 넣고 -> get -> get의 return값 반환

  • (이때, selector get(atom)을 통해서, atom을 구독하여 값을 가져와서 계산한 뒤, 결과를 return값으로 반환할 수 있다) // 원본훼손X

  • useRecoilState -> selector를 넣고 -> set -> selector의 set을 실행시킨다 (함수 호출과 비슷)

  • (이때, set(atom, 새로운데이터)를 통해서, selector가 atom 값을 바꿀 수 있다) // 원본훼손O

3) store에서 비동기 처리는 selector 안에서만 가능하다.

4) 여러 컴포넌트에서 같은 atom을 구독할 수 있다.

  • 이때, 각 컴포넌트들마다 atom의 state, setState 명을 다르게 사용해도 전혀 문제가 없다.
  • 만약, 한 컴포넌트에서 setState로 atom을 업데이트 하면 -> 해당 atom을 구독하고 있던 다른 컴포넌트들의 atom도 업데이트 되고, 리렌더링 된다.

5) atom()과 useRecoilState()라인은, 같은 파일에서 사용하지 못한다.
6) atom과 selector는 유니크한 key값으로 고유하게 구분된다. (키값이 존재하여, 상태를 컴포넌트간 공유)

2. 기본 설명 2: 컴포넌트에서 store 사용하기

1) atom, selector 불러오기 (1번)

  • import { atom명, selector명 } from '../store/파일명'
  • import { Aatom, Cselector } from '../store/store'

2) atom, selector 사용하기 (2번)

// 1) useRecoilState(atom명 or selector명)
// useState()와 같이, 배열의 1번째 요소가 상태 값, 2번째 요소가 상태를 업데이트하는 함수 // setSave(새로운값)
const [ save, setSave ] = useRecoilState(saveCart);

// 2) useRecoilValue(atom명 or selector명)
// 상태 값만 필요한 경우에 사용
const save = useRecoilValue(saveCart);

// 3) useSetRecoilState(atom명 or selector명)
// 상태를 업데이트하는 함수만 필요한 경우에 사용 // setSave(새로운값)
const setSave = useSetRecoilState(saveCart);

// 4) useResetRecoilState(atom명)
// atom의 state를, default 값으로 reset 시키는 역할 // resetSave()
const resetSave = useResetRecoilState(setSave);

3. 기본 예시

// <!-- store.js -->    ---> store
import { atom, selector } from 'recoil';

export const Aatom = atom({
  key: 'Aatom',
  default: 0 // 기본값을 설정한다 (default는 = 기본값)
});

export const Bselector = selector({
  key: 'Bselector',
  // get: 이 안에서 get을 여러번 사용할 수 있다.
  get: ({ get }) => { // 원본훼손X
    return get(Aatom) * 10 // B 버튼 3번) Aatom을 구독해서 사용하고 있는 얘도 리렌더링되서, return Aatom의 new값 * 10
    // C 버튼 4번) Aatom을 구독해서 사용하고 있던 얘까지 리렌더링되서, return Aatom의 new값 * 10
  },
  // set: { set, get } 모두 사용할 수 있다.
  set: ({ set, get }, newValue) => { // 원본훼손O
    // set(Aatom, newValue) // Aatom = newValue 이런식으로, 기존값 무시하고 재할당된다.
    set(Aatom, get(Aatom) + newValue) // B 버튼 2번) Aatom = Aatom + newValue 이런식으로, count = count + 1 의 방식을 유지할 수 있다.
  }
});

export const Cselector = selector({
  key: 'Cselector',
  get: ({ get }) => { // 원본훼손X
    return get(Aatom) / 10 // B 버튼 4번) Aatom을 구독해서 사용하고 있던 얘까지 리렌더링되서, return Aatom의 new값 / 10
    // C 버튼 3번) Aatom을 구독해서 사용하고 있는 얘도 리렌더링되서, return Aatom의 new값 / 10
  },
  set: ({ set, get }, newValue) => { // 원본훼손O
    set(Aatom, get(Aatom) - newValue) // C 버튼 2번) Aatom = Aatom - newValue
  }
});
// <!-- Apage.jsx -->    ---> components
import { useState, useEffect } from 'react'
import { useRecoilValue, useSetRecoilState, useRecoilState } from 'recoil'; 

import { Aatom, Bselector, Cselector } from '../store/store'; // import { atom명, selector명 } from '../store/파일명'

function Apage() {
  const [ aatom, setAatom ] = useRecoilState(Aatom)
  useEffect(() => {
    setAatom(1)
  }, [])

  const [ bselector, setBselector ] = useRecoilState(Bselector)
  const BcallMe = () => { setBselector(7) }; // B 버튼 1번)

  const [ cselector, setCselector ] = useRecoilState(Cselector)
  const CcallMe = () => { setCselector(7) }; // C 버튼 1번)

  useEffect(() => {
    console.log('Aatom 값이 바뀌었어요!!');
  }, [aatom]) // aatom으로 해야, 바뀌었을때 계속 적용된다. (만약, 이때 Aatom으로 하면 로드되었을때 딱 한번만 작동한다)

  return (
    <div>
      { aatom }
      <br/>
      { bselector }
      <br/>
      { cselector }
      <br/>
      <button onClick={ BcallMe }> B 버튼 </button>
      <button onClick={ CcallMe }> C 버튼 </button>
    </div>
  )
}

export default Apage
// <!-- Bpage.jsx -->    ---> components
import { useState, useEffect } from 'react'
import { useRecoilValue, useSetRecoilState, useRecoilState } from 'recoil';

import { Aatom } from '../store/store'; // import { atom명, selector명 } from '../store/파일명'

function Bpage() {
  const [ A, setA ] = useRecoilState(Aatom) // Aatom값이 변할때마다 같이 변한다.
  useEffect(() => {
    setA(3)
  }, [])

  const wantKnow = () => {
    setA(A + 10000) // 값이 계속 쌓인다. count = count + 1 의 방식을 유지할 수 있다.
  }

  return (
    <div>
      { A }
      <br/>
      <button onClick={ wantKnow }> 확인합니다 </button>
    </div>
  )
}

export default Bpage

4. 심화 예시 (비동기 처리)

// <!-- store.js -->    ---> store
import { atom, selector } from 'recoil';
import axios from 'axios';

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

export const Bselector = selector({
  key: 'Bselector',
  // get: 이 안에서 get을 여러번 사용할 수 있다. get은 atom과 selector 모두 가져올 수 있다.
  get: ({ get }) => { // 원본훼손X
    return get(Aatom) * 10 // B 버튼 3번) Aatom을 구독해서 사용하고 있는 얘도 리렌더링되서, return Aatom의 new값 * 10
    // C 버튼 4번) Aatom을 구독해서 사용하고 있던 얘까지 리렌더링되서, return Aatom의 new값 * 10
  },
  // set: { set, get } 모두 사용할 수 있다.
  set: ({ set, get }, newValue) => { // 원본훼손O
    // set(Aatom, newValue) // Aatom = newValue 이런식으로, 기존값 무시하고 재할당된다.
    set(Aatom, get(Aatom) + newValue) // B 버튼 2번) Aatom = Aatom + newValue 이런식으로, count = count + 1 의 방식을 유지할 수 있다.
  }
});

export const Cselector = selector({
  key: 'Cselector',
  get: ({ get }) => { // 원본훼손X
    return get(Aatom) / 10 // B 버튼 4번) Aatom을 구독해서 사용하고 있던 얘까지 리렌더링되서, return Aatom의 new값 / 10
    // C 버튼 3번) Aatom을 구독해서 사용하고 있는 얘도 리렌더링되서, return Aatom의 new값 / 10
  },
  set: ({ set, get }, newValue) => { // 원본훼손O
    set(Aatom, get(Aatom) - newValue) // C 버튼 2번) Aatom = Aatom - newValue
  }
});

// 1번)
export const Dselector = selector({
  key: 'Dselector',
  get: async () => { // 비동기 처리는 get에서 한다.
    const { data } = await axios.get('https://fakestoreapi.com/products');
    const aa = 'aa'
    const bb = 'bb'
    console.log(data);

    return { data, aa, bb }
  } // 만약, 비동기처리한 데이터를 atom으로 갖고 싶으면, set에서 get으로 호출한 값을 atom에 넣어줘도 되고, use라인 사용해서 컴포넌트에서 atom에 넣어줘도 되고.
});
// <!-- Bpage.jsx -->    ---> components
import { useState, useEffect } from 'react'
import { useRecoilValue, useSetRecoilState, useRecoilState } from 'recoil';

import { Aatom, Dselector } from '../store/store'; // import { atom명, selector명 } from '../store/파일명'

function Bpage() {
  const [ A, setA ] = useRecoilState(Aatom) // Aatom값이 변할때마다 같이 변한다.
  useEffect(() => {
    setA(3)
  }, [])

  // 2번)
  const { data, aa, bb } = useRecoilValue(Dselector)
  const result = data.map((item) => (<div key={item.id}>{ item.id }</div>)) // data바로 사용 X, 풀어서만 사용가능. key값 꼭 설정해야한다.

  return (
    <div>
      { A }
      <br/>
      <br/>
      { result } // 3번) 결과가 1 ~ 20 까지
      <br/>
      { aa }
      <br/>
      { bb }
    </div>
  )
}

export default Bpage
// <!-- Apage.jsx -->    ---> components
import { useState, useEffect } from 'react'
import { useRecoilValue, useSetRecoilState, useRecoilState, useResetRecoilState } from 'recoil'; 

import { Aatom, Bselector, Cselector, Dselector } from '../store/store'; // import { atom명, selector명 } from '../store/파일명'

function Apage() {
  const [ aatom, setAatom ] = useRecoilState(Aatom)
  useEffect(() => {
    setAatom(1)
  }, [])

  const [ bselector, setBselector ] = useRecoilState(Bselector)
  const BcallMe = () => { setBselector(7) }; // B 버튼 1번)

  const [ cselector, setCselector ] = useRecoilState(Cselector)
  const CcallMe = () => { setCselector(7) }; // C 버튼 1번)

  useEffect(() => {
    console.log('Aatom 값이 바뀌었어요!!');
  }, [aatom]) // aatom으로 해야, 바뀌었을때 계속 적용된다. (만약, 이때 Aatom으로 하면 로드되었을때 딱 한번만 작동한다)

  const { data, aa, bb } = useRecoilValue(Dselector) // 4번) 다른 컴포넌트에서 사용해도, 함수가 리렌더링되지 않고, 캐싱된 값을 가져온다.

  const reset = useResetRecoilState(Aatom); // 5번)
  // Aatom이 초기값으로 돌아가서, Aatom을 구독하고 있던 모든 컴포넌트들의 Aatom값 부분에, 초기값이 들어간다.
  // 문제) Aatom의 초기값이 1이고, 현재값이 10일때 -> selector에서 Aatom을 구독하고 있었다 -> 이때, reset을 하면?
  // 답) selector의 get부분이 return get(Aatom) * 10 으로 되어있었다면, return 10 * 10 에서 -> return 1 * 10 으로 바뀌어서 결과값이 반환된다.

  return (
    <div>
      { aa }
      <br/>
      { aatom }
      <br/>
      { bselector }
      <br/>
      { cselector }
      <br/>
      <button onClick={ BcallMe }> B 버튼 </button>
      <button onClick={ CcallMe }> C 버튼 </button>
      <button onClick={ reset }> Reset 버튼 </button>
    </div>
  )
}

export default Apage

5. store를 사용하기 위해서 필요한 단계

// <!-- main.jsx -->    ---> Vite 버전
// <!-- index.js -->    ---> Vue에서는 main.js
import React, { Suspense } from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
import { RecoilRoot } from 'recoil'; // store를 사용하기 위해서 필요한 단계 1

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
  // <React.StrictMode>
  <RecoilRoot> // store를 사용하기 위해서 필요한 단계 2
    <Suspense fallback={<div>Loading...</div>}> // store를 사용하기 위해서 필요한 단계 3 (비동기 처리 사용하기 위해서)
      <App />
    </Suspense>
  </RecoilRoot>
  // </React.StrictMode>
);

6. 매개변수에 따른 api 비동기 처리: 추가 예시

1) 매개변수 X

import { selector } from 'recoil';
import { productsApi } from '@apis/goodsApi';

export const productLists = selector({
  key: 'productLists',
  get: async () => { // 1번) 매개변수가 없을 때는
    const data = await productsApi(); // 2번) 바로 api 불러오면 된다
    const allLists: AllListsGuard = {};

    const all = data.map((item: ProductGuard) => {
      item.price = Math.round(item.price);
      allLists[item.id] = item;

      return item;
    });

    return { all, allLists };
  }
});

2) 매개변수 O (selector + atom)

import { atom, selector } from 'recoil';
import { singleApi } from '@apis/goodsApi';

export const productId = atom({ // 1번) store를 사용하는 컴포넌트에서 set을 통해 해당 아톰을 변경하면
  key: 'productId',
  default: ''
});

export const singleProduct = selector({
  key: 'singleProduct',
  get: async ({ get }) => {
    const data = await singleApi(get(productId)); // 2번) productId 아톰을 구독하고 있던 selector도 영향을 받는다

    if (data === '') return { data };
    data.price = Math.round(data.price);
    
    return { data };
  }
});

3) 매개변수 O (selectorFamily)

import { selectorFamily } from 'recoil';
import { singleApi } from '@apis/goodsApi';

export const singleProduct = selectorFamily({ // 1번) selector 대신 selectorFamily
  key: 'singleProduct',
  get: (productId: string | undefined) => async () => { // 2번) 앞에 매개변수 넣어주고
    const data = await singleApi(productId); // 3번) 사용한다

    if (data === '') return { data };
    data.price = Math.round(data.price);
    
    return { data };
  }
});
profile
JS, TS, React, Vue, Node.js, Express, SQL 공부한 내용을 기록하는 장소입니다

0개의 댓글