From 86be480d565d6da60e1237858e0cc2ff11592fbe Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 18 Dec 2024 08:13:58 +0000 Subject: [PATCH 01/11] Create draft PR for #963 From 21ba4f921967eb5e40e4f5d60670ef6aa2e57fea Mon Sep 17 00:00:00 2001 From: Jeongwoo Park Date: Wed, 18 Dec 2024 22:42:12 +0900 Subject: [PATCH 02/11] =?UTF-8?q?test:=20useLocalStorageState=20=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20=EC=BD=94=EB=93=9C=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../useLocalStorageState.test.tsx | 87 +++++++++++++++++++ 1 file changed, 87 insertions(+) create mode 100644 frontend/src/hooks/useLocalStorageState/useLocalStorageState.test.tsx diff --git a/frontend/src/hooks/useLocalStorageState/useLocalStorageState.test.tsx b/frontend/src/hooks/useLocalStorageState/useLocalStorageState.test.tsx new file mode 100644 index 00000000..f912c326 --- /dev/null +++ b/frontend/src/hooks/useLocalStorageState/useLocalStorageState.test.tsx @@ -0,0 +1,87 @@ +/* eslint-disable react/function-component-definition */ +/* eslint-disable react/jsx-one-expression-per-line */ +/* eslint-disable react/button-has-type */ +import { render, fireEvent, screen } from '@testing-library/react'; +import useLocalStorageState from '.'; +import '@testing-library/jest-dom'; + +// 1. 원시값을 테스트하는 컴포넌트 +const PrimitiveValueComponent = () => { + const [value, setValue] = useLocalStorageState('primitiveKey', 0); + + return ( +
+

Value: {value}

+ + +
+ ); +}; + +// 2. 객체를 테스트하는 컴포넌트 +const ObjectValueComponent = () => { + const [user, setUser] = useLocalStorageState<{ name: string; age: number }>('objectKey', { + name: 'lurgi', + age: 30, + }); + + return ( +
+

+ Name: {user.name}, Age: {user.age} +

