Pink Bobblehead Bunny Recoil atom๋งŒ ์จ๋ดค๋‹ค๋ฉด ์ฃผ๋ชฉ! isLoading, isError์™€ ์ž‘๋ณ„ํ•˜๋Š” ๋น„๋™๊ธฐ ์ฒ˜๋ฆฌ ์‹ค์ „ ํŒจํ„ด ๐Ÿš€
 

Recoil atom๋งŒ ์จ๋ดค๋‹ค๋ฉด ์ฃผ๋ชฉ! isLoading, isError์™€ ์ž‘๋ณ„ํ•˜๋Š” ๋น„๋™๊ธฐ ์ฒ˜๋ฆฌ ์‹ค์ „ ํŒจํ„ด ๐Ÿš€

"์„œ๋ฒ„์—์„œ ๋ฐ์ดํ„ฐ๋ฅผ ๊ฐ€์ ธ์™€์•ผ ํ•˜๋Š”๋ฐ... useEffect ์•ˆ์—์„œ API๋ฅผ ํ˜ธ์ถœํ•˜๊ณ , ๋กœ๋”ฉ ์ƒํƒœ๋ฅผ ์œ„ํ•œ useState(true), ์—๋Ÿฌ ์ƒํƒœ๋ฅผ ์œ„ํ•œ useState(null)์„ ๋˜ ๋งŒ๋“ค์–ด์•ผ ํ•˜๋‚˜?"

์ด๋Ÿฐ ๊ณ ๋ฏผ, ๋‹ค๋“ค ํ•œ ๋ฒˆ์ฏค ํ•ด๋ณด์…จ์„ ๊ฒ๋‹ˆ๋‹ค. ์ปดํฌ๋„ŒํŠธ๋Š” ์ˆœ์‹๊ฐ„์— ์ง€์ €๋ถ„ํ•œ ์ƒํƒœ ๊ฐ’๋“ค๋กœ ๊ฐ€๋“ ์ฐจ๊ณ , ๋น„๋™๊ธฐ ๋กœ์ง์€ ์—ฌ๊ธฐ์ €๊ธฐ ํฉ์–ด์ ธ ์œ ์ง€๋ณด์ˆ˜๋ฅผ ์–ด๋ ต๊ฒŒ ๋งŒ๋“ญ๋‹ˆ๋‹ค.

์˜ค๋Š˜์€ ๋ฐ”๋กœ ์ด ๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐํ•˜๊ธฐ ์œ„ํ•ด Recoil์˜ ์ง„์งœ ๊ฐ•๋ ฅํ•จ, ๋น„๋™๊ธฐ selector์™€ selectorFamily๋ฅผ ํ™œ์šฉํ•˜์—ฌ ์„œ๋ฒ„ ์ƒํƒœ๋ฅผ ์–ผ๋งˆ๋‚˜ ๊น”๋”ํ•˜๊ฒŒ ๊ด€๋ฆฌํ•  ์ˆ˜ ์žˆ๋Š”์ง€ ์•Œ๋ ค๋“œ๋ฆด๊ฒŒ์š”. ์ด ๊ธ€์„ ๋‹ค ์ฝ๊ณ  ๋‚˜๋ฉด, ์—ฌ๋Ÿฌ๋ถ„์˜ ์ฝ”๋“œ๋Š” ํ›จ์”ฌ ์„ ์–ธ์ ์ผ๊ฒ๋‹ˆ๋‹ค.


์™œ useEffect + useState ์กฐํ•ฉ์€ ๊ดด๋กœ์šด๊ฐ€?

์šฐ๋ฆฌ๊ฐ€ API ํ†ต์‹ ์„ ํ•  ๋•Œ ๋ณดํ†ต ์ด๋Ÿฐ ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•ฉ๋‹ˆ๋‹ค.

function UserProfile({ userId }) {
  const [user, setUser] = useState(null);
  const [isLoading, setIsLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    const fetchUser = async () => {
      try {
        setIsLoading(true);
        const response = await fetch(`/api/users/${userId}`);
        const data = await response.json();
        setUser(data);
      } catch (e) {
        setError(e);
      } finally {
        setIsLoading(false);
      }
    };
    fetchUser();
  }, [userId]);

  if (isLoading) return <div>๋กœ๋”ฉ ์ค‘...</div>;
  if (error) return <div>์—๋Ÿฌ ๋ฐœ์ƒ!</div>;

  return <div>{user.name}</div>;
}

