diff --git a/com.twin.app.shoptime/src/hooks/usePrevious.js b/com.twin.app.shoptime/src/hooks/usePrevious.js index 118110b3..9db23f86 100644 --- a/com.twin.app.shoptime/src/hooks/usePrevious.js +++ b/com.twin.app.shoptime/src/hooks/usePrevious.js @@ -2,25 +2,21 @@ import { useRef, useEffect } from 'react'; /** * usePrevious - React 16.7 전용 - * @param {*} value – 현재값 (배열, 객체, 프롭 등) - * @param {*} [initial] – 최초 렌더시 반환할 값 (선택사항, default: undefined) - * @return {*} – 이전 렌더에서의 값 + * @param {*} value – 현재값 (배열, 객체, 프롭 등) + * @return {*} – useRef 객체 { current: previousValue } * * 사용 예시 - * const prev = usePrevious(value); - * if (prev !== value) { … } - * - * // 초기값 지정 - * const prev = usePrevious(count, 0); + * const prevRef = usePrevious(value); + * if (prevRef.current !== value) { … } */ -export default function usePrevious(value, initial) { +export default function usePrevious(value) { // useRef 가 저장한 객체는 렌더 사이에서도 동일합니다. - const ref = useRef(initial); + const ref = useRef(); // value 가 바뀔 때마다 ref 를 갱신 useEffect(() => { ref.current = value; }, [value]); // value 의 변경만 감지 - return ref.current; + return ref; } diff --git a/com.twin.app.shoptime/src/hooks/usePrevious.test.js b/com.twin.app.shoptime/src/hooks/usePrevious.test.js index 1a95e606..6ab8f0b5 100644 --- a/com.twin.app.shoptime/src/hooks/usePrevious.test.js +++ b/com.twin.app.shoptime/src/hooks/usePrevious.test.js @@ -4,31 +4,27 @@ import usePrevious from './usePrevious'; describe('usePrevious', () => { // 단일 값 테스트 describe('단일 값 추적', () => { - it('초기값 없이 호출하면 초기 렌더링에서 undefined를 반환해야 한다', () => { + it('초기 렌더링에서 ref 객체를 반환하고 ref.current는 undefined여야 한다', () => { const { result } = renderHook(() => usePrevious(0)); - expect(result.current).toBeUndefined(); + expect(result.current).toBeDefined(); + expect(result.current.current).toBeUndefined(); }); - it('초기값이 지정되면 초기 렌더링에서 초기값을 반환해야 한다', () => { - const { result } = renderHook(() => usePrevious(0, -1)); - expect(result.current).toBe(-1); - }); - - it('값이 변경되면 이전 값을 반환해야 한다', () => { + it('값이 변경되면 ref.current에 이전 값이 저장되어야 한다', () => { const { result, rerender } = renderHook(({ value }) => usePrevious(value), { initialProps: { value: 0 }, }); - expect(result.current).toBeUndefined(); + expect(result.current.current).toBeUndefined(); rerender({ value: 1 }); - expect(result.current).toBe(0); + expect(result.current.current).toBe(0); rerender({ value: 2 }); - expect(result.current).toBe(1); + expect(result.current.current).toBe(1); rerender({ value: 3 }); - expect(result.current).toBe(2); + expect(result.current.current).toBe(2); }); it('숫자 값을 정확히 추적해야 한다', () => { @@ -37,10 +33,10 @@ describe('usePrevious', () => { }); rerender({ num: 200 }); - expect(result.current).toBe(100); + expect(result.current.current).toBe(100); rerender({ num: 300 }); - expect(result.current).toBe(200); + expect(result.current.current).toBe(200); }); it('문자열 값을 정확히 추적해야 한다', () => { @@ -49,10 +45,10 @@ describe('usePrevious', () => { }); rerender({ str: 'world' }); - expect(result.current).toBe('hello'); + expect(result.current.current).toBe('hello'); rerender({ str: 'react' }); - expect(result.current).toBe('world'); + expect(result.current.current).toBe('world'); }); it('boolean 값을 정확히 추적해야 한다', () => { @@ -61,10 +57,10 @@ describe('usePrevious', () => { }); rerender({ bool: false }); - expect(result.current).toBe(true); + expect(result.current.current).toBe(true); rerender({ bool: true }); - expect(result.current).toBe(false); + expect(result.current.current).toBe(false); }); it('null 값을 추적할 수 있어야 한다', () => { @@ -73,10 +69,10 @@ describe('usePrevious', () => { }); rerender({ val: 'something' }); - expect(result.current).toBeNull(); + expect(result.current.current).toBeNull(); rerender({ val: null }); - expect(result.current).toBe('something'); + expect(result.current.current).toBe('something'); }); }); @@ -90,12 +86,12 @@ describe('usePrevious', () => { initialProps: { obj: obj1 }, }); - expect(result.current).toBeUndefined(); + expect(result.current.current).toBeUndefined(); rerender({ obj: obj2 }); - expect(result.current).toBe(obj1); - expect(result.current.name).toBe('John'); - expect(result.current.age).toBe(30); + expect(result.current.current).toBe(obj1); + expect(result.current.current.name).toBe('John'); + expect(result.current.current.age).toBe(30); }); it('중첩된 객체를 추적할 수 있어야 한다', () => { @@ -107,8 +103,8 @@ describe('usePrevious', () => { }); rerender({ obj: obj2 }); - expect(result.current.user.name).toBe('John'); - expect(result.current.user.profile.age).toBe(30); + expect(result.current.current.user.name).toBe('John'); + expect(result.current.current.user.profile.age).toBe(30); }); it('변동 감지에 사용할 수 있어야 한다', () => { @@ -117,11 +113,11 @@ describe('usePrevious', () => { const { result, rerender } = renderHook( ({ obj }) => { - const prev = usePrevious(obj); + const prevRef = usePrevious(obj); return { - prev, - nameChanged: prev?.name !== obj.name, - ageChanged: prev?.age !== obj.age, + prev: prevRef.current, + nameChanged: prevRef.current?.name !== obj.name, + ageChanged: prevRef.current?.age !== obj.age, }; }, { initialProps: { obj: obj1 } } @@ -145,17 +141,18 @@ describe('usePrevious', () => { initialProps: { arr: arr1 }, }); - expect(result.current).toBeUndefined(); + expect(result.current.current).toBeUndefined(); rerender({ arr: arr2 }); - expect(result.current).toEqual([1, 2, 3]); - expect(result.current).not.toBe(arr2); + expect(result.current.current).toEqual([1, 2, 3]); + expect(result.current.current).not.toBe(arr2); }); it('배열 요소의 이전 값을 추적할 수 있어야 한다', () => { const { result, rerender } = renderHook( ({ a, b }) => { - const [prevA, prevB] = usePrevious([a, b]) || []; + const prevRef = usePrevious([a, b]); + const [prevA, prevB] = prevRef.current || []; return { prevA, prevB }; }, { initialProps: { a: 1, b: 2 } } @@ -181,8 +178,8 @@ describe('usePrevious', () => { }); rerender({ arr: arr2 }); - expect(result.current[0].id).toBe(1); - expect(result.current[1].id).toBe(2); + expect(result.current.current[0].id).toBe(1); + expect(result.current.current[1].id).toBe(2); }); }); @@ -191,8 +188,8 @@ describe('usePrevious', () => { it('값이 변경되었는지 감지할 수 있어야 한다', () => { const { result, rerender } = renderHook( ({ value }) => { - const prev = usePrevious(value); - return prev !== value; + const prevRef = usePrevious(value); + return prevRef.current !== value; }, { initialProps: { value: 'initial' } } ); @@ -209,10 +206,10 @@ describe('usePrevious', () => { it('깊은 비교 없이도 객체 필드 변경을 감지할 수 있어야 한다', () => { const { result, rerender } = renderHook( ({ obj }) => { - const prev = usePrevious(obj); + const prevRef = usePrevious(obj); return { - prevValue: prev?.value, - changed: prev?.value !== obj.value, + prevValue: prevRef.current?.value, + changed: prevRef.current?.value !== obj.value, }; }, { initialProps: { obj: { value: 100 } } } @@ -226,11 +223,11 @@ describe('usePrevious', () => { it('여러 필드 변경을 각각 추적할 수 있어야 한다', () => { const { result, rerender } = renderHook( ({ name, age, email }) => { - const prev = usePrevious({ name, age, email }); + const prevRef = usePrevious({ name, age, email }); return { - nameChanged: prev?.name !== name, - ageChanged: prev?.age !== age, - emailChanged: prev?.email !== email, + nameChanged: prevRef.current?.name !== name, + ageChanged: prevRef.current?.age !== age, + emailChanged: prevRef.current?.email !== email, }; }, { initialProps: { name: 'John', age: 30, email: 'john@example.com' } } @@ -245,16 +242,17 @@ describe('usePrevious', () => { // 엣지 케이스 describe('엣지 케이스', () => { - it('동일한 값으로 리렌더링되면 이전 값은 변경되지 않아야 한다', () => { + it('동일한 ref 객체를 반환해야 한다 (참조 안정성)', () => { const { result, rerender } = renderHook(({ value }) => usePrevious(value), { initialProps: { value: 5 }, }); - rerender({ value: 10 }); - expect(result.current).toBe(5); + const firstRef = result.current; rerender({ value: 10 }); - expect(result.current).toBe(5); // 여전히 처음 값 + const secondRef = result.current; + + expect(firstRef).toBe(secondRef); }); it('undefined를 값으로 전달할 수 있어야 한다', () => { @@ -262,13 +260,13 @@ describe('usePrevious', () => { initialProps: { value: undefined }, }); - expect(result.current).toBeUndefined(); + expect(result.current.current).toBeUndefined(); rerender({ value: 'something' }); - expect(result.current).toBeUndefined(); + expect(result.current.current).toBeUndefined(); rerender({ value: undefined }); - expect(result.current).toBe('something'); + expect(result.current.current).toBe('something'); }); it('0과 false를 정확히 추적해야 한다', () => { @@ -277,10 +275,10 @@ describe('usePrevious', () => { }); rerender({ value: false }); - expect(result.current).toBe(0); + expect(result.current.current).toBe(0); rerender({ value: 0 }); - expect(result.current).toBe(false); + expect(result.current.current).toBe(false); }); it('빈 배열과 객체를 추적할 수 있어야 한다', () => { @@ -293,8 +291,8 @@ describe('usePrevious', () => { ); arrRerender({ arr: [1, 2, 3] }); - expect(arrResult.current).toEqual([]); - expect(arrResult.current).toBe(emptyArr); + expect(arrResult.current.current).toEqual([]); + expect(arrResult.current.current).toBe(emptyArr); const { result: objResult, rerender: objRerender } = renderHook( ({ obj }) => usePrevious(obj), @@ -302,8 +300,8 @@ describe('usePrevious', () => { ); objRerender({ obj: { name: 'test' } }); - expect(objResult.current).toEqual({}); - expect(objResult.current).toBe(emptyObj); + expect(objResult.current.current).toEqual({}); + expect(objResult.current.current).toBe(emptyObj); }); }); }); diff --git a/com.twin.app.shoptime/src/hooks/usePreviousExample.jsx b/com.twin.app.shoptime/src/hooks/usePreviousExample.jsx index 444907d0..bdd35e31 100644 --- a/com.twin.app.shoptime/src/hooks/usePreviousExample.jsx +++ b/com.twin.app.shoptime/src/hooks/usePreviousExample.jsx @@ -8,13 +8,13 @@ import usePrevious from './usePrevious'; // 예시 1: 단일 값 추적 - 카운터 export function CounterExample() { const [count, setCount] = useState(0); - const prevCount = usePrevious(count); + const prevCountRef = usePrevious(count); return (
현재값: {count}
-이전값: {prevCount !== undefined ? prevCount : '초기값'}
+이전값: {prevCountRef.current !== undefined ? prevCountRef.current : '초기값'}