https://recoiljs.org/ko/docs/introduction/installation
사용동기
리액트 상태관리의 어려움
- react context
- Context가 필요한 최상위 컴포넌트에 Context를 선언하고 사용하면 되지만
- 한 Context에 하나의 값만 사용가능하기 때문에 다수의 값을 사용하려면 Wrapper Hell에 빠집니다.
- 예시
- redux
- 이해하기 상당히 어렵습니다.
- 리덕스만의 철학을 따르기 위해 만들어야 하는 부수적인 코드가 증가해 생산성이 좋지 않습니다.
- 이해하기 상당히 어렵습니다.
- recoil
- 데이터 흐름이 직관적이고 이해하기 쉽습니다.
- 렌더링이 최적화되어 있습니다. (구독한 atom과 selector가 변경될 시 구독한 컴포넌트만 re-rendering)
주요개념
- recoil을 사용하면 atom(공유 상태)에서 selectors(순수 함수)를 거쳐 react 컴포넌트로 내려가는 data-flow graph를 만들 수 있습니다.
RecoilRoot
- recoil 상태를 사용하면 컴포넌트는 RecoilRoot가 필요합니다.
- 루트 컴포넌트가 RecoilRoot를 넣기에 가장 좋은 장소입니다.
-
import React from 'react'; import { RecoilRoot, atom, selector, useRecoilState, useRecoilValue, } from 'recoil'; function App() { return ( <RecoilRoot> <CharacterCounter /> </RecoilRoot> ); }
- 이 코드에서는 App 하위의 컴포넌트에서 모두 recoil 개념을 사용할 수 있습니다.
Atom
- Atom은 상태(state)의 일부를 나타냅니다.
- 어떤 컴포넌트에서나 읽고 쓸 수 있습니다.
- atom의 값을 읽는 컴포넌트들은 암묵적으로 atom을 구독합니다.
- 그래서 atom에 변화가 발생하면 atom을 구독하는 모든 컴포넌트들이 re-rendering 됩니다.
const todoListState = atom({
key: 'todoListState', // unique ID
default: [], // default value
});
- Atom은 위와 같이 atom() 함수를 사용해 생성합니다.
function TodoList() {
const todoList = useRecoilValue(todoListState);
return (
<>
<TodoListStats />
<TodoListFilters />
<TodoItemCreator />
{todoList.map((todoItem) => (
<TodoItem key={todoItem.id} item={todoItem} />
))}
</>
);
}
- Atom의 값을 위와같이 useRecoilValue(atom)을 통해 읽을 수 있습니다.
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>
);
}
// 고유한 Id 생성을 위한 유틸리티
let id = 0;
function getId() {
return id++;
}
- Atom의 값을 위와 같이 useSetRecoilState(atom)을 통해 수정할 수 있습니다.
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>
);
}
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)];
}
- useRecoilState(atom)의 경우 useRecoilValue(atom)과 useSetRecoilState(atom)의 기능을 모두 가진 함수입니다.
Selector
- Selector는 파생된 상태(derived state)의 일부를 나타냅니다.
- 파생된 상태는 다른 데이터에 의존하는 동적인 데이터를 만들 수 있습니다.
- 예시) 필터링된 todo list, todo list 통계
const todoListFilterState = atom({
key: 'todoListFilterState',
default: 'Show All',
});
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;
}
},
});
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,
};
},
});
- selector는 위와 같이 atom 혹은 selector의 결과에 따라 상태를 파생시킬 수 있는 순수 함수입니다.
- 위의 filteredTodoListState selector의 경우 todoListFilterState Atom과 todoListState Atom을 추적합니다.
- 그래서 Atom 두개 중 하나라도 변하면 이를 구독하고있는 filteredTodoListState 또한 재실행됩니다.
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>
</>
);
}
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>
);
}
- 만들고 있던 todo list를 계속 만들어봅니다.
비동기 데이터 쿼리
- 비동기로 데이터를 받아와서 렌더링해야 할 때도 selector를 사용할 수 있습니다.
- selector에서 get 콜백으로 나온 값 그 자체 대신 Promise를 return하면 인터페이스 그대로 사용할 수 있습니다.
const currentUserIDState = atom({
key: 'CurrentUserID',
default: 1,
});
const currentUserNameQuery = selector({
key: 'CurrentUserName',
get: async ({get}) => {
const response = await myDBQuery({
userID: get(currentUserIDState),
});
if (response.error) {
throw response.error;
}
return response.name;
},
});
function CurrentUserInfo() {
const userName = useRecoilValue(currentUserNameQuery);
return <div>{userName}</div>;
}
function MyApp() {
return (
<RecoilRoot>
<ErrorBoundary>
<React.Suspense fallback={<div>Loading...</div>}>
<CurrentUserInfo />
</React.Suspense>
</ErrorBoundary>
</RecoilRoot>
);
}
- 위 예제에서 currentUserNameQuery는 비동기로 db에서 유저 정보를 받아와 반환해주는 selector 함수입니다.
- MyApp의 Suspense에서 currentUserNameQuery의 값을 받아올 때 까지 fallback에 있는 컴포넌트를 렌더합니다.
- recoil은 보류중인 데이터(currentUserName)를 다루기 위해 React Suspense와 동작하게 되어있습니다.
- currentUserNameQuery에서 error가 발생하면 그 에러가 다른 컴포넌트에 영향이 가지 않게끔 ErrorBoundary로 지정하여 에러를 가둡니다.
const userNameQuery = selectorFamily({
key: 'UserName',
get: (userID) => async () => {
const response = await myDBQuery({userID});
if (response.error) {
throw response.error;
}
return response.name;
},
});
function UserInfo({userID}) {
const userName = useRecoilValue(userNameQuery(userID));
return <div>{userName}</div>;
}
function MyApp() {
return (
<RecoilRoot>
<ErrorBoundary>
<React.Suspense fallback={<div>Loading...</div>}>
<UserInfo userID={1} />
<UserInfo userID={2} />
<UserInfo userID={3} />
</React.Suspense>
</ErrorBoundary>
</RecoilRoot>
);
}
- 매개변수를 기반으로 상태를 가져오고 싶다고 한다면 selectorFamily를 사용해서 가져올 수 있습니다.
const friendsInfoQuery = selector({
key: 'FriendsInfoQuery',
get: ({get}) => {
const {friendList} = get(currentUserInfoQuery);
const friends = get(
waitForAll(friendList.map((friendID) => userInfoQuery(friendID))),
);
return friends;
},
});
- 여러 데이터를 받아와 한번에 렌더링 해야한다면 waitForAll()을 이용해 데이터를 한번에 렌더링할 수 있습니다.
const friendsInfoQuery = selector({
key: 'FriendsInfoQuery',
get: ({get}) => {
const {friendList} = get(currentUserInfoQuery);
const friendLoadables = get(
waitForNone(friendList.map((friendID) => userInfoQuery(friendID))),
);
return friendLoadables
.filter(({state}) => state === 'hasValue')
.map(({contents}) => contents);
},
});
- 여러 데이터를 받아올 때 받아오는 대로 렌더링을 추가하고 싶으면 waitForNone()을 이용해 렌더링할 수 있습니다.
function CurrentUserInfo() {
const currentUser = useRecoilValue(currentUserInfoQuery);
const friends = useRecoilValue(friendsInfoQuery);
const changeUser = useRecoilCallback(({snapshot, set}) => (userID) => {
snapshot.getLoadable(userInfoQuery(userID)); // pre-fetch user info
set(currentUserIDState, userID); // change current user to start new render
});
return (
<div>
<h1>{currentUser.name}</h1>
<ul>
{friends.map((friend) => (
<li key={friend.id} onClick={() => changeUser(friend.id)}>
{friend.name}
</li>
))}
</ul>
</div>
);
}
- useRecoilCallback을 사용하면 먼저 callback함수를 실행하고 rendering을 시킬 수 있습니다.
'공부' 카테고리의 다른 글
쿠버네티스 학습 (0) | 2022.03.24 |
---|---|
RabbitMQ vs Kafka vs Redis (0) | 2022.03.11 |
Log4j 학습 (0) | 2022.01.28 |
Apache vs NGINX, 그리고 NGINX 설정 (0) | 2022.01.26 |