์ด ์ฝ”๋“œ์˜ ๋ฌธ์ œ์ ์€ ๋ช…ํ™•ํ•ฉ๋‹ˆ๋‹ค.

  1. ๋ฐ˜๋ณต์ ์ธ ๋ณด์ผ๋Ÿฌํ”Œ๋ ˆ์ดํŠธ: API๋ฅผ ํ˜ธ์ถœํ•  ๋•Œ๋งˆ๋‹ค isLoading, error, data ์ƒํƒœ 3์ข… ์„ธํŠธ๋ฅผ ๋งŒ๋“ค์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.
  2. ๋ช…๋ นํ˜• ์ฝ”๋“œ: "๋กœ๋”ฉ ์‹œ์ž‘ํ•˜๊ณ , ๋ฐ์ดํ„ฐ ๊ฐ€์ ธ์˜ค๊ณ , ์„ฑ๊ณตํ•˜๋ฉด ๋ฐ์ดํ„ฐ ๋„ฃ๊ณ , ๋๋‚˜๋ฉด ๋กœ๋”ฉ ๋๋‚ด๊ณ ..." ์™€ ๊ฐ™์ด ๊ณผ์ •์„ ํ•˜๋‚˜ํ•˜๋‚˜ ๋ช…๋ นํ•˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค.
  3. ์ƒํƒœ ๋ถ„๋ฆฌ์˜ ์–ด๋ ค์›€: ์ด ๋ฐ์ดํ„ฐ๊ฐ€ ๋‹ค๋ฅธ ์ปดํฌ๋„ŒํŠธ์—๋„ ํ•„์š”ํ•˜๋‹ค๋ฉด? ์ƒํƒœ๋ฅผ ๋ถ€๋ชจ๋กœ ๋Œ์–ด์˜ฌ๋ฆฌ๋Š” 'Props Drilling' ์ง€์˜ฅ์ด ์‹œ์ž‘๋ฉ๋‹ˆ๋‹ค.

๋น„๋™๊ธฐ selector๋กœ ์„œ๋ฒ„ ๋ฐ์ดํ„ฐ๋ฅผ '์ƒํƒœ'์ฒ˜๋Ÿผ ๋‹ค๋ฃจ๊ธฐ

selector๋Š” ๋‹ค๋ฅธ atom์ด๋‚˜ selector๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ ์ƒˆ๋กœ์šด ํŒŒ์ƒ ๋ฐ์ดํ„ฐ๋ฅผ ๋งŒ๋“œ๋Š” ์—ญํ• ๋งŒ ํ•œ๋‹ค๊ณ  ์•Œ๊ณ  ๊ณ„์…จ๋‚˜์š”? ์‚ฌ์‹ค selector์˜ get ํ•จ์ˆ˜๋Š” Promise๋ฅผ ๋ฐ˜ํ™˜ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์ฆ‰, ๋น„๋™๊ธฐ API ํ˜ธ์ถœ ๋กœ์ง์„ selector ์•ˆ์— ๊ทธ๋Œ€๋กœ ๋„ฃ์„ ์ˆ˜ ์žˆ๋‹ค๋Š” ๋œป์ด์ฃ !

// atoms.js
import { selector } from 'recoil';
import axios from 'axios';

// ๐Ÿ’ก 'userState'๋ผ๋Š” ์ด๋ฆ„์˜ ๋น„๋™๊ธฐ selector
export const userState = selector({
  key: 'userState',
  get: async () => {
    // try-catch๊ฐ€ ํ•„์š” ์—†์–ด์š”! Recoil์ด ์•Œ์•„์„œ ์ฒ˜๋ฆฌํ•ฉ๋‹ˆ๋‹ค.
    const response = await axios.get('/api/users/1'); 
    return response.data;
  },
});

