diff --git a/com.twin.app.shoptime/src/hooks/usePrevious.js b/com.twin.app.shoptime/src/hooks/usePrevious.js index cd5b8173..118110b3 100644 --- a/com.twin.app.shoptime/src/hooks/usePrevious.js +++ b/com.twin.app.shoptime/src/hooks/usePrevious.js @@ -1,13 +1,26 @@ -import {useRef, useEffect} from 'react'; +import { useRef, useEffect } from 'react'; -function usePrevious (value) { - const ref = useRef(); +/** + * usePrevious - React 16.7 전용 + * @param {*} value – 현재값 (배열, 객체, 프롭 등) + * @param {*} [initial] – 최초 렌더시 반환할 값 (선택사항, default: undefined) + * @return {*} – 이전 렌더에서의 값 + * + * 사용 예시 + * const prev = usePrevious(value); + * if (prev !== value) { … } + * + * // 초기값 지정 + * const prev = usePrevious(count, 0); + */ +export default function usePrevious(value, initial) { + // useRef 가 저장한 객체는 렌더 사이에서도 동일합니다. + const ref = useRef(initial); - useEffect(() => { - ref.current = value; - }, [value]); + // value 가 바뀔 때마다 ref 를 갱신 + useEffect(() => { + ref.current = value; + }, [value]); // value 의 변경만 감지 - return ref; + return ref.current; } - -export default usePrevious; diff --git a/com.twin.app.shoptime/src/hooks/usePrevious.test.js b/com.twin.app.shoptime/src/hooks/usePrevious.test.js new file mode 100644 index 00000000..1a95e606 --- /dev/null +++ b/com.twin.app.shoptime/src/hooks/usePrevious.test.js @@ -0,0 +1,309 @@ +import { renderHook } from '@testing-library/react'; +import usePrevious from './usePrevious'; + +describe('usePrevious', () => { + // 단일 값 테스트 + describe('단일 값 추적', () => { + it('초기값 없이 호출하면 초기 렌더링에서 undefined를 반환해야 한다', () => { + const { result } = renderHook(() => usePrevious(0)); + expect(result.current).toBeUndefined(); + }); + + it('초기값이 지정되면 초기 렌더링에서 초기값을 반환해야 한다', () => { + const { result } = renderHook(() => usePrevious(0, -1)); + expect(result.current).toBe(-1); + }); + + it('값이 변경되면 이전 값을 반환해야 한다', () => { + const { result, rerender } = renderHook(({ value }) => usePrevious(value), { + initialProps: { value: 0 }, + }); + + expect(result.current).toBeUndefined(); + + rerender({ value: 1 }); + expect(result.current).toBe(0); + + rerender({ value: 2 }); + expect(result.current).toBe(1); + + rerender({ value: 3 }); + expect(result.current).toBe(2); + }); + + it('숫자 값을 정확히 추적해야 한다', () => { + const { result, rerender } = renderHook(({ num }) => usePrevious(num), { + initialProps: { num: 100 }, + }); + + rerender({ num: 200 }); + expect(result.current).toBe(100); + + rerender({ num: 300 }); + expect(result.current).toBe(200); + }); + + it('문자열 값을 정확히 추적해야 한다', () => { + const { result, rerender } = renderHook(({ str }) => usePrevious(str), { + initialProps: { str: 'hello' }, + }); + + rerender({ str: 'world' }); + expect(result.current).toBe('hello'); + + rerender({ str: 'react' }); + expect(result.current).toBe('world'); + }); + + it('boolean 값을 정확히 추적해야 한다', () => { + const { result, rerender } = renderHook(({ bool }) => usePrevious(bool), { + initialProps: { bool: true }, + }); + + rerender({ bool: false }); + expect(result.current).toBe(true); + + rerender({ bool: true }); + expect(result.current).toBe(false); + }); + + it('null 값을 추적할 수 있어야 한다', () => { + const { result, rerender } = renderHook(({ val }) => usePrevious(val), { + initialProps: { val: null }, + }); + + rerender({ val: 'something' }); + expect(result.current).toBeNull(); + + rerender({ val: null }); + expect(result.current).toBe('something'); + }); + }); + + // 객체 테스트 + describe('객체 값 추적', () => { + it('객체를 추적하고 이전 객체를 반환해야 한다', () => { + const obj1 = { name: 'John', age: 30 }; + const obj2 = { name: 'Jane', age: 25 }; + + const { result, rerender } = renderHook(({ obj }) => usePrevious(obj), { + initialProps: { obj: obj1 }, + }); + + expect(result.current).toBeUndefined(); + + rerender({ obj: obj2 }); + expect(result.current).toBe(obj1); + expect(result.current.name).toBe('John'); + expect(result.current.age).toBe(30); + }); + + it('중첩된 객체를 추적할 수 있어야 한다', () => { + const obj1 = { user: { name: 'John', profile: { age: 30 } } }; + const obj2 = { user: { name: 'Jane', profile: { age: 25 } } }; + + const { result, rerender } = renderHook(({ obj }) => usePrevious(obj), { + initialProps: { obj: obj1 }, + }); + + rerender({ obj: obj2 }); + expect(result.current.user.name).toBe('John'); + expect(result.current.user.profile.age).toBe(30); + }); + + it('변동 감지에 사용할 수 있어야 한다', () => { + const obj1 = { name: 'John', age: 30 }; + const obj2 = { name: 'Jane', age: 30 }; + + const { result, rerender } = renderHook( + ({ obj }) => { + const prev = usePrevious(obj); + return { + prev, + nameChanged: prev?.name !== obj.name, + ageChanged: prev?.age !== obj.age, + }; + }, + { initialProps: { obj: obj1 } } + ); + + expect(result.current.nameChanged).toBeUndefined(); + + rerender({ obj: obj2 }); + expect(result.current.nameChanged).toBe(true); + expect(result.current.ageChanged).toBe(false); + }); + }); + + // 배열 테스트 + describe('배열 값 추적', () => { + it('배열을 추적하고 이전 배열을 반환해야 한다', () => { + const arr1 = [1, 2, 3]; + const arr2 = [4, 5, 6]; + + const { result, rerender } = renderHook(({ arr }) => usePrevious(arr), { + initialProps: { arr: arr1 }, + }); + + expect(result.current).toBeUndefined(); + + rerender({ arr: arr2 }); + expect(result.current).toEqual([1, 2, 3]); + expect(result.current).not.toBe(arr2); + }); + + it('배열 요소의 이전 값을 추적할 수 있어야 한다', () => { + const { result, rerender } = renderHook( + ({ a, b }) => { + const [prevA, prevB] = usePrevious([a, b]) || []; + return { prevA, prevB }; + }, + { initialProps: { a: 1, b: 2 } } + ); + + expect(result.current.prevA).toBeUndefined(); + + rerender({ a: 10, b: 20 }); + expect(result.current.prevA).toBe(1); + expect(result.current.prevB).toBe(2); + + rerender({ a: 100, b: 200 }); + expect(result.current.prevA).toBe(10); + expect(result.current.prevB).toBe(20); + }); + + it('복잡한 배열 요소를 추적할 수 있어야 한다', () => { + const arr1 = [{ id: 1 }, { id: 2 }]; + const arr2 = [{ id: 3 }, { id: 4 }]; + + const { result, rerender } = renderHook(({ arr }) => usePrevious(arr), { + initialProps: { arr: arr1 }, + }); + + rerender({ arr: arr2 }); + expect(result.current[0].id).toBe(1); + expect(result.current[1].id).toBe(2); + }); + }); + + // 변동 감지 테스트 + describe('변동 감지 활용', () => { + it('값이 변경되었는지 감지할 수 있어야 한다', () => { + const { result, rerender } = renderHook( + ({ value }) => { + const prev = usePrevious(value); + return prev !== value; + }, + { initialProps: { value: 'initial' } } + ); + + expect(result.current).toBe(false); + + rerender({ value: 'changed' }); + expect(result.current).toBe(true); + + rerender({ value: 'changed' }); + expect(result.current).toBe(false); + }); + + it('깊은 비교 없이도 객체 필드 변경을 감지할 수 있어야 한다', () => { + const { result, rerender } = renderHook( + ({ obj }) => { + const prev = usePrevious(obj); + return { + prevValue: prev?.value, + changed: prev?.value !== obj.value, + }; + }, + { initialProps: { obj: { value: 100 } } } + ); + + rerender({ obj: { value: 200 } }); + expect(result.current.prevValue).toBe(100); + expect(result.current.changed).toBe(true); + }); + + it('여러 필드 변경을 각각 추적할 수 있어야 한다', () => { + const { result, rerender } = renderHook( + ({ name, age, email }) => { + const prev = usePrevious({ name, age, email }); + return { + nameChanged: prev?.name !== name, + ageChanged: prev?.age !== age, + emailChanged: prev?.email !== email, + }; + }, + { initialProps: { name: 'John', age: 30, email: 'john@example.com' } } + ); + + rerender({ name: 'Jane', age: 30, email: 'john@example.com' }); + expect(result.current.nameChanged).toBe(true); + expect(result.current.ageChanged).toBe(false); + expect(result.current.emailChanged).toBe(false); + }); + }); + + // 엣지 케이스 + describe('엣지 케이스', () => { + it('동일한 값으로 리렌더링되면 이전 값은 변경되지 않아야 한다', () => { + const { result, rerender } = renderHook(({ value }) => usePrevious(value), { + initialProps: { value: 5 }, + }); + + rerender({ value: 10 }); + expect(result.current).toBe(5); + + rerender({ value: 10 }); + expect(result.current).toBe(5); // 여전히 처음 값 + }); + + it('undefined를 값으로 전달할 수 있어야 한다', () => { + const { result, rerender } = renderHook(({ value }) => usePrevious(value), { + initialProps: { value: undefined }, + }); + + expect(result.current).toBeUndefined(); + + rerender({ value: 'something' }); + expect(result.current).toBeUndefined(); + + rerender({ value: undefined }); + expect(result.current).toBe('something'); + }); + + it('0과 false를 정확히 추적해야 한다', () => { + const { result, rerender } = renderHook(({ value }) => usePrevious(value), { + initialProps: { value: 0 }, + }); + + rerender({ value: false }); + expect(result.current).toBe(0); + + rerender({ value: 0 }); + expect(result.current).toBe(false); + }); + + it('빈 배열과 객체를 추적할 수 있어야 한다', () => { + const emptyArr = []; + const emptyObj = {}; + + const { result: arrResult, rerender: arrRerender } = renderHook( + ({ arr }) => usePrevious(arr), + { initialProps: { arr: emptyArr } } + ); + + arrRerender({ arr: [1, 2, 3] }); + expect(arrResult.current).toEqual([]); + expect(arrResult.current).toBe(emptyArr); + + const { result: objResult, rerender: objRerender } = renderHook( + ({ obj }) => usePrevious(obj), + { initialProps: { obj: emptyObj } } + ); + + objRerender({ obj: { name: 'test' } }); + expect(objResult.current).toEqual({}); + expect(objResult.current).toBe(emptyObj); + }); + }); +}); diff --git a/com.twin.app.shoptime/src/hooks/usePreviousExample.jsx b/com.twin.app.shoptime/src/hooks/usePreviousExample.jsx new file mode 100644 index 00000000..444907d0 --- /dev/null +++ b/com.twin.app.shoptime/src/hooks/usePreviousExample.jsx @@ -0,0 +1,242 @@ +import { useState } from 'react'; +import usePrevious from './usePrevious'; + +/** + * usePrevious 훅의 다양한 사용 예시를 보여주는 컴포넌트 + */ + +// 예시 1: 단일 값 추적 - 카운터 +export function CounterExample() { + const [count, setCount] = useState(0); + const prevCount = usePrevious(count); + + return ( +
현재값: {count}
+이전값: {prevCount !== undefined ? prevCount : '초기값'}
+ + +이름: {prev?.name || '(없음)'}
+나이: {prev?.age || '(없음)'}
+이메일: {prev?.email || '(없음)'}
++ + 상태: {hasAnyChanges ? '데이터가 변경되었습니다' : '모든 데이터가 저장되었습니다'} + +
+이전 값을 추적하는 다양한 방법을 보여주는 예시들입니다.
+ +