Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat-fe: useLocalStorageState 구현 #964

Merged
merged 11 commits into from
Dec 20, 2024
61 changes: 61 additions & 0 deletions frontend/src/hooks/useLocalStorageState/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { useState } from 'react';

interface OptionProp {
key: string;
enableStorage?: boolean;
}

const safeParseJSON = <T>(value: string | null, fallback: T): T => {
try {
return value !== null ? JSON.parse(value) : fallback;
} catch (error) {
if (process.env.NODE_ENV === 'development') {
console.error('JSON 파싱 실패:', error);
}
return fallback;
}
};

const safeStringifyJSON = <T>(value: T): string | null => {
try {
return JSON.stringify(value);
} catch (error) {
if (process.env.NODE_ENV === 'development') {
console.error('JSON 직렬화 실패:', error);
}
return null;
}
};

Comment on lines +8 to +29
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

parsing, stringify 로직을 깔끔하게 분리해주셨네요 👍

/**
* useLocalStorageState
* @param initialValue - 초기 상태 값
* @param option - { key: LocalStorage에 저장될 키 값, enableStorage: LocalStorage의 값을 사용할지 여부}
* @returns [상태 값, 상태를 변경하는 함수] useState의 반환값과 동일합니다.
*/
function useLocalStorageState<T>(initialValue: T, option: OptionProp): [T, (value: T | ((prev: T) => T)) => void] {
const { key, enableStorage = true } = option;

const [state, _setState] = useState<T>(() => {
if (!enableStorage) return initialValue;

const storedValue = window.localStorage.getItem(key);
return safeParseJSON(storedValue, initialValue);
});

const saveToLocalStorage = (value: T) => {
const stringifiedValue = safeStringifyJSON(value);
if (stringifiedValue !== null) {
window.localStorage.setItem(key, stringifiedValue);
}
};

const setState = (value: T | ((prev: T) => T)) => {
_setState(value);
saveToLocalStorage(value instanceof Function ? value(state) : value);
};

return [state, setState];
}

export default useLocalStorageState;
122 changes: 122 additions & 0 deletions frontend/src/hooks/useLocalStorageState/useLocalStorageState.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
/* eslint-disable function-paren-newline */
import { act } from 'react';
import { renderHook } from '@testing-library/react';
import useLocalStorageState from '.';

describe('useLocalStorageState의 값이 원시값인 경우에 대한 테스트', () => {
beforeEach(() => {
window.localStorage.clear();
});

describe('Primitive Value Tests', () => {
it('초기 상태를 설정한다', () => {
const { result } = renderHook(() => useLocalStorageState(0, { key: 'primitiveKey' }));
expect(result.current[0]).toBe(0);
});

it('[setState 인자 원시값] 상태 변경 시 localStorage에 저장한다', () => {
const { result } = renderHook(() => useLocalStorageState(0, { key: 'primitiveKey' }));

act(() => {
result.current[1]((prev) => prev + 1);
});

expect(result.current[0]).toBe(1);
expect(window.localStorage.getItem('primitiveKey')).toBe('1');
});

it('[setState 인자 함수] 상태 변경 시 localStorage에 저장한다', () => {
const { result } = renderHook(() => useLocalStorageState(0, { key: 'primitiveKey' }));

act(() => {
result.current[1]((prev) => prev + 1);
});

expect(result.current[0]).toBe(1);
expect(window.localStorage.getItem('primitiveKey')).toBe('1');
});

it('localStorage에 값이 있으면 초기 상태로 사용한다', () => {
window.localStorage.setItem('primitiveKey', '10');

const { result } = renderHook(() => useLocalStorageState(0, { key: 'primitiveKey' }));
expect(result.current[0]).toBe(10);

act(() => {
result.current[1]((prev) => prev + 1);
});

expect(result.current[0]).toBe(11);
expect(window.localStorage.getItem('primitiveKey')).toBe('11');
});

it('enableStorage 옵션을 false로 지정한 경우 localStorage를 사용하지 않는다', () => {
window.localStorage.setItem('primitiveKey', '10');

const { result } = renderHook(() => useLocalStorageState(0, { key: 'primitiveKey', enableStorage: false }));

expect(result.current[0]).toBe(0);
expect(window.localStorage.getItem('primitiveKey')).toBe('10');
});
});

describe('useLocalStorageState의 값이 객체인 경우에 대한 테스트', () => {
it('초기 상태를 설정한다', () => {
const initialObject = { name: 'lurgi', age: 30 };
const { result } = renderHook(() => useLocalStorageState(initialObject, { key: 'objectKey' }));
expect(result.current[0]).toEqual(initialObject);
});

it('[setState 인자 원시값] 상태 변경 시 localStorage에 저장한다', () => {
const initialObject = { name: 'lurgi', age: 30 };
const { result } = renderHook(() => useLocalStorageState(initialObject, { key: 'objectKey' }));

act(() => {
result.current[1]({ name: 'lurgi', age: 31 });
});

expect(result.current[0]).toEqual({ name: 'lurgi', age: 31 });
expect(window.localStorage.getItem('objectKey')).toBe(JSON.stringify({ name: 'lurgi', age: 31 }));
});

it('[setState 인자 함수] 상태 변경 시 localStorage에 저장한다', () => {
const initialObject = { name: 'lurgi', age: 30 };
const { result } = renderHook(() => useLocalStorageState(initialObject, { key: 'objectKey' }));

act(() => {
result.current[1]((prev) => ({ ...prev, age: prev.age + 1 }));
});

expect(result.current[0]).toEqual({ name: 'lurgi', age: 31 });
expect(window.localStorage.getItem('objectKey')).toBe(JSON.stringify({ name: 'lurgi', age: 31 }));
});

it('localStorage에 값이 있으면 초기 상태로 사용한다', () => {
const storedObject = JSON.stringify({ name: 'jeong woo', age: 28 });
window.localStorage.setItem('objectKey', storedObject);

const { result } = renderHook(() => useLocalStorageState({ name: 'default', age: 0 }, { key: 'objectKey' }));
expect(result.current[0]).toEqual({ name: 'jeong woo', age: 28 });

act(() => {
result.current[1]((prev) => ({ ...prev, age: prev.age + 1 }));
});

expect(result.current[0]).toEqual({ name: 'jeong woo', age: 29 });
expect(window.localStorage.getItem('objectKey')).toBe(JSON.stringify({ name: 'jeong woo', age: 29 }));
});

it('enableStorage 옵션을 false로 지정한 경우 localStorage를 사용하지 않는다', () => {
const storedObject = JSON.stringify({ name: 'jeong woo', age: 28 });
window.localStorage.setItem('objectKey', storedObject);

const initialObject = { name: 'lurgi', age: 30 };
const { result } = renderHook(() =>
useLocalStorageState(initialObject, { key: 'objectKey', enableStorage: false }),
);

expect(result.current[0]).toEqual(initialObject);
expect(window.localStorage.getItem('objectKey')).toBe(storedObject);
});
});
});
Loading