์–ด๋ผ? isLoading์ด๋ž‘ error ์ƒํƒœ๋Š” ์–ด๋”” ๊ฐ”์ฃ ?" ๋ผ๊ณ  ์ƒ๊ฐํ•˜์…จ๋‹ค๋ฉด ์ •ํ™•ํ•ฉ๋‹ˆ๋‹ค. Recoil์€ ๋น„๋™๊ธฐ selector๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ์ปดํฌ๋„ŒํŠธ์˜ ์ƒํƒœ๋ฅผ ๋‚ด๋ถ€์ ์œผ๋กœ 'loading', 'hasValue', 'hasError' ๋กœ ๊ด€๋ฆฌํ•ฉ๋‹ˆ๋‹ค.

์šฐ๋ฆฌ๋Š” ์ด ์ƒํƒœ๋ฅผ React์˜ Suspense์™€ ErrorBoundary๋ฅผ ํ†ตํ•ด ์•„์ฃผ ์šฐ์•„ํ•˜๊ฒŒ ์ฒ˜๋ฆฌํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

 

// UserProfile.jsx
import { Suspense } from 'react';
import { useRecoilValue } from 'recoil';
import { userState } from './atoms';
import ErrorBoundary from './ErrorBoundary'; // ์ง์ ‘ ๊ตฌํ˜„ํ•˜๊ฑฐ๋‚˜ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ์‚ฌ์šฉ

function UserInfo() {
  // ๐Ÿ’ก ๋น„๋™๊ธฐ ์ฒ˜๋ฆฌ๊ฐ€ ๋๋‚  ๋•Œ๊นŒ์ง€ ์ด ์ปดํฌ๋„ŒํŠธ๋Š” '์ผ์‹œ ์ •์ง€' ๋ฉ๋‹ˆ๋‹ค.
  const user = useRecoilValue(userState);
  return <h1>{user.name}</h1>;
}

function UserProfile() {
  return (
    <ErrorBoundary fallback={<div>์—๋Ÿฌ๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.</div>}>
      <Suspense fallback={<div>๋กœ๋”ฉ ์ค‘์ž…๋‹ˆ๋‹ค...</div>}>
        <UserInfo />
      </Suspense>
    </ErrorBoundary>
  );
}

์–ด๋–ค๊ฐ€์š”? UserProfile ์ปดํฌ๋„ŒํŠธ์—์„œ isLoading, error ์ƒํƒœ๊ฐ€ ์™„์ „ํžˆ ์‚ฌ๋ผ์กŒ์Šต๋‹ˆ๋‹ค. UserInfo ์ปดํฌ๋„ŒํŠธ๋Š” ๋ฐ์ดํ„ฐ๊ฐ€ ์„ฑ๊ณต์ ์œผ๋กœ ๋„์ฐฉํ–ˆ์„ ๋•Œ์˜ UI๋งŒ ์ฑ…์ž„์ง€๋ฉด ๋ฉ๋‹ˆ๋‹ค. ๋กœ๋”ฉ๊ณผ ์—๋Ÿฌ ์ฒ˜๋ฆฌ๋Š” ๊ฐ๊ฐ Suspense์™€ ErrorBoundary์— ์œ„์ž„ํ–ˆ์ฃ . ์ด๊ฒƒ์ด ๋ฐ”๋กœ ๊ด€์‹ฌ์‚ฌ์˜ ๋ถ„๋ฆฌ(Separation of Concerns) ์ž…๋‹ˆ๋‹ค.


SelectorFamily๋กœ ๋™์ ์ธ ๋ฐ์ดํ„ฐ ์š”์ฒญํ•˜๊ธฐ

"๊ทธ๋Ÿฐ๋ฐ ์‚ฌ์šฉ์ž ID๊ฐ€ 1๋กœ ๊ณ ์ •๋˜์–ด ์žˆ์ž–์•„์š”. ID๋ฅผ ํŒŒ๋ผ๋ฏธํ„ฐ๋กœ ๋„˜๊ฒจ์„œ ๋™์ ์œผ๋กœ ๋ฐ์ดํ„ฐ๋ฅผ ๊ฐ€์ ธ์˜ค๊ณ  ์‹ถ์–ด์š”!"

์•„์ฃผ ์ข‹์€ ์งˆ๋ฌธ์ž…๋‹ˆ๋‹ค. ์ด๋Ÿด ๋•Œ ์‚ฌ์šฉํ•˜๋Š” ๊ฒƒ์ด ๋ฐ”๋กœ selectorFamily ์ž…๋‹ˆ๋‹ค. selectorFamily๋Š” ๋ง ๊ทธ๋Œ€๋กœ 'selector๋ฅผ ๋งŒ๋“œ๋Š” ๊ฐ€์กฑ(ํŒจ๋ฐ€๋ฆฌ)' ์ฆ‰, selector๋ฅผ ๋ฐ˜ํ™˜ํ•˜๋Š” ํ•จ์ˆ˜์ž…๋‹ˆ๋‹ค.

