JotaiJotai

상태
React를 위한 기본적이고 유연한 상태 관리

Persistence

아톰(atom)을 영구적으로 저장하는 방법에 대해 알아봅니다. 아톰은 상태 관리 라이브러리에서 사용되는 기본 단위로, 애플리케이션의 상태를 나타냅니다. 아톰을 영구적으로 저장하면, 페이지를 새로 고침하거나 애플리케이션을 다시 시작해도 이전 상태를 유지할 수 있습니다.

아톰을 영구적으로 저장하려면, atom 함수에 persist 옵션을 추가해야 합니다. 이 옵션은 아톰의 상태를 로컬 스토리지나 세션 스토리지와 같은 브라우저 저장소에 저장하도록 설정합니다.

import { atom } from 'recoil';
const counterState = atom({
key: 'counterState',
default: 0,
persist: true,
});

위 예제에서 counterState 아톰은 persist: true 옵션을 통해 영구적으로 저장됩니다. 이제 이 아톰의 상태는 브라우저의 로컬 스토리지에 저장되며, 페이지를 새로 고침해도 이전 상태가 유지됩니다.

또한, persist 옵션에 저장소를 직접 지정할 수도 있습니다. 예를 들어, 세션 스토리지를 사용하려면 다음과 같이 설정할 수 있습니다.

import { atom } from 'recoil';
const counterState = atom({
key: 'counterState',
default: 0,
persist: {
type: 'session',
},
});

이렇게 하면 counterState 아톰의 상태는 세션 스토리지에 저장됩니다. 세션 스토리지는 브라우저를 닫을 때까지 데이터를 유지하지만, 브라우저를 닫으면 데이터가 삭제됩니다.

아톰을 영구적으로 저장하는 기능은 사용자 설정이나 폼 데이터와 같이 중요한 상태를 유지해야 할 때 매우 유용합니다. 이를 통해 사용자 경험을 향상시키고, 데이터 손실을 방지할 수 있습니다.

Jotai는 sessionStorage, localStorage, AsyncStorage, 또는 URL 해시에 상태를 저장할 수 있는 utils 번들에 있는 atomWithStorage 함수를 제공합니다.

(참고: 이 가이드는 약간 오래되었고, 일부 수정이 필요합니다.)

여기에는 몇 가지 대체 구현도 있습니다:

localStorage를 활용한 간단한 패턴

const strAtom = atom(localStorage.getItem('myKey') ?? 'foo')
const strAtomWithPersistence = atom(
(get) => get(strAtom),
(get, set, newStr) => {
set(strAtom, newStr)
localStorage.setItem('myKey', newStr)
},
)

localStorage와 JSON 파싱을 활용한 헬퍼 함수

const atomWithLocalStorage = (key, initialValue) => {
const getInitialValue = () => {
const item = localStorage.getItem(key)
if (item !== null) {
return JSON.parse(item)
}
return initialValue
}
const baseAtom = atom(getInitialValue())
const derivedAtom = atom(
(get) => get(baseAtom),
(get, set, update) => {
const nextValue =
typeof update === 'function' ? update(get(baseAtom)) : update
set(baseAtom, nextValue)
localStorage.setItem(key, JSON.stringify(nextValue))
},
)
return derivedAtom
}

(에러 처리가 추가되어야 합니다.)

AsyncStorage와 JSON 파싱을 사용한 헬퍼 함수

이 기능은 onMount가 필요합니다.

const atomWithAsyncStorage = (key, initialValue) => {
const baseAtom = atom(initialValue)
baseAtom.onMount = (setValue) => {
;(async () => {
const item = await AsyncStorage.getItem(key)
setValue(JSON.parse(item))
})()
}
const derivedAtom = atom(
(get) => get(baseAtom),
(get, set, update) => {
const nextValue =
typeof update === 'function' ? update(get(baseAtom)) : update
set(baseAtom, nextValue)
AsyncStorage.setItem(key, JSON.stringify(nextValue))
},
)
return derivedAtom
}

비동기 아톰을 사용하는 방법에 대한 자세한 내용은 Async 문서를 확인하세요.

sessionStorage 예제

AsyncStorage와 동일하게 atomWithStorage 유틸리티를 사용하고, 기본 스토리지를 sessionStorage로 재정의하면 됩니다.

import { atomWithStorage, createJSONStorage } from 'jotai/utils'
const storage = createJSONStorage(() => sessionStorage)
const someAtom = atomWithStorage('some-key', someInitialValue, storage)

직렬화 아톰 패턴

type Actions =
| { type: 'serialize'; callback: (value: string) => void }
| { type: 'deserialize'; value: string }
const serializeAtom = atom(null, (get, set, action: Actions) => {
if (action.type === 'serialize') {
const obj = {
todos: get(todosAtom).map(get),
}
action.callback(JSON.stringify(obj))
} else if (action.type === 'deserialize') {
const obj = JSON.parse(action.value)
// 에러 처리와 타입 검사가 필요함
set(
todosAtom,
obj.todos.map((todo: Todo) => atom(todo)),
)
}
})
const Persist = () => {
const [, dispatch] = useAtom(serializeAtom)
const save = () => {
dispatch({
type: 'serialize',
callback: (value) => {
localStorage.setItem('serializedTodos', value)
},
})
}
const load = () => {
const value = localStorage.getItem('serializedTodos')
if (value) {
dispatch({ type: 'deserialize', value })
}
}
return (
<div>
<button onClick={save}>localStorage에 저장</button>
<button onClick={load}>localStorage에서 불러오기</button>
</div>
)
}

예제

atomFamily를 사용한 패턴

type Actions =
| { type: 'serialize'; callback: (value: string) => void }
| { type: 'deserialize'; value: string }
const serializeAtom = atom(null, (get, set, action: Actions) => {
if (action.type === 'serialize') {
const todos = get(todosAtom)
const todoMap: Record<string, { title: string; completed: boolean }> = {}
todos.forEach((id) => {
todoMap[id] = get(todoAtomFamily({ id }))
})
const obj = {
todos,
todoMap,
filter: get(filterAtom),
}
action.callback(JSON.stringify(obj))
} else if (action.type === 'deserialize') {
const obj = JSON.parse(action.value)
// 에러 처리와 타입 검사가 필요함
set(filterAtom, obj.filter)
obj.todos.forEach((id: string) => {
const todo = obj.todoMap[id]
set(todoAtomFamily({ id, ...todo }), todo)
})
set(todosAtom, obj.todos)
}
})
const Persist = () => {
const [, dispatch] = useAtom(serializeAtom)
const save = () => {
dispatch({
type: 'serialize',
callback: (value) => {
localStorage.setItem('serializedTodos', value)
},
})
}
const load = () => {
const value = localStorage.getItem('serializedTodos')
if (value) {
dispatch({ type: 'deserialize', value })
}
}
return (
<div>
<button onClick={save}>Save to localStorage</button>
<button onClick={load}>Load from localStorage</button>
</div>
)
}