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

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

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

📁 추가된 파일:
  + com.twin.app.shoptime/src/hooks/usePrevious.test.js
  + com.twin.app.shoptime/src/hooks/usePreviousExample.jsx

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

🔧 함수 변경 내용:
  📄 com.twin.app.shoptime/src/hooks/usePrevious.js (javascript):
     Deleted: usePrevious()
  📄 com.twin.app.shoptime/src/hooks/usePreviousExample.jsx (javascript):
     Added: handleUpdate()

🔧 주요 변경 내용:
  • 핵심 비즈니스 로직 개선
  • 테스트 커버리지 및 안정성 향상
This commit is contained in:
2025-11-20 06:05:41 +09:00
parent 1747eb1326
commit 78bf217d75
3 changed files with 573 additions and 9 deletions

View File

@@ -1,13 +1,26 @@
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);
// value 가 바뀔 때마다 ref 를 갱신
useEffect(() => {
ref.current = value;
}, [value]);
}, [value]); // value 의 변경만 감지
return ref;
return ref.current;
}
export default usePrevious;

View File

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

View File

@@ -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 (
<div style={{ padding: '20px', border: '1px solid #ccc', marginBottom: '20px' }}>
<h3>예시 1: 단일 추적 (카운터)</h3>
<p>현재값: {count}</p>
<p>이전값: {prevCount !== undefined ? prevCount : '초기값'}</p>
<button onClick={() => setCount(count + 1)}>증가</button>
<button onClick={() => setCount(count - 1)}>감소</button>
</div>
);
}
// 예시 2: 다중 값 추적 (객체) - 사용자 정보
export function UserFormExample() {
const [name, setName] = useState('');
const [age, setAge] = useState('');
const [email, setEmail] = useState('');
const prev = usePrevious({ name, age, email });
const hasNameChanged = prev?.name !== name;
const hasAgeChanged = prev?.age !== age;
const hasEmailChanged = prev?.email !== email;
return (
<div style={{ padding: '20px', border: '1px solid #ccc', marginBottom: '20px' }}>
<h3>예시 2: 다중 추적 (객체) - 사용자 정보</h3>
<div style={{ marginBottom: '10px' }}>
<label>
이름:
<input
type="text"
value={name}
onChange={(e) => setName(e.target.value)}
style={{ marginLeft: '10px' }}
/>
{hasNameChanged && <span style={{ color: 'red', marginLeft: '10px' }}>변경됨</span>}
</label>
</div>
<div style={{ marginBottom: '10px' }}>
<label>
나이:
<input
type="text"
value={age}
onChange={(e) => setAge(e.target.value)}
style={{ marginLeft: '10px' }}
/>
{hasAgeChanged && <span style={{ color: 'red', marginLeft: '10px' }}>변경됨</span>}
</label>
</div>
<div style={{ marginBottom: '10px' }}>
<label>
이메일:
<input
type="text"
value={email}
onChange={(e) => setEmail(e.target.value)}
style={{ marginLeft: '10px' }}
/>
{hasEmailChanged && <span style={{ color: 'red', marginLeft: '10px' }}>변경됨</span>}
</label>
</div>
<div style={{ marginTop: '15px', padding: '10px', backgroundColor: '#f5f5f5' }}>
<h4>이전 </h4>
<p>이름: {prev?.name || '(없음)'}</p>
<p>나이: {prev?.age || '(없음)'}</p>
<p>이메일: {prev?.email || '(없음)'}</p>
</div>
</div>
);
}
// 예시 3: 배열 값 추적 - 다중 숫자
export function ArrayValuesExample() {
const [a, setA] = useState(0);
const [b, setB] = useState(0);
const [c, setC] = useState(0);
const [prevA, prevB, prevC] = usePrevious([a, b, c]) || [];
return (
<div style={{ padding: '20px', border: '1px solid #ccc', marginBottom: '20px' }}>
<h3>예시 3: 배열 추적 - 다중 숫자</h3>
<div style={{ marginBottom: '15px' }}>
<div>
<label>
A:
<input
type="number"
value={a}
onChange={(e) => setA(Number(e.target.value))}
style={{ marginLeft: '10px' }}
/>
</label>
<span style={{ marginLeft: '20px' }}>이전: {prevA !== undefined ? prevA : '초기값'}</span>
</div>
<div style={{ marginTop: '10px' }}>
<label>
B:
<input
type="number"
value={b}
onChange={(e) => setB(Number(e.target.value))}
style={{ marginLeft: '10px' }}
/>
</label>
<span style={{ marginLeft: '20px' }}>이전: {prevB !== undefined ? prevB : '초기값'}</span>
</div>
<div style={{ marginTop: '10px' }}>
<label>
C:
<input
type="number"
value={c}
onChange={(e) => setC(Number(e.target.value))}
style={{ marginLeft: '10px' }}
/>
</label>
<span style={{ marginLeft: '20px' }}>이전: {prevC !== undefined ? prevC : '초기값'}</span>
</div>
</div>
</div>
);
}
// 예시 4: 변동 감지 - 데이터 수정 여부 추적
export function ChangeDetectionExample() {
const [data, setData] = useState({
title: 'React Hook Guide',
description: 'usePrevious 훅 사용법',
views: 1000,
});
const prevData = usePrevious(data);
const changes = {
titleChanged: prevData?.title !== data.title,
descriptionChanged: prevData?.description !== data.description,
viewsChanged: prevData?.views !== data.views,
};
const hasAnyChanges = Object.values(changes).some((v) => v);
const handleUpdate = (field, value) => {
setData((prev) => ({ ...prev, [field]: value }));
};
return (
<div style={{ padding: '20px', border: '1px solid #ccc', marginBottom: '20px' }}>
<h3>예시 4: 변동 감지 - 데이터 수정 여부 추적</h3>
<div style={{ marginBottom: '15px' }}>
<label>
제목:
<input
type="text"
value={data.title}
onChange={(e) => handleUpdate('title', e.target.value)}
style={{ marginLeft: '10px', width: '200px' }}
/>
{changes.titleChanged && (
<span style={{ color: 'orange', marginLeft: '10px' }}>수정됨</span>
)}
</label>
</div>
<div style={{ marginBottom: '15px' }}>
<label>
설명:
<input
type="text"
value={data.description}
onChange={(e) => handleUpdate('description', e.target.value)}
style={{ marginLeft: '10px', width: '200px' }}
/>
{changes.descriptionChanged && (
<span style={{ color: 'orange', marginLeft: '10px' }}>수정됨</span>
)}
</label>
</div>
<div style={{ marginBottom: '15px' }}>
<label>
조회수:
<input
type="number"
value={data.views}
onChange={(e) => handleUpdate('views', Number(e.target.value))}
style={{ marginLeft: '10px' }}
/>
{changes.viewsChanged && (
<span style={{ color: 'orange', marginLeft: '10px' }}>수정됨</span>
)}
</label>
</div>
<div style={{ padding: '10px', backgroundColor: hasAnyChanges ? '#fff3cd' : '#e8f5e9' }}>
<p style={{ margin: '0' }}>
<strong>
상태: {hasAnyChanges ? '데이터가 변경되었습니다' : '모든 데이터가 저장되었습니다'}
</strong>
</p>
</div>
</div>
);
}
// 전체 예시 조합
export function UsePreviousExamples() {
return (
<div style={{ padding: '20px', fontFamily: 'Arial, sans-serif' }}>
<h1>usePrevious 사용 예시</h1>
<p>이전 값을 추적하는 다양한 방법을 보여주는 예시들입니다.</p>
<CounterExample />
<UserFormExample />
<ArrayValuesExample />
<ChangeDetectionExample />
</div>
);
}
export default UsePreviousExamples;