๊ฐœ๋… ์„ค๋ช…
selector ๊ณ ์ •๋œ ํ•˜๋‚˜์˜ ํŒŒ์ƒ ์ƒํƒœ ๊ฐ’์„ ์ •์˜ํ•ฉ๋‹ˆ๋‹ค.
selectorFamily ๋งค๊ฐœ๋ณ€์ˆ˜๋ฅผ ๋ฐ›์•„, ๊ทธ์— ๋งž๋Š” selector๋ฅผ ๋™์ ์œผ๋กœ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค.

userId๋ฅผ ํŒŒ๋ผ๋ฏธํ„ฐ๋กœ ๋ฐ›๋Š” userQuery๋ฅผ ๋งŒ๋“ค์–ด ๋ด…์‹œ๋‹ค.

// atoms.js
import { selectorFamily } from 'recoil';
import axios from 'axios';

export const userQuery = selectorFamily({
  key: 'userQuery',
  get: (userId) => async () => { // ๐Ÿ’ก ํŒŒ๋ผ๋ฏธํ„ฐ๋ฅผ ๋ฐ›๋Š” ํ•จ์ˆ˜๊ฐ€ Promise๋ฅผ ๋ฐ˜ํ™˜ํ•˜๋Š” ํ•จ์ˆ˜๋ฅผ ๋ฆฌํ„ด
    const response = await axios.get(`/api/users/${userId}`);
    return response.data;
  },
});

์ด์ œ ์ปดํฌ๋„ŒํŠธ์—์„œ๋Š” ์ด userQuery๋ฅผ ๋งˆ์น˜ ํ•จ์ˆ˜์ฒ˜๋Ÿผ ํ˜ธ์ถœํ•˜์—ฌ ์‚ฌ์šฉํ•˜๋ฉด ๋ฉ๋‹ˆ๋‹ค. Recoil์€ userId ๋ณ„๋กœ API ํ˜ธ์ถœ ๊ฒฐ๊ณผ๋ฅผ ์ž๋™์œผ๋กœ ์บ์‹ฑํ•ด์ค๋‹ˆ๋‹ค. ๊ฐ™์€ userId๋กœ ๋‹ค์‹œ ์š”์ฒญํ•˜๋ฉด API๋ฅผ ๋˜ ํ˜ธ์ถœํ•˜์ง€ ์•Š๊ณ  ์บ์‹ฑ๋œ ๊ฐ’์„ ์ฆ‰์‹œ ๋ฐ˜ํ™˜ํ•˜์ฃ .

// UserInfo.jsx
function UserInfo({ userId }) {
  // ๐Ÿ’ก userQuery์— userId๋ฅผ ์ „๋‹ฌํ•˜์—ฌ ํ•ด๋‹น ์œ ์ €์˜ ๋ฐ์ดํ„ฐ๋ฅผ ๊ตฌ๋…
  const user = useRecoilValue(userQuery(userId)); 
  return <h1>{user.name}</h1>
}

// App.js์—์„œ <UserInfo userId={1} />, <UserInfo userId={2} /> ์ฒ˜๋Ÿผ ์‚ฌ์šฉ

์„œ๋ฒ„ ์ƒํƒœ๋ฅผ atom์— ๋„ฃ์ง€ ๋งˆ์„ธ์š”

ํ”ํžˆ ํ•˜๋Š” ์‹ค์ˆ˜๋Š” useEffect์—์„œ API๋ฅผ ํ˜ธ์ถœํ•œ ๋’ค, ๊ทธ ๊ฒฐ๊ณผ๋ฅผ setRecoilState๋ฅผ ์ด์šฉํ•ด atom์— ์ €์žฅํ•˜๋Š” ๊ฒƒ์ž…๋‹ˆ๋‹ค.

// ๐Ÿ‘Ž ์•ˆํ‹ฐ-ํŒจํ„ด
const [users, setUsers] = useRecoilState(usersAtom);

