์๋ ํ์ธ์! ๋ฆฌ์กํธ ๊ฐ๋ฐ์๋ผ๋ฉด ํ ๋ฒ์ฏค ์ํ ๊ด๋ฆฌ์ ๋ช์ ๋น ์ ธ๋ณธ ๊ฒฝํ์ด ์์ผ์ค ๊ฒ๋๋ค. Prop-drilling์ ๊ณ ํต, Redux์ ๋ณต์กํ ๋ณด์ผ๋ฌํ๋ ์ดํธ... ์๊ฐ๋ง ํด๋ ๋จธ๋ฆฌ๊ฐ ์ํ์ค์ฃ . ๐คฏ
์ด๋ฐ ๊ณ ๋ฏผ์ ํด๊ฒฐํ๊ธฐ ์ํด ํ์ด์ค๋ถ(ํ Meta)์์ ์ง์ ์ ๋ณด์ธ ์ํ ๊ด๋ฆฌ ๋ผ์ด๋ธ๋ฌ๋ฆฌ, Recoil์ ๋ํด ๊น์ด ์๊ฒ ์์๋ณด๋ ค ํฉ๋๋ค. ์ด๋ฒ ํฌ์คํ ์์๋ Recoil์ด ๋ฌด์์ธ์ง, ์ ์ฐ๋ฆฌ๊ฐ Recoil์ ์ฃผ๋ชฉํด์ผ ํ๋์ง, ๊ทธ๋ฆฌ๊ณ ๊ธฐ๋ณธ ์ฌ์ฉ๋ฒ๋ถํฐ ์ค์ฉ์ ์ธ ๊ฟํ๊น์ง ๋ชจ๋ ๋ด์์ต๋๋ค.
Recoil, ์ ๋ฑ์ฅํ์๊น์? ๐ค
๊ธฐ์กด์ ๊ฐ์ฅ ์ ๋ช ํ ์ํ ๊ด๋ฆฌ ๋ผ์ด๋ธ๋ฌ๋ฆฌ์ธ Redux๋ ๊ฐ๋ ฅํ์ง๋ง, ๋ฐฐ์ฐ๊ธฐ ์ด๋ ต๊ณ ์ค์ ์ด ๋ณต์กํ๋ค๋ ๋จ์ ์ด ์์์ต๋๋ค. ๊ฐ๋จํ ์ํ ํ๋๋ฅผ ์ถ๊ฐํ๋ ค ํด๋ ์ก์ , ๋ฆฌ๋์, ๋์คํจ์น ๋ฑ ์๋ง์ ๋ณด์ผ๋ฌํ๋ ์ดํธ ์ฝ๋๋ฅผ ์์ฑํด์ผ ํ์ฃ .
Recoil์ ์ด๋ฌํ ๋ฌธ์ ์์์์ ์ถ๋ฐํ์ต๋๋ค. "๋ง์น ๋ฆฌ์กํธ์ useState๋ฅผ ์ฌ์ฉํ๋ฏ, ๊ฐ๋จํ๊ณ ์ง๊ด์ ์ผ๋ก ์ ์ญ ์ํ๋ฅผ ๋ค๋ฃฐ ์๋ ์์๊น?"๋ผ๋ ์๊ฐ์์ ํ์ํ ๊ฒ์ด์ฃ . Recoil์ ๋ฆฌ์กํธ์ ๋ด๋ถ ๊ตฌ์กฐ์ ๋งค์ฐ ํก์ฌํ๊ฒ ๋์ํ์ฌ, ๋ฆฌ์กํธ ๊ฐ๋ฐ์๋ผ๋ฉด ๋ณ๋์ ํ์ต ๊ณก์ ์์ด ์์ฐ์ค๋ฝ๊ฒ ์ฌ์ฉํ ์ ์์ต๋๋ค.
Recoil์ ํต์ฌ ์ฒ ํ:
- React-ish: ๋ฆฌ์กํธ์ ์ฒ ํ์ ๊ทธ๋๋ก ๋ฐ๋ฆ ๋๋ค. Hooks์ ํจ๊ป ์ฌ์ฉํ๋ฉฐ, ์ ์ธ์ ์ผ๋ก ์ํ๋ฅผ ๊ด๋ฆฌํฉ๋๋ค.
- ๊ฐ๊ฒฐํจ (Minimal & Boilerplate-free): ๋ถํ์ํ ์ฝ๋๋ฅผ ์์ฑํ ํ์ ์์ด, atom ํ๋๋ก ์ ์ญ ์ํ๋ฅผ ๋๋ฑ ๋ง๋ค ์ ์์ต๋๋ค.
- ํ์ ๋ฐ์ดํฐ (Derived data): ๊ธฐ์กด ์ํ ๊ฐ์ ์กฐํฉํ๊ฑฐ๋ ๊ฐ๊ณตํ์ฌ ์๋ก์ด ์ํ๋ฅผ ๋ง๋๋ selector ๊ธฐ๋ฅ์ด ๋งค์ฐ ๊ฐ๋ ฅํ๊ณ ํจ์จ์ ์ ๋๋ค.
- ์ฑ ์ ์ฒด์ ์ํ ๊ด์ฐฐ: ์ํ ๋ณํ์ ๋ฐ๋ฅธ ๋ฆฌ๋ ๋๋ง์ ์ต์ ํํ๊ณ , ๋๋ฒ๊น ์ ์ฉ์ดํ๊ฒ ํฉ๋๋ค.
Recoil์ ๋ ๊ฐ์ง ํต์ฌ ๊ฐ๋ : Atom & Selector
Recoil์ ์ดํดํ๊ธฐ ์ํด์๋ ๋ฑ ๋ ๊ฐ์ง๋ง ๊ธฐ์ตํ๋ฉด ๋ฉ๋๋ค: atom๊ณผ selector.
1. Atom: ์ํ์ ์ต์ ๋จ์ โ๏ธ
Atom์ ์ํ(state)์ ๊ฐ์ฅ ์์ ๋จ์์ ๋๋ค. ํ๋์ atom์ ํ๋์ ์ํ ๊ฐ์ ์๋ฏธํ์ฃ . ์ปดํฌ๋ํธ๋ค์ ์ด atom์ ๊ตฌ๋ (subscribe)ํ๊ณ , atom์ ๊ฐ์ด ๋ณ๊ฒฝ๋๋ฉด ๊ตฌ๋ ํ๋ ๋ชจ๋ ์ปดํฌ๋ํธ๊ฐ ๋ฆฌ๋ ๋๋ง ๋ฉ๋๋ค.
๋ง์น ๋ฆฌ์กํธ์ useState์ ๋น์ทํ์ง๋ง, ์ปดํฌ๋ํธ ๋ด๋ถ์ ์ข ์๋์ง ์๊ณ ์ฑ ์ด๋์๋ ๊ณต์ ํ ์ ์๋ ์ ์ญ useState๋ผ๊ณ ์๊ฐํ๋ฉด ์ฝ์ต๋๋ค.
๐ Atom ์์
// state/todoState.js
import { atom } from 'recoil';
// ํ ์ผ ๋ชฉ๋ก์ ์ ์ฅํ atom
export const todoListState = atom({
key: 'todoListState', // ๐ฅ ์ฑ ์ ์ฒด์์ ๊ณ ์ ํด์ผ ํ๋ ํค!
default: [], // ๊ธฐ๋ณธ๊ฐ (๋น ๋ฐฐ์ด)
});
// ํ ์ผ ์
๋ ฅ์ฐฝ์ ํ
์คํธ๋ฅผ ๊ด๋ฆฌํ atom
export const todoInputState = atom({
key: 'todoInputState',
default: '', // ๊ธฐ๋ณธ๊ฐ (๋น ๋ฌธ์์ด)
});
key๋ ๋์ค์ ์ํ๋ฅผ ๋๋ฒ๊น ํ๊ฑฐ๋ ํน์ ์ํ๋ฅผ ์๋ณํ ๋ ์ฌ์ฉ๋๋ฏ๋ก, ์ถฉ๋ํ์ง ์๋๋ก ๋ช ํํ๊ฒ ์์ฑํ๋ ๊ฒ์ด ์ค์ํฉ๋๋ค.
2. Selector: ํ์๋ ์ํ โจ
Selector๋ Recoil์ ์ง์ ํ ๊ฐ๋ ฅํจ์ด ๋๋ฌ๋๋ ๋ถ๋ถ์ ๋๋ค. selector๋ ๊ธฐ์กด atom์ด๋ ๋ค๋ฅธ selector์ ์ํ ๊ฐ์ ๊ฐ์ ธ์, ๊ทธ๊ฒ์ ๋ฐํ์ผ๋ก ์๋ก์ด ํ์๋ ๋ฐ์ดํฐ(Derived State)๋ฅผ ๋ฐํํ๋ ์์ ํจ์์ ๋๋ค.
์๋ฅผ ๋ค์ด, ์ ์ฒดํ ์ผ ๋ชฉ๋ก(atom)์์ '์๋ฃ๋ ํ ์ผ'์ ๊ฐ์๋ง ๋ฐ๋ก ๊ณ์ฐํ๊ณ ์ถ์ ๋ selector๋ฅผ ์ฌ์ฉํ ์ ์์ต๋๋ค.
๊ฐ์ฅ ์ค์ํ ํน์ง์ ์บ์ฑ(Caching/Memoization) ๊ธฐ๋ฅ์ ๋๋ค. selector๊ฐ ์์กดํ๋ atom์ ๊ฐ์ด ๋ณ๊ฒฝ๋ ๋๋ง ์ฌ๊ณ์ฐ๋ฉ๋๋ค. ๋ง์ฝ ์์กดํ๋ ์ํ๊ฐ ๊ฐ๋ค๋ฉด, ์ด์ ์ ๊ณ์ฐํ๋ ๊ฐ์ ๊ทธ๋๋ก ๋ฐํํ์ฌ ๋ถํ์ํ ์ฐ์ฐ์ ๋ง์ ์ฑ๋ฅ์ ์ต์ ํํฉ๋๋ค.
๐ Selector ์์
// state/todoState.js
import { selector } from 'recoil';
import { todoListState } from './todoState';
export const todoStatsState = selector({
key: 'todoStatsState',
get: ({ get }) => {
// get ํจ์๋ฅผ ํตํด ๋ค๋ฅธ atom์ด๋ selector์ ๊ฐ์ ๊ฐ์ ธ์ต๋๋ค.
const todoList = get(todoListState);
const totalNum = todoList.length;
const totalCompletedNum = todoList.filter((item) => item.isComplete).length;
const totalUncompletedNum = totalNum - totalCompletedNum;
const percentCompleted = totalNum === 0 ? 0 : (totalCompletedNum / totalNum) * 100;
return {
totalNum,
totalCompletedNum,
totalUncompletedNum,
percentCompleted,
};
},
});
์ ์์ ์์ todoStatsState๋ todoListState์ ์์กดํฉ๋๋ค. todoListState๊ฐ ๋ฐ๋ ๋๋ง ํต๊ณ ๊ณ์ฐ์ ๋ค์ ์ํํ๊ณ , ๋ค๋ฅธ ์ํ๊ฐ ๋ณ๊ฒฝ๋์ด๋ ์ ํ ์ํฅ์ ๋ฐ์ง ์์ต๋๋ค.
Recoil ์ค์ ์ฌ์ฉ๋ฒ: A to Z ๐ง
1. ์ค์น ๋ฐ ์ค์
๋จผ์ ํ๋ก์ ํธ์ Recoil์ ์ค์นํฉ๋๋ค.
npm install recoil
# ๋๋
yarn add recoil
๊ทธ๋ค์, ์ฑ์ ์ต์์ ์ปดํฌ๋ํธ(๋ณดํต App.js๋ index.js)๋ฅผ RecoilRoot๋ก ๊ฐ์ธ์ค๋๋ค. ์ด๋ ๊ฒ ํด์ผ ํ์ ์ปดํฌ๋ํธ๋ค์ด Recoil ์ํ์ ์ ๊ทผํ ์ ์์ต๋๋ค.
// index.js
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
import { RecoilRoot } from 'recoil';
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<React.StrictMode>
<RecoilRoot>
<App />
</RecoilRoot>
</React.StrictMode>
);
2. ์ปดํฌ๋ํธ์์ ์ํ ์ฌ์ฉํ๊ธฐ (feat. Recoil Hooks)
Recoil์ ๋ค์ํ Hooks๋ฅผ ์ ๊ณตํ์ฌ ์ปดํฌ๋ํธ์์ ์ํ๋ฅผ ์ฝ๊ฒ ์ฝ๊ณ ์์ ํ ์ ์๋๋ก ๋์ต๋๋ค.
- useRecoilState(atom): useState์ ์ฌ์ฉ๋ฒ์ด ์์ ํ ๋์ผํฉ๋๋ค. [value, setValue] ๋ฐฐ์ด์ ๋ฐํํ๋ฉฐ, ์ํ๋ฅผ ์ฝ๊ณ ์ ๋ฐ์ดํธํ ๋ ์ฌ์ฉํฉ๋๋ค.
- useRecoilValue(atom): ์ํ์ ๊ฐ๋ง ํ์ํ ๋ ์ฌ์ฉํฉ๋๋ค. ์ปดํฌ๋ํธ๊ฐ ์ํ๋ฅผ ์์ ํ ํ์๊ฐ ์๋ค๋ฉด, ์ด Hook์ ์ฌ์ฉํด ๋ถํ์ํ ๋ฆฌ๋ ๋๋ง์ ๋ฐฉ์งํ ์ ์์ต๋๋ค.
- useSetRecoilState(atom): ์ํ๋ฅผ ๋ณ๊ฒฝํ๋ ํจ์๋ง ํ์ํ ๋ ์ฌ์ฉํฉ๋๋ค. ์๋ฅผ ๋ค์ด, ๋ฒํผ ํด๋ฆญ ์ ์ํ๋ฅผ ์ด๊ธฐํํ๋ ๊ธฐ๋ฅ์ ์ ์ฉํฉ๋๋ค.
- useResetRecoilState(atom): ์ํ๋ฅผ default ๊ฐ์ผ๋ก ์ด๊ธฐํํ ๋ ์ฌ์ฉํฉ๋๋ค.
๐ ์ปดํฌ๋ํธ ํ์ฉ ์์
์์ ๋ง๋ atom๊ณผ selector๋ฅผ ํ์ฉํ ๊ฐ๋จํ ํ ์ผ ๊ด๋ฆฌ ์ฑ ์์ ์ ๋๋ค
import React from 'react';
import { useRecoilState, useRecoilValue } from 'recoil';
import { todoInputState, todoListState, todoStatsState } from './state';
function TodoApp() {
const [text, setText] = useRecoilState(todoInputState);
const [todos, setTodos] = useRecoilState(todoListState);
const { totalNum, totalCompletedNum } = useRecoilValue(todoStatsState);
const addTodo = () => {
if (!text) return;
setTodos((oldTodos) => [
...oldTodos,
{ id: Date.now(), text, isComplete: false },
]);
setText(''); // ์
๋ ฅ์ฐฝ ์ด๊ธฐํ
};
const toggleTodoCompletion = (id) => {
setTodos(
todos.map((todo) =>
todo.id === id ? { ...todo, isComplete: !todo.isComplete } : todo
)
);
};
return (
<div>
<h1>Todo List</h1>
<div>
<input
type="text"
value={text}
onChange={(e) => setText(e.target.value)}
placeholder="ํ ์ผ์ ์
๋ ฅํ์ธ์..."
/>
<button onClick={addTodo}>์ถ๊ฐ</button>
</div>
<ul>
{todos.map((todo) => (
<li
key={todo.id}
onClick={() => toggleTodoCompletion(todo.id)}
style={{ textDecoration: todo.isComplete ? 'line-through' : 'none' }}
>
{todo.text}
</li>
))}
</ul>
<div>
<h3>ํต๊ณ</h3>
<p>์ด {totalNum}๊ฐ์ ํ ์ผ ์ค {totalCompletedNum}๊ฐ ์๋ฃ!</p>
</div>
</div>
);
}
export default TodoApp;
๋น๋๊ธฐ ๋ฐ์ดํฐ ์ฒ๋ฆฌ ๐ธ
Recoil์ selector๋ ๋น๋๊ธฐ ์์ ๋ ๋งค์ฐ ์ฐ์ํ๊ฒ ์ฒ๋ฆฌํ ์ ์์ต๋๋ค. get ํจ์ ๋ด์์ Promise๋ฅผ ๋ฐํํ๋ฉด Recoil์ด ์์์ ๋ฐ์ดํฐ๋ฅผ ์ฒ๋ฆฌํด ์ค๋๋ค. ์ด๋ ์๋ฒ API ํธ์ถ ๊ฒฐ๊ณผ๋ฅผ ์ํ๋ก ๊ด๋ฆฌํ ๋ ํนํ ์ ์ฉํฉ๋๋ค.
๋ฆฌ์กํธ์ <Suspense>์ ํจ๊ป ์ฌ์ฉํ๋ฉด ๋ก๋ฉ ์ํ๋ฅผ ์ ์ธ์ ์ผ๋ก ๊ด๋ฆฌํ ์ ์์ต๋๋ค.
๐ ๋น๋๊ธฐ Selector ์์
// state/userState.js
import { atom, selector } from 'recoil';
// ํ์ฌ ์กฐํํ ์ฌ์ฉ์์ ID๋ฅผ ์ ์ฅํ๋ atom
export const currentUserIDState = atom({
key: 'currentUserIDState',
default: 1,
});
// ์ฌ์ฉ์ ID๋ฅผ ๊ธฐ๋ฐ์ผ๋ก API๋ฅผ ํธ์ถํ์ฌ ์ฌ์ฉ์ ์ ๋ณด๋ฅผ ๊ฐ์ ธ์ค๋ ๋น๋๊ธฐ selector
export const currentUserInfoQuery = selector({
key: 'currentUserInfoQuery',
get: async ({ get }) => {
const userID = get(currentUserIDState);
const response = await fetch(`https://jsonplaceholder.typicode.com/users/${userID}`);
if (!response.ok) {
throw new Error('Failed to fetch user.');
}
const userData = await response.json();
return userData;
},
});
๐ป ์ปดํฌ๋ํธ์์ ๋น๋๊ธฐ ๋ฐ์ดํฐ ์ฌ์ฉํ๊ธฐ
import React, { Suspense } from 'react';
import { useRecoilValue } from 'recoil';
import { currentUserInfoQuery, currentUserIDState } from './state/userState';
function UserInfo() {
// ๋น๋๊ธฐ selector๋ ๋ฐ์ดํฐ๊ฐ ๋ก๋ฉ ์ค์ผ ๋ ์ปดํฌ๋ํธ๋ฅผ Suspense ์ํต๋๋ค.
const currentUser = useRecoilValue(currentUserInfoQuery);
return (
<div>
<h2>User Info</h2>
<p><strong>Name:</strong> {currentUser.name}</p>
<p><strong>Email:</strong> {currentUser.email}</p>
</div>
);
}
function App() {
return (
<div>
{/* Suspense๋ ํ์ ์ปดํฌ๋ํธ์ ๋น๋๊ธฐ ์์
์ด ๋๋ ๋๊น์ง fallback UI๋ฅผ ๋ณด์ฌ์ค๋๋ค. */}
<Suspense fallback={<div>Loading...</div>}>
<UserInfo />
</Suspense>
</div>
);
}
๋ ๋๋ํ๊ฒ Recoil ์ฌ์ฉํ๊ธฐ: ๊ฟํ & ์ฃผ์์ฌํญ ๐
- ๊ณ ์ ํ๊ณ ์ผ๊ด๋ Key ์ฌ์ฉํ๊ธฐ ๋ชจ๋ atom๊ณผ selector์ key๋ ์ฑ ์ ์ฒด์์ ๊ณ ์ ํด์ผ ํฉ๋๋ค. ๋๋ฒ๊น ๊ณผ ์ํ ๊ด๋ฆฌ๋ฅผ ์ฉ์ดํ๊ฒ ํ๋ ค๋ฉด '๋ชจ๋๋ช /์ํ์ด๋ฆ'๊ณผ ๊ฐ์ด ์ผ๊ด๋ ๋ค์ด๋ฐ ๊ท์น์ ์ ํ๋ ๊ฒ์ด ์ข์ต๋๋ค. (์: 'todo/listState')
- ๋ถํ์ํ ๋ฆฌ๋ ๋๋ง ์ต์ํ ์ํ๋ฅผ ์ฝ๊ธฐ๋ง ํ๋ ์ปดํฌ๋ํธ์์๋ useRecoilValue๋ฅผ, ์ํ๋ฅผ ์ฐ๊ธฐ๋ง ํ๋ ์ปดํฌ๋ํธ์์๋ useSetRecoilState๋ฅผ ์ฌ์ฉํ์ธ์. useRecoilState๋ ๊ฐ์ด๋ ์ธํฐ ํจ์๊ฐ ๋ณ๊ฒฝ๋ ๋๋ง๋ค ๋ฆฌ๋ ๋๋ง์ ์ ๋ฐํ ์ ์์ผ๋ฏ๋ก ๊ผญ ํ์ํ ๋๋ง ์ฌ์ฉํ๋ ๊ฒ์ด ์ฑ๋ฅ์ ์ ๋ฆฌํฉ๋๋ค.
- ์ํ ๋๊ธฐํ๋ฅผ ์ํ atomEffects atomEffects๋ atom์ ์ํ๊ฐ ๋ณ๊ฒฝ๋ ๋ ํน์ ๋ถ์ ํจ๊ณผ(side effect)๋ฅผ ์คํํ ์ ์๊ฒ ํด์ฃผ๋ ๊ณ ๊ธ ๊ธฐ๋ฅ์
๋๋ค. ์๋ฅผ ๋ค์ด, ๋ก์ปฌ ์คํ ๋ฆฌ์ง์ ์ํ๋ฅผ ๋๊ธฐํํ๊ฑฐ๋, ํน์ ์ํ ๋ณ๊ฒฝ์ ๋ก๊น
ํ ๋ ๋งค์ฐ ์ ์ฉํฉ๋๋ค.
const localStorageEffect = key => ({setSelf, onSet}) => { const savedValue = localStorage.getItem(key) if (savedValue != null) { setSelf(JSON.parse(savedValue)); } onSet((newValue, _, isReset) => { isReset ? localStorage.removeItem(key) : localStorage.setItem(key, JSON.stringify(newValue)); }); }; export const todoListState = atom({ key: 'todoListState', default: [], effects: [ localStorageEffect('todo_list'), ] }); - ๊ฐ๋ฐ์ ๋๊ตฌ ํ์ฉ Recoilize์ ๊ฐ์ ๋ธ๋ผ์ฐ์ ํ์ฅ ํ๋ก๊ทธ๋จ์ ์ฌ์ฉํ๋ฉด Recoil ์ํ ํธ๋ฆฌ, ์ํ ๊ฐ์ ๋ณํ, ์์กด์ฑ ๊ทธ๋ํ ๋ฑ์ ์๊ฐ์ ์ผ๋ก ํ์ธํ ์ ์์ด ๋๋ฒ๊น ์ด ํจ์ฌ ์ฌ์์ง๋๋ค. ๐ ๏ธ
๋ง๋ฌด๋ฆฌํ๋ฉฐ ๐
Recoil์ ๋ฆฌ์กํธ ์ ํ๋ฆฌ์ผ์ด์ ์ ๋ณต์กํ ์ํ ๊ด๋ฆฌ๋ฅผ ๋จ์ํ๊ณ ์ฐ์ํ๊ฒ ๋ง๋ค์ด์ฃผ๋ ํ์ ์ ์ธ ๋๊ตฌ์ ๋๋ค. ๊ฐ๊ฒฐํ ์ฝ๋, ๋์ ์ ์ฐ์ฑ, ๊ทธ๋ฆฌ๊ณ ๋ฆฌ์กํธ์์ ์๋ฒฝํ ํตํฉ์ ๊ฐ๋ฐ์๋ค์ด ๋ ์ค์ํ ๋น์ฆ๋์ค ๋ก์ง์ ์ง์คํ ์ ์๋๋ก ๋์์ค๋๋ค.
์์ง Recoil์ ์ฌ์ฉํด ๋ณด์ง ์์ผ์ จ๋ค๋ฉด, ๋ค์ ํ๋ก์ ํธ์ ๊ผญ ํ๋ฒ ์ ์ฉํด ๋ณด์ธ์. ๋ฆฌ์กํธ ์ํ ๊ด๋ฆฌ์ ์๋ก์ด ํจ๋ฌ๋ค์์ ๊ฒฝํํ๊ฒ ๋ ๊ฒ์ ๋๋ค!