localStorage를 통해 칸반의 추가, 삭제, 업데이트, 드래그, 드랍을 하고자 변경감지될 때마다 localStorage에서 꺼내고 빼서 쓸 수 있는 Hook이 필요했다.
import React from "react";
import { useEventCallback, useEventListener } from "usehooks-ts";
function useLocalStorage<T>(key: string, initialValue: T) {
const [storedValue, setStoredValue] = useState<T>(() => {
if (typeof window === "undefined") {
return initialValue;
}
try {
const item = window.localStorage.getItem(key);
return item ? JSON.parse(item) : initialValue;
} catch (error) {
console.log(error);
return initialValue;
}
});
const setValue = (value: T | ((val: T) => T)) => {
try {
const valueToStore =
value instanceof Function ? value(storedValue) : value;
setStoredValue(valueToStore);
window.localStorage.setItem(key, JSON.stringify(valueToStore));
} catch (error) {
console.log(error);
}
};
return [storedValue, setValue] as const;
}
export default useLocalStorage;
초기의 state값으로 localStorage에서 getItem(key) 메소드를 호출했을 때 해당 key에 대한 항목이 존재하면 string 형태로 반환하며, 없으면 null을 반환한다.
해당 key에 대한 항목이 존재한다면 사용자가 그 값을 한 번이라도 저장했다는 소리가 된다. 그러니까 초기값 대신 저장된 값을 반환해야 한다.
반면 null이 반환되었을 경우 사용자가 값을 저장한 적이 없거나 삭제했다는 의미이기 때문에 초기값을 반환해야 한다. 따라서 어떤 key에 대해 읽어진 값은 아래와 같이 쓸 수 있다.
if (typeof window === "undefined") {
return initialValue;
}
window is undefined
오류가 발생할 수 있다. localStorage의 setItem(key, value) 메소드를 이용하면 값을 쓸 수 있다. 여기서 value에는 반드시 string 만 전달해야 한다. 저장할 객체가 string이 아닌 경우 일반적으로는 JSON.stringify를 이용해 string 형태로 변환한다. 따라서 값을 쓰는 부분은 아래와 같이 쓸 수 있다.
const valueToStore = value instanceof Function ? value(storedValue) : value;
새로고침시에 동기화가 되지 않는 문제 발생
- 칸반의 카드를 추가하거나 업데이트 삭제, drag, drop할 경우 화면에서는 바뀌지만
- 새로 고침을 할 경우, 맨 마지막에 했던 동작에 관해서만 동기화가 되었다..
- 다른 코드에서 localStorage를 같이 사용할 수 있지만, 거기서 값을 업데이트할 경우, 이 훅에서는 알 수 있는 방법이 없다.
- 따라서, 기존의 값을 계속 들고 있게 되는 현상으로 화면 랜더링에서 일관성이 깨지므로, 새로고침시 동기화가 되지 않았다.
따라서, 값을 구독할 수 있도록 event를 감지할 수 있는 기능이 필요하다.
localStorage 의 변화가 있으면 custom event 를 발생시키도록 하고, 이를 수신하면서 값의 업데이트 여부를 알아내는 것이 필요하다.
usehooks-ts: useLocalStorage
에서 확인할 수 있다.
const setValue = (value: T | ((val: T) => T)) => {
try {
const valueToStore =
value instanceof Function ? value(storedValue) : value;
window.localStorage.setItem(key, JSON.stringify(valueToStore));
setStoredValue(valueToStore);
window.dispatchEvent(new Event("local-storage"));
} catch (error) {
console.log(error);
}
};
setValue를 사용할 때, setState후, dispatchEvent(event)
를 호출해 요소에 있는 이벤트를 실행
시켜 모든 useLocalStorage 후크가 알림을 받도록 맞춤 이벤트를 한다.
커스텀 이벤트인 local-storage
로 이벤트 핸들러가 일반 브라우저 이벤트처럼 이벤트에 반응할 수 있게 된다.
// this only works for other documents, not the current one
useEventListener('storage', handleStorageChange)
// this is a custom event, triggered in writeValueToLocalStorage
// See: useLocalStorage()
useEventListener('local-storage', handleStorageChange)
'storage'
의 업데이트 시 커스텀 이벤트 'local-storage'
를 발생시키고 여기에 대한 핸들러를 붙여 사용하고 있다.
import React from "react";
import { useEventListener } from "usehooks-ts";
function useLocalStorage<T>(key: string, initialValue: T) {
const readValue = React.useCallback((): T => {
// "window is undefined" 에러 방지
if (typeof window === "undefined") {
return initialValue;
}
try {
const item = window.localStorage.getItem(key);
return item ? JSON.parse(item) : initialValue;
} catch (error) {
console.warn(`Error reading localStorage key “${key}”:`, error);
return initialValue;
}
}, [initialValue, key]);
const [storedValue, setStoredValue] = React.useState<T>(readValue);
const setValue = (value: T | ((val: T) => T)) => {
try {
const valueToStore =
value instanceof Function ? value(storedValue) : value;
window.localStorage.setItem(key, JSON.stringify(valueToStore));
setStoredValue(valueToStore);
window.dispatchEvent(new Event("local-storage"));
} catch (error) {
console.log(error);
}
};
React.useEffect(() => {
setStoredValue(readValue());
}, []);
const handleStorageChange = React.useCallback(
(event: StorageEvent | CustomEvent) => {
if ((event as StorageEvent)?.key && (event as StorageEvent).key !== key) {
return;
}
setStoredValue(readValue());
},
[key, readValue]
);
useEventListener("storage", handleStorageChange);
useEventListener("local-storage", handleStorageChange);
return [storedValue, setValue] as const;
}
export default useLocalStorage;