useEffect(() => {
  const response = await axios.get('/api/users');
  setUsers(response.data); // ์„œ๋ฒ„์—์„œ ์˜จ ๋ฐ์ดํ„ฐ๋ฅผ atom์— ์ง์ ‘ ์ €์žฅ
}, []);

์ด ๋ฐฉ์‹์€ Recoil์„ ๋‹จ์ˆœํ•œ useState์˜ ์ „์—ญ ๋ฒ„์ „์œผ๋กœ๋งŒ ์‚ฌ์šฉํ•˜๋Š” ๊ฒƒ์ž…๋‹ˆ๋‹ค. ์„œ๋ฒ„ ์ƒํƒœ๋Š” ์–ธ์ œ๋“  ๋‹ค์‹œ ๋ถˆ๋Ÿฌ์˜ฌ ์ˆ˜ ์žˆ๊ณ , ์บ์‹ฑ์ด ํ•„์š”ํ•˜๋ฉฐ, ๋กœ๋”ฉ/์—๋Ÿฌ ์ƒํƒœ๋ฅผ ๊ฐ€์ง‘๋‹ˆ๋‹ค. ์ด๋Ÿฐ ๋ณต์žกํ•œ ํŠน์ง•์„ ๊ฐ€์ง„ ์„œ๋ฒ„ ์ƒํƒœ๋Š” atom์ด ์•„๋‹Œ ๋น„๋™๊ธฐ selector์— ์ €์žฅํ•˜๊ณ  ๊ด€๋ฆฌํ•˜๋Š” ๊ฒƒ์ด Recoil์˜ ์ฒ ํ•™์— ๋” ๊ฐ€๊น์Šต๋‹ˆ๋‹ค.

  • Client State (ํด๋ผ์ด์–ธํŠธ ์ƒํƒœ): ๋‹คํฌ ๋ชจ๋“œ ์—ฌ๋ถ€, ๋ชจ๋‹ฌ ์—ด๋ฆผ ์ƒํƒœ ๋“ฑ ์‚ฌ์šฉ์ž์˜ ์ธํ„ฐ๋ž™์…˜์œผ๋กœ๋งŒ ๋ณ€ํ•˜๋Š” ์ƒํƒœ -> atom
  • Server State (์„œ๋ฒ„ ์ƒํƒœ): DB์—์„œ ๊ฐ€์ ธ์˜ค๋Š” ๋ฐ์ดํ„ฐ, ๋น„๋™๊ธฐ์ ์œผ๋กœ ๋ณ€ํ•˜๋Š” ์ƒํƒœ -> ๋น„๋™๊ธฐ selector

์ด ๋‘˜์„ ๋ถ„๋ฆฌํ•ด์„œ ์ƒ๊ฐํ•˜๋Š” ๊ฒƒ๋งŒ์œผ๋กœ๋„ ์ƒํƒœ ๊ด€๋ฆฌ์˜ ๋ณต์žก๋„๊ฐ€ ํฌ๊ฒŒ ์ค„์–ด๋“ญ๋‹ˆ๋‹ค.


์˜ค๋Š˜์€ Recoil์˜ atom์„ ๋„˜์–ด, ๋น„๋™๊ธฐ selector์™€ selectorFamily๋ฅผ ํ†ตํ•ด ์„œ๋ฒ„ ์ƒํƒœ๋ฅผ ์–ผ๋งˆ๋‚˜ ํšจ์œจ์ ์œผ๋กœ ๊ด€๋ฆฌํ•  ์ˆ˜ ์žˆ๋Š”์ง€ ์•Œ์•„๋ดค์Šต๋‹ˆ๋‹ค. ํ•ต์‹ฌ์€ isLoading, error ๊ฐ™์€ ๋ถ€์ˆ˜์ ์ธ ์ƒํƒœ ๊ด€๋ฆฌ๋ฅผ Recoil๊ณผ React Suspense์— ์œ„์ž„ํ•˜๊ณ , ์ปดํฌ๋„ŒํŠธ๋Š” ์˜ค์ง ์„ฑ๊ณตํ–ˆ์„ ๋•Œ์˜ UI ๋ Œ๋”๋ง์—๋งŒ ์ง‘์ค‘ํ•˜๋Š” ๊ฒƒ์ž…๋‹ˆ๋‹ค.