[251120] refactor: hooks - usePrevious.js, usePrevious.test.js, usePreviou...

🕐 커밋 시간: 2025. 11. 20. 06:13:41

📊 변경 통계:
  • 총 파일: 3개
  • 추가: +71줄
  • 삭제: -74줄

📝 수정된 파일:
  ~ com.twin.app.shoptime/src/hooks/usePrevious.js
  ~ com.twin.app.shoptime/src/hooks/usePrevious.test.js
  ~ com.twin.app.shoptime/src/hooks/usePreviousExample.jsx

🔧 주요 변경 내용:
  • 핵심 비즈니스 로직 개선
  • 테스트 커버리지 및 안정성 향상
  • 소규모 기능 개선
  • 코드 정리 및 최적화

Performance: 코드 최적화로 성능 개선 기대
This commit is contained in:
2025-11-20 06:13:41 +09:00
parent 78bf217d75
commit 18175a03de
3 changed files with 71 additions and 74 deletions

View File

@@ -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;
}

View File

@@ -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);
});
});
});

View File

@@ -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 (
<div style={{ padding: '20px', border: '1px solid #ccc', marginBottom: '20px' }}>
<h3>예시 1: 단일 추적 (카운터)</h3>
<p>현재값: {count}</p>
<p>이전값: {prevCount !== undefined ? prevCount : '초기값'}</p>
<p>이전값: {prevCountRef.current !== undefined ? prevCountRef.current : '초기값'}</p>
<button onClick={() => setCount(count + 1)}>증가</button>
<button onClick={() => setCount(count - 1)}>감소</button>
</div>
@@ -27,7 +27,8 @@ export function UserFormExample() {
const [age, setAge] = useState('');
const [email, setEmail] = useState('');
const prev = usePrevious({ name, age, email });
const prevRef = usePrevious({ name, age, email });
const prev = prevRef.current;
const hasNameChanged = prev?.name !== name;
const hasAgeChanged = prev?.age !== age;
@@ -92,7 +93,8 @@ export function ArrayValuesExample() {
const [b, setB] = useState(0);
const [c, setC] = useState(0);
const [prevA, prevB, prevC] = usePrevious([a, b, c]) || [];
const prevRef = usePrevious([a, b, c]);
const [prevA, prevB, prevC] = prevRef.current || [];
return (
<div style={{ padding: '20px', border: '1px solid #ccc', marginBottom: '20px' }}>
@@ -150,7 +152,8 @@ export function ChangeDetectionExample() {
views: 1000,
});
const prevData = usePrevious(data);
const prevDataRef = usePrevious(data);
const prevData = prevDataRef.current;
const changes = {
titleChanged: prevData?.title !== data.title,