September 25, 2020
Facebook์์ ๋ง๋ React ์ํ ๊ด๋ฆฌ ๋ผ์ด๋ธ๋ฌ๋ฆฌ ์ ๋๋ค. Recoil์ ์์ ์ ์ฅ์ ์ ๋ค์๊ณผ ๊ฐ์ด ์ค๋ช ํ๊ณ ์์ต๋๋ค.
๊ทธ๋ ๋ค๊ณ ํฉ๋๋ค.
๋ณต์กํ react ์ฑ์์๋ ์ํ ๊ด๋ฆฌ๊ฐ ๊ฐ๋ฐ์์ ์ฃผ์ํ๊ณ ์ด๋ ค์ด ์์
์ด ๋ฉ๋๋ค. ํ๋์ ์ํ ๋ณ๊ฒฝ์ผ๋ก ์ธํด ๋ค๋ฅธ ์ปดํฌ๋ํธ๋ค์ ์ํฅ์ ์ฃผ๊ธฐ๋ ํ๊ธฐ ๋๋ฌธ์ธ๋ฐ,
๊ทธ๋์ Redux๋ MobX๊ฐ์ ์ํ ๊ด๋ฆฌ ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ค์ด ๋ฑ์ฅํ๊ฒ ๋์์ต๋๋ค.
Redux๋ MobX์ API๋ ๋จ์ํ์ง ์๊ณ , ๋ณธ ๋ชฉ์ ์ด React์์ ์ฌ์ฉํ๊ธฐ ์ํด ๋์จ๊ฒ์ด ์๋๋๋ค.
๊ทธ๋์ API์ ๋์ ๋ฐฉ์์ ๊ฐ๋ฅํ ๋ฆฌ์กํธ์ค๋ฝ๊ฒ ๋ง๋ค๊ธฐ ์ํด ๋ฆฌ์ฝ์ผ์ ์ ์ํ๋ค๊ณ ํฉ๋๋ค.
์ผ๋จ CRA๋ก ์ฑ ํ๋๋ฅผ ๋ง๋ค์ด ์ค๋๋ค.
npx create-react-app recoil-practice
๊ทธ๋ฆฌ๊ณ ๋ฆฌ์ฝ์ผ์ ์ธ์คํจ ํฉ๋๋ค.
cd recoil-practice
npm i recoil
or
yarn add recoil
ํ๋ก์ ํธ์ ๋ฃจํธ ์ปดํฌ๋ํธ์ RecoilRoot๋ฅผ ์ถ๊ฐํฉ๋๋ค. ๋ฆฌ์ฝ์ผ ๋ฃจํธ๋ฅผ ์ค์ ํด์ฃผ์ด์ผ ๋ด๋ถ์์ ๋ฆฌ์ฝ์ผ ์ฌ์ฉ์ด ๊ฐ๋ฅํฉ๋๋ค.
ํ ์ํ๊ด๋ฆฌ์ provider์ ๋น์ทํ ์ญํ ์ ํ๋ ๊ฒ ๊ฐ์ต๋๋ค.
ํ๋ก์ ํธ์ index.js์ ์ถ๊ฐํด์ค๋๋ค.
import React from 'react'
import ReactDOM from 'react-dom'
import './index.css'
import App from './App'
import * as serviceWorker from './serviceWorker'
import { RecoilRoot } from 'recoil'
ReactDOM.render(
<React.StrictMode>
<RecoilRoot>
<App />
</RecoilRoot>
</React.StrictMode>,
document.getElementById('root')
)
Recoil์ Atom์ผ๋ก๋ถํฐ ์์ํด์ Selector๋ฅผ ๊ฑฐ์ณ React ์ปดํฌ๋ํธ๊น์ง ์ ๋ฌ๋๋ ํ๋์ Data-Flow Graph๋ฅผ ๋ง๋ค๊ฒ ํ๋ค. Atom์ ์ปดํฌ๋ํธ๋ค์ด ๊ตฌ๋ ํ ์ ์๋ ๋จ์์ด๊ณ , Selector๋ ๋๊ธฐ์ ํน์ ๋น๋๊ธฐ์ ์ผ๋ก ์ํ๋ฅผ ๋ณํํ๋ค. ์ด ๋๊ฐ์ง์ ํต์ฌ ๊ฐ๋ ์ผ๋ก ์ด๋ฃจ์ด์ง ๋ผ์ด๋ธ๋ฌ๋ฆฌ์ด๋ค.
atom ์ํฐ์ ์ํ์ ์กฐ๊ฐ์ ๋ํ๋ด๋ฉฐ key์ value๋ฅผ ์ค์ ํด์ค๋ค. ๊ธฐ๋ณธ ์ํ์ด๋ค.
import { atom } from 'recoil'
const number = atom({
key: 'number',
default: 0,
})
๋ฐฐ์ด, ๊ฐ์ฒด๋ฅผ ๊ฐ์ผ๋ก ์ฌ์ฉํ ์ ์๋ค.
import { atom } from 'recoil'
const todos = atom({
key: 'todos',
default: ['ํ์ฅ์ค ๊ฐ๊ธฐ', '๋ฐฅ๋จน๊ธฐ'],
})
๊ณต์๋ฌธ์์ ์๋ ํฌ๋๋ฆฌ์คํธ ์์ ๋ฅผ ๋ฐ๋ผํด๋ณด๋ฉฐ ์ ์ฉํ๊ธฐ ์ํด todoList์ปดํฌ๋ํธ๋ฅผ ๋ง๋ญ๋๋ค.
import React from 'react'
import { atom, useRecoilValue } from 'recoil'
const todoListState = atom({
key: 'todoListState',
default: [],
})
function TodoList() {
const todoList = useRecoilValue(todoListState)
return (
<>
{/* <TodoListStats /> */}
{/* <TodoListFilters /> */}
<TodoItemCreater />
{todoList.map(todoItem => (
<TodoItem key={todoItem.id} item={todoItem} />
))}
</>
)
}
export default TodoList
iimport React, { useState } from 'react';
import { useSetRecoilState } from 'recoil';
import { todoListState } from '../recoilState';
function TodoItemCreator() {
const [inputValue, setInputValue] = useState('');
const setTodoList = useSetRecoilState(todoListState);
const additem = () => {
setTodoList(oldTodoList => [
...oldTodoList,
{
id: getId(),
text: inputValue,
isComplete: false
}
]);
setInputValue('');
};
const onChange = ({ target: { value } }) => {
setInputValue(value);
};
return (
<div>
<input type="text" value={inputValue} onChange={onChange} />
<button onClick={additem}>Add</button>
</div>
);
}
export default TodoItemCreator;
let id = 0;
function getId() {
return id++;
}
useSetRecoilStateํ ์ ์ฌ์ฉํด์ ์ํฐ์ผ๋ก ์ ์ํ ์ํ๋ฅผ ์ธ์๋ก ๋ฃ์ด์ฃผ๋ฉด ์ธํฐ ํจ์๋ฅผ ๊ฐ์ ธ์ต๋๋ค. ์ธํฐ ํจ์๋ฅผ ๊ฐ์ ธ์จ ๋ค, ์ธํ ์ํ๊ด๋ฆฌ๋ฅผ ๋ด๋ถ์ํ๋ก ๊ด๋ฆฌํ๊ณ ํฌ๋๋ฆฌ์คํธ ๋ด์ฉ ์ถ๊ฐ ํจ์์ ์ฌ์ฉํ์ต๋๋ค.
function TodoItem({ item }) {
const [todoList, setTodoList] = useRecoilState(todoListState)
const index = todoList.findIndex(listItem => listItem === item)
const editItemText = ({ target: { value } }) => {
const newList = replaceItemAtIndex(todoList, index, {
...item,
text: value,
})
setTodoList(newList)
}
const toggleItemCompletion = () => {
const newList = replaceItemAtIndex(todoList, index, {
...item,
isComplete: !item.isComplete,
})
setTodoList(newList)
}
const deleteItem = () => {
const newList = removeItemAtIndex(todoList, index)
setTodoList(newList)
}
return (
<div>
<input type="text" value={item.text} onChange={editItemText} />
<input
type="checkbox"
checked={item.isComplete}
onChange={toggleItemCompletion}
/>
<button onClick={deleteItem}>X</button>
</div>
)
}
export default TodoItem
function replaceItemAtIndex(arr, index, newValue) {
return [...arr.slice(0, index), newValue, ...arr.slice(index + 1)]
}
function removeItemAtIndex(arr, index) {
return [...arr.slice(0, index), ...arr.slice(index + 1)]
}
TodoItem ์ปดํฌ๋ํธ์์๋ useRecoilStateํ ์ ์ผ๋ฐ useStateํ ์ฒ๋ผ ์ฌ์ฉํด ์ํ๊ฐ๊ณผ ์ธํฐํจ์๋ฅผ ๊ฐ์ ธ์์ต๋๋ค. ์ผ๋ฐ ํฌ๋๋ฆฌ์คํธ ์์ ๋ด์ฉ๊ณผ ๋น์ทํ๋ฐ, delete, edit๋ฐฉ์์ ํธํ๊ฒ ReplaceItemAtIndex์ ๊ฐ์ด ํจ์๋ฅผ ๋ฐ๋ก ๋นผ์ด์ ์ฌ์ฉํฉ๋๋ค.
export const todoListFilterState = atom({
key: 'todoListFilterState',
default: 'Show All',
})
๋ชฉ๋ก์ ํํฐ๋งํ ์ํ๋ฅผ ๊ด๋ฆฌํ๊ธฐ ์ํด todoListFilterState๋ฅผ ์ถ๊ฐํฉ๋๋ค.
export const filteredTodoListState = selector({
key: 'filteredTodoListState',
get: ({ get }) => {
const filter = get(todoListFilterState)
const list = get(todoListState)
switch (filter) {
case 'Show Completed':
return list.filter(item => item.isComplete)
case 'Show Uncompleted':
return list.filter(item => !item.isComplete)
default:
return list
}
},
})
filteredTodoListState๋ผ๋ ํจ์์ selector๋ฅผ ์ฌ์ฉํ์ฌ ํค์ ๊ฒํฐ ํจ์๋ฅผ ์ ๋ฌํ์ฌ ์์ฑํ์์ต๋๋ค.
todoListFilterState์ todoListState ๋ ๊ฐ์ง ์ํ๋ฅผ ์ฌ์ฉํ์ฌ ๊ฐ๊ณต๋ ๋ฐ์ดํฐ๋ฅผ ๋ฐํํ๋๊ฒ ์ด ๋ชฉ์ ์
๋๋ค. ๊ฐ๊ณต๋ ๋ฐ์ดํฐ๋ฅผ ๋ฐ๊ฑฐ๋ ๋ฐ์ดํฐ๋ฅผ ๊ฐ๊ณตํ์ฌ ์ ์ฅํ๊ณ ์ถ์๋ ์ฌ์ฉํฉ๋๋ค.
ํด๋น ์
๋ ํฐ์์ todoListFilterState์ todoListState๋ฅผ ํธ๋ํนํ์ฌ, ๋ณํ๊ฐ ๊ฐ์ง๋๋ฉด ๋ค์ ์คํ๋ฉ๋๋ค.
function TodoList() {
const todoList = useRecoilValue(filteredTodoListState)
return (
<>
<TodoListStats />
<TodoListFilters />
<TodoItemCreator />
{todoList.map(todoItem => (
<TodoItem key={todoItem.id} item={todoItem} />
))}
</>
)
}
export default TodoList
ํฌ๋ ๋ฆฌ์คํธ ์ปดํฌ๋ํธ์ useRecoilValue์์ todolistState์ํ์์ ๊ฐ์ ธ์ค๋ ๊ฐ์ filteredTodoListState์ํ์์ ๊ฐ์ ธ์ค๋๋ก ๋ฐ๊ฟ์ค๋๋ค.
import React from 'react'
import { useRecoilState } from 'recoil'
import { todoListFilterState } from '../recoilState'
function TodoListFilters() {
const [filter, setFilter] = useRecoilState(todoListFilterState)
const updateFilter = ({ target: { value } }) => {
setFilter(value)
}
return (
<>
Filter:
<select value={filter} onChange={updateFilter}>
<option value="Show All">All</option>
<option value="Show Completed">Completed</option>
<option value="Show Uncompleted">Uncompleted</option>
</select>
</>
)
}
export default TodoListFilters
ํํฐ๋ง์ ์ ํํ ์ปดํฌ๋ํธ๋ฅผ ๋ฃ์ด์ค๋๋ค. ์ ๋ ํธ๊ฐ ์ ํ๋๋ฉด ํด๋น ๊ฐ์ผ๋ก todoListFilterState์ ์ํ๋ฅผ ๋ฐ๊ฟ๋๋ค.
export const todoListStatsState = selector({
key: 'todoListStatsState',
get: ({ get }) => {
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
return {
totalNum,
totalCompletedNum,
totalUncompletedNum,
percentCompleted,
}
},
})
๋ฆฌ์คํธ์ ํ ํ ์, ํผ์ผํฐ์ง๋ฅผ ํํํ๊ธฐ์ํด todoListState์ ์ํ๋ฅผ ๊ฐ๊ณตํด ๋ฐํํ๋ ์ ๋ ํฐ ํ๋๋ฅผ ์์ฑํด์ค๋๋ค.
import React from 'react'
import { useRecoilValue } from 'recoil'
import { todoListStatsState } from '../recoilState'
function TodoListStats() {
const {
totalNum,
totalCompletedNum,
totalUncompletedNum,
percentCompleted,
} = useRecoilValue(todoListStatsState)
const formattedPercentCompleted = Math.round(percentCompleted * 100)
return (
<ul>
<li>Total items: {totalNum}</li>
<li>Items completed: {totalCompletedNum}</li>
<li>Items not completed: {totalUncompletedNum}</li>
<li>Percent completed: {formattedPercentCompleted}</li>
</ul>
)
}
export default TodoListStats
์์์ ์ ์ํ ์คํฏ ์ํ๋ฅผ ๊ฐ์ ธ์์ ์ฌ์ฉํ๊ธฐ ์ํด TodoListStats ์ปดํฌ๋ํธ๋ฅผ ๋ง๋ญ๋๋ค. ์ฌ๊ธฐ์์๋ useRecoilValue๋ฅผ ์ฌ์ฉํด ๋ฆฌ์ฝ์ผ ์ณํ์ ๊ฐ์ ๊ฐ์ ธ์์ต๋๋ค.
์ด์ npm run start๋ฅผ ํด์ ์คํ์์ผ ๋ณด๋ฉด, ๊ฐ ํฌ๋๋ฆฌ์คํธ์ ํฉ๊ณ๊ฐ ๋์ค๊ณ , ํํฐ๋ง์ด ์์ผ๋ฉฐ ํ ๋ฐ๋ชฉ๋ก๋ค์ ๋ด์ฉ์ด ์ ๋์ค๋๊ฒ์ ํ์ธํ
์ ์์ต๋๋ค.
๊ธฐ๋ณธ์ ์ธ ํ ๋ฐ๋ชฉ๋ก ์์ ๋ฅผ ๋ฐ๋ผํด๋ณด๋ฉฐ ์ด๋ ์ ๋ Recoil์ ๊ธฐ๋ณธ์ ์ธ ์ฌ์ฉ๋ฐฉ๋ฒ์ ๋ํด์ ์๊ฒ ์ผ๋, ํนํ ์ด๋ค์ ์์ ์ข์์ง๋ ์์ง ์ ๋ชจ๋ฅด๊ฒ ์ต๋๋ค.
์ผ๋จ ์ฌ์ฉ๋ฒ์ด ๋ฆฌ๋์ค์ ๋นํด ๊ฐํธํ๊ณ ์ฌ์ด ๊ฒ ๊ฐ์ผ๋ฉฐ ํ
์ฒ๋ผ ์ฌ์ฉํ๊ธฐ์ ์ต์ํ ์ ๋ ์์ต๋๋ค.
์ด๊ฒ๋ง์ผ๋ก๋ ํฐ ์ฅ์ ์ด ๋๊ฒ ์ง๋งโฆ ๋ค์ ๋น๋๊ธฐ ๋ฐ์ดํฐ ๋ค๋ฃจ๊ธฐ๋
ํ๋ฒ ์งํํด๋ณด๊ฒ ์ต๋๋ค.