"์๋ฒ์์ ๋ฐ์ดํฐ๋ฅผ ๊ฐ์ ธ์์ผ ํ๋๋ฐ... 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>;
}
์ด ์ฝ๋์ ๋ฌธ์ ์ ์ ๋ช ํํฉ๋๋ค.
- ๋ฐ๋ณต์ ์ธ ๋ณด์ผ๋ฌํ๋ ์ดํธ: API๋ฅผ ํธ์ถํ ๋๋ง๋ค isLoading, error, data ์ํ 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 ๋ ๋๋ง์๋ง ์ง์คํ๋ ๊ฒ์ ๋๋ค.
'๐บ React & Next.js' ์นดํ ๊ณ ๋ฆฌ์ ๋ค๋ฅธ ๊ธ
| FSD์ํคํ ์ฒ(Feature-Sliced Design) ๊ฐ์ด๋ (0) | 2025.10.15 |
|---|---|
| "๋ฐฐํฌ๋ ํ๋ฒ๋ง" Vercel CI/CD๋ก ์นผํด๋ฅผ ์๋น๊ธฐ๋ ๋ฒ (0) | 2025.09.23 |
| Recoil: React ์ํ ๊ด๋ฆฌ์ ์๋ก์ด ํจ๋ฌ๋ค์ (0) | 2025.09.05 |
| Vercel๋ก Next.js ํ๋ก์ ํธ ์๋ ๋ฐฐํฌํ๊ธฐ (0) | 2025.08.31 |
| ๋ฆฌ์กํธ ์ํ ๊ด๋ฆฌ, Recoil ์ดํดํ๊ธฐ (4) | 2025.08.28 |