+ +
+ ); +}; + +describe('useLocalStorageState의 값이 원시값인 경우에 대한 테스트', () => { + beforeEach(() => { + window.localStorage.clear(); + }); + + // 1. 원시값 테스트 + describe('Primitive Value Tests', () => { + it('초기 상태를 설정한다', () => { + render(); + expect(screen.getByText('Value: 0')).toBeInTheDocument(); + }); + + it('상태 변경 시 localStorage에 저장한다', () => { + render(); + fireEvent.click(screen.getByText('Increment')); + expect(screen.getByText('Value: 1')).toBeInTheDocument(); + expect(window.localStorage.getItem('primitiveKey')).toBe('1'); + }); + + it('localStorage에 값이 있으면 초기 상태로 사용한다', () => { + window.localStorage.setItem('primitiveKey', '10'); + render(); + expect(screen.getByText('Value: 10')).toBeInTheDocument(); + }); + }); + + describe('useLocalStorageState의 값이 객체인 경우에 대한 테스트', () => { + it('초기 상태를 설정한다', () => { + render(); + expect(screen.getByText('Name: lurgi, Age: 30')).toBeInTheDocument(); + }); + + it('상태 변경 시 localStorage에 저장한다', () => { + render(); + fireEvent.click(screen.getByText('Increase Age')); + expect(screen.getByText('Name: lurgi, Age: 31')).toBeInTheDocument(); + + const storedValue = window.localStorage.getItem('objectKey'); + expect(storedValue).toBe(JSON.stringify({ name: 'lurgi', age: 31 })); + }); + + it('localStorage에 값이 있으면 초기 상태로 사용한다', () => { + const storedObject = JSON.stringify({ name: 'Jane Doe', age: 28 }); + window.localStorage.setItem('objectKey', storedObject); + + render(); + expect(screen.getByText('Name: Jane Doe, Age: 28')).toBeInTheDocument(); + }); + }); +}); From 3b0dfdb776f4c413d87598b6216e3b317c34bb0f Mon Sep 17 00:00:00 2001 From: Jeongwoo Park Date: Wed, 18 Dec 2024 22:42:24 +0900 Subject: [PATCH 03/11] =?UTF-8?q?feat:=20useLocalStorageState=20hook=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/hooks/useLocalStorageState/index.ts | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 frontend/src/hooks/useLocalStorageState/index.ts diff --git a/frontend/src/hooks/useLocalStorageState/index.ts b/frontend/src/hooks/useLocalStorageState/index.ts new file mode 100644 index 00000000..a4053cad --- /dev/null +++ b/frontend/src/hooks/useLocalStorageState/index.ts @@ -0,0 +1,32 @@ +import { useState } from 'react'; + +/** + * useLocalStorageState + * @param key - LocalStorage에 저장될 키 값 + * @param initialValue - 초기 상태 값 + * @returns [상태 값, 상태를 변경하는 함수] useState의 반환값과 동일합니다. + */ +function useLocalStorageState(key: string, initialValue: T): [T, (value: T | ((prev: T) => T)) => void] { + const [state, _setState] = useState(() => { + try { + const storedValue = window.localStorage.getItem(key); + return storedValue !== null ? JSON.parse(storedValue) : initialValue; + } catch (error) { + return initialValue; + } + }); + + const setState = (value: T | ((prev: T) => T)) => { + try { + const newState = value instanceof Function ? value(state) : value; + _setState(newState); + window.localStorage.setItem(key, JSON.stringify(newState)); + } catch (error) { + console.error(`"${key}":`, error); + } + }; + + return [state, setState]; +} + +export default useLocalStorageState; From c709bcda61f68aaaafa038530e86ae87627f70e4 Mon Sep 17 00:00:00 2001 From: Jeongwoo Park Date: Wed, 18 Dec 2024 22:54:09 +0900 Subject: [PATCH 04/11] =?UTF-8?q?test:=20useLocalStorageState=20=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20=EC=BC=80=EC=9D=B4=EC=8A=A4=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../useLocalStorageState.test.tsx | 33 +++++++++++++++---- 1 file changed, 26 insertions(+), 7 deletions(-) diff --git a/frontend/src/hooks/useLocalStorageState/useLocalStorageState.test.tsx b/frontend/src/hooks/useLocalStorageState/useLocalStorageState.test.tsx index f912c326..02d99604 100644 --- a/frontend/src/hooks/useLocalStorageState/useLocalStorageState.test.tsx +++ b/frontend/src/hooks/useLocalStorageState/useLocalStorageState.test.tsx @@ -5,7 +5,6 @@ import { render, fireEvent, screen } from '@testing-library/react'; import useLocalStorageState from '.'; import '@testing-library/jest-dom'; -// 1. 원시값을 테스트하는 컴포넌트 const PrimitiveValueComponent = () => { const [value, setValue] = useLocalStorageState('primitiveKey', 0); @@ -13,12 +12,12 @@ const PrimitiveValueComponent = () => {

Value: {value}

+
); }; -// 2. 객체를 테스트하는 컴포넌트 const ObjectValueComponent = () => { const [user, setUser] = useLocalStorageState<{ name: string; age: number }>('objectKey', { name: 'lurgi', @@ -31,6 +30,7 @@ const ObjectValueComponent = () => { Name: {user.name}, Age: {user.age}

