노마드코더의 React JS 마스터 클래스 강의를 듣던 중에 Todo List를 만들면서 다음과 같은 2개의 코드 챌린지가 주어졌다.
- Todo List의 삭제 기능 구현하기
- local Storage를 통해 Recoil state 저장하기
이번 글을 작성하는 이유는, 2번 때문이지만 먼저 1번부터 빠르게 해결해보자.
각각의 Todo마다 고유한 id를 가지고 있기 때문에 전체 Todo List 배열에서 삭제버튼을 누를 때, 해당 id를 가진 Todo를 제외한 나머지 Todo List 배열을 return 해주면 된다.
// TodoList.tsx
import { useSetRecoilState } from 'recoil';
function ToDo({ text, category, id }: IToDo) {
const setToDos = useSetRecoilState(toDoState);
...
const handleDelete = () => {
setToDos((oldToDos) => {
const newToDos = oldToDos.filter((toDo) => toDo.id !== id);
return newToDos;
});
};
return (
<li>
<span>{text}</span>
{category !== Categories.DOING && (
<button name={Categories.DOING} onClick={onClick}>
Doing
</button>
)}
...
<button onClick={handleDelete}>delete</button>
</li>
);
}
export default ToDo;
이번 글을 쓰는 목적인 2번 문제다. 해당 코드 챌린지에 대해서 추가적으로 설명하자면, 위의 코드에도 나와있지만 TodoList state가 Recoil을 통해서 전역으로 관리가 되고 있는 상황이다.
하지만 여기에서 state는 새로고침을 하면 저장된 데이터가 다 날아가기 때문에 열심히 작성해둔 Todo들이 없어진다. 그래서 데이터들을 브라우저 저장소인 localStorage에 저장하는 방법이 있고, 저장을 해두면 애플리케이션이 재실행 되거나 나중에 다시 들어왔을 때도 해당 정보를 state에 넣어주어 사용 할 수 있을 것이다.
해당 방법으로 구현하기 전에 먼저 공식문서를 찾아봤다. 다행히도 공식문서에 관련 내용이 있었고 Recoil에서는 이것을 Local Storage Persistence(로컬 스토리지 지속성)이라고 부른다.
이는 atom 상태를 브라우저 로컬 스토리지에서 유지하기 위해 사용 할 수 있는 방법이고 예제 코드는 여기에서 볼 수 있다. 설명 중에 Atom Effects라는 단어가 등장하는데, 이에 대해서도 공식문서의 설명을 첨부한다.
* Atom Effects란?
Atom Effects는 부수효과를 관리하고 Recoil의 atom을 초기화 또는 동기화하기 위한 API입니다. Atom Effects는 state persistence(상태 지속성), state synchronization(상태 동기화), managing history(히스토리 관리), logging(로깅) 등등 유용한 어플리케이션을 여럿 가지고 있습니다.
이는 React effects 와도 유사하지만, Atom Effects는 atom 정의의 일부로 정의되므로 각 atom은 자체적인 정책들을 지정하고 구성할 수 있습니다.
설명이 길었는데, 본론으로 돌아와서 예제 코드를 참고하여 다음과 같이 구현했다.
// atoms.ts
const localStorageEffect =
(key: string) =>
({ setSelf, onSet }: any) => {
const savedValue = localStorage.getItem(key);
// setSelf -> Callbacks to set or reset the value of the atom.
if (savedValue != null) {
setSelf(JSON.parse(savedValue));
}
// onSet -> Subscribe to changes in the atom value.
onSet((newValue: any, _: any, isReset: boolean) => {
isReset
? localStorage.removeItem(key)
: localStorage.setItem(key, JSON.stringify(newValue));
});
};
export const toDoState = atom<IToDo[]>({
key: 'toDo',
default: [],
effects: [localStorageEffect('toDo')],
});
매개변수의 key는 localStorage에 저장되는 key 값이며 내부의 콜백 함수에서의 객체 속 매개변수로 주어지는 setSelf 함수는 연결된 atom의 값을 초기화 해주는 함수이며, onSet 함수는 해당하는 atom의 값이 변경이 되었을 때 실행되는 함수이다.
onSet 함수의 3번째 매개변수인 isReset은 useResetRecoilState 함수를 통해 atom 상태가 default 값으로 리셋될 때 localStorage의 데이터도 같이 삭제시키는 역할인 것 같다.
정리해보면, React의 useEffect와 비슷하게 toDoState의 변경이 감지되면 effects에 있는 함수가 실행되면서 localStorage에 있는 값을 변화시킨다. 이전에 저장소에 저장된 데이터가 있으면 setSelf 함수가 그 값을 불러와서 초기값으로 지정하고, 이후에 atom이 변경될 때마다 onSet 함수가 실행되면서 atom 상태를 localStorage에 동기화하는 역할을 한다.
직접 구현하지 않고 라이브러리를 사용하는 방법도 있다.
Redux에서도 redux-persist라는 라이브러리를 활용하여 state를 LocalStorage에 보관하여 해당 state를 브라우저가 꺼지더라도 유지할 수 있는데, recoil에서도 recoil-persist라는 비슷한 라이브러리가 있다.
하지만, 해당 라이브러리는 recoil의 최신 버전에서 지원되지 않으며 최근 업데이트가 된지 1년이나 넘었을 정도로 관리가 되고 있지 않아 안정성 이슈가 있다고 한다.
직접 확인해본 결과, 올해 3월에 4.2.0 버전을 릴리즈하면서 recoil 0.7.2 버전까지는 지원을 하는 것 같다. 2022년 8월 기준 recoil의 가장 최신 버전은 0.7.4이며 해당 버전에서 recoil-persist를 사용해본 결과, 문제없이 잘 작동하긴 한다.
업데이트가 잘 안되는건.. 맞는 것 같다. 최근 몇 달 동안 recoil 최신 버전을 지원하는 것 외에 별 다른 업데이트가 없었다. 해당 내용에 대해 더 궁금하다면, recoil-persist의 github를 참고해보면 될 것 같다.
어쨌든, recoil-persist의 예제도 같이 작성해봤다. 비교적 코드가 간결해지긴 하지만 Recoil의 짧은 업데이트 주기 상 언제까지 호환이 될지 몰라 추후에 문제가 발생할 수도 있다. 그래서 해당 방법은 사용하지 않고, 위에서 작성했던 recoil에서 제공하는 기본 기능을 통해 구현할 것이다.
const { persistAtom } = recoilPersist({
key: 'todoLocal',
storage: localStorage,
});
export const toDoState = atom<IToDo[]>({
key: 'toDo',
default: [],
effects_UNSTABLE: [persistAtom],
});
Recoil이 아무래도 Redux보단 레퍼런스가 적어 공부하기 어려운 부분이 있지만, 최근 토스를 비롯한 여럿 팀에서 도입해서 사용하고 있기도 하고 팀 프로젝트를 진행하면서 실제로 사용해봤을 때도 꽤 만족(Redux의 보일러플레이트에 비하면..)했기 때문에 계속 사용해볼 예정이다. 공식문서가 한글화도 돼있고 설명도 나름 잘되어있다고 생각한다.
https://tech.osci.kr/2022/07/05/recoil-react-js-state-management/
https://recoiljs.org/ko/docs/guides/atom-effects
https://www.npmjs.com/package/recoil-persist
8월에 쓰신 글이라 지금은 어떨지 잘 모르겠지만 둘러보다가 틀린 부분이 있어 남깁니다.
"Redux에서도 redux-persist라는 라이브러리를 활용하여 state를 LocalStorage 혹은 SessionStorage에 보관하여 해당 state를 브라우저가 꺼지더라도 유지할 수 있는데, recoil에서도 recoil-persist라는 비슷한 라이브러리가 있다."
여기서 localStorage는 브라우저가 종료 되어도 정보가 유지되는 특성을 가지고 있구요
sessionStorage는 브라우저가 종료되면 정보도 사라집니다.
sessionStorage에 대한 자세한 정보 링크 남깁니다
https://developer.mozilla.org/ko/docs/Web/API/Window/sessionStorage