+ ); }; @@ -40,24 +40,32 @@ describe('useLocalStorageState의 값이 원시값인 경우에 대한 테스트 window.localStorage.clear(); }); - // 1. 원시값 테스트 describe('Primitive Value Tests', () => { it('초기 상태를 설정한다', () => { render(); expect(screen.getByText('Value: 0')).toBeInTheDocument(); }); - it('상태 변경 시 localStorage에 저장한다', () => { + it('[setState 인자 원시값] 상태 변경 시 localStorage에 저장한다', () => { render(); fireEvent.click(screen.getByText('Increment')); expect(screen.getByText('Value: 1')).toBeInTheDocument(); expect(window.localStorage.getItem('primitiveKey')).toBe('1'); }); + it('[setState 인자 함수] 상태 변경 시 localStorage에 저장한다', () => { + render(); + fireEvent.click(screen.getByText('Increment2')); + expect(screen.getByText('Value: 1')).toBeInTheDocument(); + expect(window.localStorage.getItem('primitiveKey')).toBe('1'); + }); + it('localStorage에 값이 있으면 초기 상태로 사용한다', () => { window.localStorage.setItem('primitiveKey', '10'); render(); expect(screen.getByText('Value: 10')).toBeInTheDocument(); + fireEvent.click(screen.getByText('Increment2')); + expect(window.localStorage.getItem('primitiveKey')).toBe('11'); }); }); @@ -67,7 +75,7 @@ describe('useLocalStorageState의 값이 원시값인 경우에 대한 테스트 expect(screen.getByText('Name: lurgi, Age: 30')).toBeInTheDocument(); }); - it('상태 변경 시 localStorage에 저장한다', () => { + it('[setState 인자 원시값] 상태 변경 시 localStorage에 저장한다', () => { render(); fireEvent.click(screen.getByText('Increase Age')); expect(screen.getByText('Name: lurgi, Age: 31')).toBeInTheDocument(); @@ -76,12 +84,23 @@ describe('useLocalStorageState의 값이 원시값인 경우에 대한 테스트 expect(storedValue).toBe(JSON.stringify({ name: 'lurgi', age: 31 })); }); + it('[setState 인자 함수] 상태 변경 시 localStorage에 저장한다', () => { + render(); + fireEvent.click(screen.getByText('Increase Age2')); + expect(screen.getByText('Name: lurgi, Age: 31')).toBeInTheDocument(); + + const storedValue = window.localStorage.getItem('objectKey'); + expect(storedValue).toBe(JSON.stringify({ name: 'lurgi', age: 31 })); + }); + it('localStorage에 값이 있으면 초기 상태로 사용한다', () => { - const storedObject = JSON.stringify({ name: 'Jane Doe', age: 28 }); + const storedObject = JSON.stringify({ name: 'jeong woo', age: 28 }); window.localStorage.setItem('objectKey', storedObject); render(); - expect(screen.getByText('Name: Jane Doe, Age: 28')).toBeInTheDocument(); + expect(screen.getByText('Name: jeong woo, Age: 28')).toBeInTheDocument(); + fireEvent.click(screen.getByText('Increase Age2')); + expect(screen.getByText('Name: jeong woo, Age: 29')).toBeInTheDocument(); }); }); }); From cad53675bfc3dbcf10def49d762b3fee0ab5afe0 Mon Sep 17 00:00:00 2001 From: Jeongwoo Park Date: Wed, 18 Dec 2024 22:54:28 +0900 Subject: [PATCH 05/11] =?UTF-8?q?refactor:=20setState=20=EC=9D=B8=EC=9E=90?= =?UTF-8?q?=20=EA=B0=92=20=EB=AA=85=EC=8B=9C=EC=A0=81=EC=9C=BC=EB=A1=9C=20?= =?UTF-8?q?=EB=B3=80=ED=99=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/hooks/useLocalStorageState/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/hooks/useLocalStorageState/index.ts b/frontend/src/hooks/useLocalStorageState/index.ts index a4053cad..a87611b2 100644 --- a/frontend/src/hooks/useLocalStorageState/index.ts +++ b/frontend/src/hooks/useLocalStorageState/index.ts @@ -19,7 +19,7 @@ function useLocalStorageState(key: string, initialValue: T): [T, (value: T | const setState = (value: T | ((prev: T) => T)) => { try { const newState = value instanceof Function ? value(state) : value; - _setState(newState); + _setState(value); window.localStorage.setItem(key, JSON.stringify(newState)); } catch (error) { console.error(`"${key}":`, error); From f9b52cda2db974b863c843880f994347c7b45ed1 Mon Sep 17 00:00:00 2001 From: Jeongwoo Park Date: Thu, 19 Dec 2024 16:14:03 +0900 Subject: [PATCH 06/11] =?UTF-8?q?test:=20=EC=BB=B4=ED=8F=AC=EB=84=8C?= =?UTF-8?q?=ED=8A=B8=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=ED=9B=85=20=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../useLocalStorageState.test.tsx | 117 ++++++++---------- 1 file changed, 55 insertions(+), 62 deletions(-) diff --git a/frontend/src/hooks/useLocalStorageState/useLocalStorageState.test.tsx b/frontend/src/hooks/useLocalStorageState/useLocalStorageState.test.tsx index 02d99604..ba2168d2 100644 --- a/frontend/src/hooks/useLocalStorageState/useLocalStorageState.test.tsx +++ b/frontend/src/hooks/useLocalStorageState/useLocalStorageState.test.tsx @@ -1,39 +1,6 @@ -/* eslint-disable react/function-component-definition */ -/* eslint-disable react/jsx-one-expression-per-line */ -/* eslint-disable react/button-has-type */ -import { render, fireEvent, screen } from '@testing-library/react'; +import { act } from 'react'; +import { renderHook } from '@testing-library/react'; import useLocalStorageState from '.'; -import '@testing-library/jest-dom'; - -const PrimitiveValueComponent = () => { - const [value, setValue] = useLocalStorageState('primitiveKey', 0); - - return ( -
-

Value: {value}

- - - -
- ); -}; - -const ObjectValueComponent = () => { - const [user, setUser] = useLocalStorageState<{ name: string; age: number }>('objectKey', { - name: 'lurgi', - age: 30, - }); - - return ( -
-

- Name: {user.name}, Age: {user.age} -

- - -
- ); -}; describe('useLocalStorageState의 값이 원시값인 경우에 대한 테스트', () => { beforeEach(() => { @@ -42,65 +9,91 @@ describe('useLocalStorageState의 값이 원시값인 경우에 대한 테스트 describe('Primitive Value Tests', () => { it('초기 상태를 설정한다', () => { - render(); - expect(screen.getByText('Value: 0')).toBeInTheDocument(); + const { result } = renderHook(() => useLocalStorageState('primitiveKey', 0)); + expect(result.current[0]).toBe(0); }); it('[setState 인자 원시값] 상태 변경 시 localStorage에 저장한다', () => { - render(); - fireEvent.click(screen.getByText('Increment')); - expect(screen.getByText('Value: 1')).toBeInTheDocument(); + const { result } = renderHook(() => useLocalStorageState('primitiveKey', 0)); + + act(() => { + result.current[1]((prev) => prev + 1); + }); + + expect(result.current[0]).toBe(1); expect(window.localStorage.getItem('primitiveKey')).toBe('1'); }); it('[setState 인자 함수] 상태 변경 시 localStorage에 저장한다', () => { - render(); - fireEvent.click(screen.getByText('Increment2')); - expect(screen.getByText('Value: 1')).toBeInTheDocument(); + const { result } = renderHook(() => useLocalStorageState('primitiveKey', 0)); + + 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'); - render(); - expect(screen.getByText('Value: 10')).toBeInTheDocument(); - fireEvent.click(screen.getByText('Increment2')); + + const { result } = renderHook(() => useLocalStorageState('primitiveKey', 0)); + 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'); }); }); describe('useLocalStorageState의 값이 객체인 경우에 대한 테스트', () => { it('초기 상태를 설정한다', () => { - render(); - expect(screen.getByText('Name: lurgi, Age: 30')).toBeInTheDocument(); + const initialObject = { name: 'lurgi', age: 30 }; + const { result } = renderHook(() => useLocalStorageState('objectKey', initialObject)); + expect(result.current[0]).toEqual(initialObject); }); it('[setState 인자 원시값] 상태 변경 시 localStorage에 저장한다', () => { - render(); - fireEvent.click(screen.getByText('Increase Age')); - expect(screen.getByText('Name: lurgi, Age: 31')).toBeInTheDocument(); + const initialObject = { name: 'lurgi', age: 30 }; + const { result } = renderHook(() => useLocalStorageState('objectKey', initialObject)); + + act(() => { + result.current[1]({ name: 'lurgi', age: 31 }); + }); - const storedValue = window.localStorage.getItem('objectKey'); - expect(storedValue).toBe(JSON.stringify({ 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에 저장한다', () => { - render(); - fireEvent.click(screen.getByText('Increase Age2')); - expect(screen.getByText('Name: lurgi, Age: 31')).toBeInTheDocument(); + const initialObject = { name: 'lurgi', age: 30 }; + const { result } = renderHook(() => useLocalStorageState('objectKey', initialObject)); - const storedValue = window.localStorage.getItem('objectKey'); - expect(storedValue).toBe(JSON.stringify({ name: 'lurgi', age: 31 })); + 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); - render(); - expect(screen.getByText('Name: jeong woo, Age: 28')).toBeInTheDocument(); - fireEvent.click(screen.getByText('Increase Age2')); - expect(screen.getByText('Name: jeong woo, Age: 29')).toBeInTheDocument(); + const { result } = renderHook(() => useLocalStorageState('objectKey', { name: 'default', age: 0 })); + 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 })); }); }); }); From f9478114e4910bb9d9bb7af28d9df81bd5699ec6 Mon Sep 17 00:00:00 2001 From: Jeongwoo Park Date: Thu, 19 Dec 2024 16:17:18 +0900 Subject: [PATCH 07/11] =?UTF-8?q?test:=20enableStorage=EC=98=B5=EC=85=98?= =?UTF-8?q?=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../useLocalStorageState.test.tsx | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/frontend/src/hooks/useLocalStorageState/useLocalStorageState.test.tsx b/frontend/src/hooks/useLocalStorageState/useLocalStorageState.test.tsx index ba2168d2..655d030b 100644 --- a/frontend/src/hooks/useLocalStorageState/useLocalStorageState.test.tsx +++ b/frontend/src/hooks/useLocalStorageState/useLocalStorageState.test.tsx @@ -48,6 +48,15 @@ describe('useLocalStorageState의 값이 원시값인 경우에 대한 테스트 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('primitiveKey', 0, { enableStorage: false })); + + expect(result.current[0]).toBe(0); + expect(window.localStorage.getItem('primitiveKey')).toBe('10'); + }); }); describe('useLocalStorageState의 값이 객체인 경우에 대한 테스트', () => { @@ -95,5 +104,16 @@ describe('useLocalStorageState의 값이 원시값인 경우에 대한 테스트 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('objectKey', initialObject, { enableStorage: false })); + + expect(result.current[0]).toEqual(initialObject); + expect(window.localStorage.getItem('objectKey')).toBe(storedObject); + }); }); }); From 60c5c6d31fd74add17037f7eaa151358f9df7312 Mon Sep 17 00:00:00 2001 From: Jeongwoo Park Date: Thu, 19 Dec 2024 16:17:38 +0900 Subject: [PATCH 08/11] =?UTF-8?q?feat:=20enableStorage=20=EC=98=B5?= =?UTF-8?q?=EC=85=98=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/hooks/useLocalStorageState/index.ts | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/frontend/src/hooks/useLocalStorageState/index.ts b/frontend/src/hooks/useLocalStorageState/index.ts index a87611b2..e0e93332 100644 --- a/frontend/src/hooks/useLocalStorageState/index.ts +++ b/frontend/src/hooks/useLocalStorageState/index.ts @@ -1,13 +1,24 @@ import { useState } from 'react'; +interface OptionProp { + enableStorage: boolean; +} + /** * useLocalStorageState * @param key - LocalStorage에 저장될 키 값 * @param initialValue - 초기 상태 값 + * @param option - { use: boolean } * @returns [상태 값, 상태를 변경하는 함수] useState의 반환값과 동일합니다. */ -function useLocalStorageState(key: string, initialValue: T): [T, (value: T | ((prev: T) => T)) => void] { +function useLocalStorageState( + key: string, + initialValue: T, + option: OptionProp = { enableStorage: true }, +): [T, (value: T | ((prev: T) => T)) => void] { const [state, _setState] = useState(() => { + if (!option.enableStorage) return initialValue; + try { const storedValue = window.localStorage.getItem(key); return storedValue !== null ? JSON.parse(storedValue) : initialValue; From e7956f2a7fd3086bc5ae24631054bc0b24279e44 Mon Sep 17 00:00:00 2001 From: Jeongwoo Park Date: Thu, 19 Dec 2024 16:21:51 +0900 Subject: [PATCH 09/11] =?UTF-8?q?refactor:=20useLocalStorageState=20?= =?UTF-8?q?=EC=BD=94=EB=93=9C=20=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/hooks/useLocalStorageState/index.ts | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/frontend/src/hooks/useLocalStorageState/index.ts b/frontend/src/hooks/useLocalStorageState/index.ts index e0e93332..fcabe462 100644 --- a/frontend/src/hooks/useLocalStorageState/index.ts +++ b/frontend/src/hooks/useLocalStorageState/index.ts @@ -8,7 +8,7 @@ interface OptionProp { * useLocalStorageState * @param key - LocalStorage에 저장될 키 값 * @param initialValue - 초기 상태 값 - * @param option - { use: boolean } + * @param option - { enableStorage: boolean } * @returns [상태 값, 상태를 변경하는 함수] useState의 반환값과 동일합니다. */ function useLocalStorageState( @@ -27,16 +27,19 @@ function useLocalStorageState( } }); - const setState = (value: T | ((prev: T) => T)) => { + const saveToLocalStorage = (value: T) => { try { - const newState = value instanceof Function ? value(state) : value; - _setState(value); - window.localStorage.setItem(key, JSON.stringify(newState)); + window.localStorage.setItem(key, JSON.stringify(value)); } catch (error) { console.error(`"${key}":`, error); } }; + const setState = (value: T | ((prev: T) => T)) => { + _setState(value); + saveToLocalStorage(value instanceof Function ? value(state) : value); + }; + return [state, setState]; } From 938a8379bb8f8573f1a6f60c630220da67bb0974 Mon Sep 17 00:00:00 2001 From: Jeongwoo Park Date: Fri, 20 Dec 2024 08:33:10 +0900 Subject: [PATCH 10/11] =?UTF-8?q?chore:=20useLocalStorageState=EC=9D=98=20?= =?UTF-8?q?Prop=20=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/hooks/useLocalStorageState/index.ts | 16 ++++++------- .../useLocalStorageState.test.tsx | 23 +++++++++++-------- 2 files changed, 20 insertions(+), 19 deletions(-) diff --git a/frontend/src/hooks/useLocalStorageState/index.ts b/frontend/src/hooks/useLocalStorageState/index.ts index fcabe462..9df2f6bc 100644 --- a/frontend/src/hooks/useLocalStorageState/index.ts +++ b/frontend/src/hooks/useLocalStorageState/index.ts @@ -1,23 +1,21 @@ import { useState } from 'react'; interface OptionProp { - enableStorage: boolean; + key: string; + enableStorage?: boolean; } /** * useLocalStorageState - * @param key - LocalStorage에 저장될 키 값 * @param initialValue - 초기 상태 값 - * @param option - { enableStorage: boolean } + * @param option - { key: LocalStorage에 저장될 키 값, enableStorage: LocalStorage의 값을 사용할지 여부} * @returns [상태 값, 상태를 변경하는 함수] useState의 반환값과 동일합니다. */ -function useLocalStorageState( - key: string, - initialValue: T, - option: OptionProp = { enableStorage: true }, -): [T, (value: T | ((prev: T) => T)) => void] { +function useLocalStorageState(initialValue: T, option: OptionProp): [T, (value: T | ((prev: T) => T)) => void] { + const { key, enableStorage = true } = option; + const [state, _setState] = useState(() => { - if (!option.enableStorage) return initialValue; + if (!enableStorage) return initialValue; try { const storedValue = window.localStorage.getItem(key); diff --git a/frontend/src/hooks/useLocalStorageState/useLocalStorageState.test.tsx b/frontend/src/hooks/useLocalStorageState/useLocalStorageState.test.tsx index 655d030b..4cbee9dc 100644 --- a/frontend/src/hooks/useLocalStorageState/useLocalStorageState.test.tsx +++ b/frontend/src/hooks/useLocalStorageState/useLocalStorageState.test.tsx @@ -1,3 +1,4 @@ +/* eslint-disable function-paren-newline */ import { act } from 'react'; import { renderHook } from '@testing-library/react'; import useLocalStorageState from '.'; @@ -9,12 +10,12 @@ describe('useLocalStorageState의 값이 원시값인 경우에 대한 테스트 describe('Primitive Value Tests', () => { it('초기 상태를 설정한다', () => { - const { result } = renderHook(() => useLocalStorageState('primitiveKey', 0)); + const { result } = renderHook(() => useLocalStorageState(0, { key: 'primitiveKey' })); expect(result.current[0]).toBe(0); }); it('[setState 인자 원시값] 상태 변경 시 localStorage에 저장한다', () => { - const { result } = renderHook(() => useLocalStorageState('primitiveKey', 0)); + const { result } = renderHook(() => useLocalStorageState(0, { key: 'primitiveKey' })); act(() => { result.current[1]((prev) => prev + 1); @@ -25,7 +26,7 @@ describe('useLocalStorageState의 값이 원시값인 경우에 대한 테스트 }); it('[setState 인자 함수] 상태 변경 시 localStorage에 저장한다', () => { - const { result } = renderHook(() => useLocalStorageState('primitiveKey', 0)); + const { result } = renderHook(() => useLocalStorageState(0, { key: 'primitiveKey' })); act(() => { result.current[1]((prev) => prev + 1); @@ -38,7 +39,7 @@ describe('useLocalStorageState의 값이 원시값인 경우에 대한 테스트 it('localStorage에 값이 있으면 초기 상태로 사용한다', () => { window.localStorage.setItem('primitiveKey', '10'); - const { result } = renderHook(() => useLocalStorageState('primitiveKey', 0)); + const { result } = renderHook(() => useLocalStorageState(0, { key: 'primitiveKey' })); expect(result.current[0]).toBe(10); act(() => { @@ -52,7 +53,7 @@ describe('useLocalStorageState의 값이 원시값인 경우에 대한 테스트 it('enableStorage 옵션을 false로 지정한 경우 localStorage를 사용하지 않는다', () => { window.localStorage.setItem('primitiveKey', '10'); - const { result } = renderHook(() => useLocalStorageState('primitiveKey', 0, { enableStorage: false })); + const { result } = renderHook(() => useLocalStorageState(0, { key: 'primitiveKey', enableStorage: false })); expect(result.current[0]).toBe(0); expect(window.localStorage.getItem('primitiveKey')).toBe('10'); @@ -62,13 +63,13 @@ describe('useLocalStorageState의 값이 원시값인 경우에 대한 테스트 describe('useLocalStorageState의 값이 객체인 경우에 대한 테스트', () => { it('초기 상태를 설정한다', () => { const initialObject = { name: 'lurgi', age: 30 }; - const { result } = renderHook(() => useLocalStorageState('objectKey', initialObject)); + 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('objectKey', initialObject)); + const { result } = renderHook(() => useLocalStorageState(initialObject, { key: 'objectKey' })); act(() => { result.current[1]({ name: 'lurgi', age: 31 }); @@ -80,7 +81,7 @@ describe('useLocalStorageState의 값이 원시값인 경우에 대한 테스트 it('[setState 인자 함수] 상태 변경 시 localStorage에 저장한다', () => { const initialObject = { name: 'lurgi', age: 30 }; - const { result } = renderHook(() => useLocalStorageState('objectKey', initialObject)); + const { result } = renderHook(() => useLocalStorageState(initialObject, { key: 'objectKey' })); act(() => { result.current[1]((prev) => ({ ...prev, age: prev.age + 1 })); @@ -94,7 +95,7 @@ describe('useLocalStorageState의 값이 원시값인 경우에 대한 테스트 const storedObject = JSON.stringify({ name: 'jeong woo', age: 28 }); window.localStorage.setItem('objectKey', storedObject); - const { result } = renderHook(() => useLocalStorageState('objectKey', { name: 'default', age: 0 })); + const { result } = renderHook(() => useLocalStorageState({ name: 'default', age: 0 }, { key: 'objectKey' })); expect(result.current[0]).toEqual({ name: 'jeong woo', age: 28 }); act(() => { @@ -110,7 +111,9 @@ describe('useLocalStorageState의 값이 원시값인 경우에 대한 테스트 window.localStorage.setItem('objectKey', storedObject); const initialObject = { name: 'lurgi', age: 30 }; - const { result } = renderHook(() => useLocalStorageState('objectKey', initialObject, { enableStorage: false })); + const { result } = renderHook(() => + useLocalStorageState(initialObject, { key: 'objectKey', enableStorage: false }), + ); expect(result.current[0]).toEqual(initialObject); expect(window.localStorage.getItem('objectKey')).toBe(storedObject); From f0fdad72ff55644adc5d6245e61aa80064789907 Mon Sep 17 00:00:00 2001 From: Jeongwoo Park Date: Fri, 20 Dec 2024 08:51:25 +0900 Subject: [PATCH 11/11] =?UTF-8?q?refactor:=20=EC=97=90=EB=9F=AC=20?= =?UTF-8?q?=ED=95=B8=EB=93=A4=EB=A7=81=20=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/hooks/useLocalStorageState/index.ts | 37 ++++++++++++++----- 1 file changed, 27 insertions(+), 10 deletions(-) diff --git a/frontend/src/hooks/useLocalStorageState/index.ts b/frontend/src/hooks/useLocalStorageState/index.ts index 9df2f6bc..94715952 100644 --- a/frontend/src/hooks/useLocalStorageState/index.ts +++ b/frontend/src/hooks/useLocalStorageState/index.ts @@ -5,6 +5,28 @@ interface OptionProp { enableStorage?: boolean; } +const safeParseJSON = (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 = (value: T): string | null => { + try { + return JSON.stringify(value); + } catch (error) { + if (process.env.NODE_ENV === 'development') { + console.error('JSON 직렬화 실패:', error); + } + return null; + } +}; + /** * useLocalStorageState * @param initialValue - 초기 상태 값 @@ -17,19 +39,14 @@ function useLocalStorageState(initialValue: T, option: OptionProp): [T, (valu const [state, _setState] = useState(() => { if (!enableStorage) return initialValue; - try { - const storedValue = window.localStorage.getItem(key); - return storedValue !== null ? JSON.parse(storedValue) : initialValue; - } catch (error) { - return initialValue; - } + const storedValue = window.localStorage.getItem(key); + return safeParseJSON(storedValue, initialValue); }); const saveToLocalStorage = (value: T) => { - try { - window.localStorage.setItem(key, JSON.stringify(value)); - } catch (error) { - console.error(`"${key}":`, error); + const stringifiedValue = safeStringifyJSON(value); + if (stringifiedValue !== null) { + window.localStorage.setItem(key, stringifiedValue); } };