From 18175a03de832767b531998c3327b897d8da90ca Mon Sep 17 00:00:00 2001 From: optrader Date: Thu, 20 Nov 2025 06:13:41 +0900 Subject: [PATCH] [251120] refactor: hooks - usePrevious.js, usePrevious.test.js, usePreviou... MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ๐Ÿ• ์ปค๋ฐ‹ ์‹œ๊ฐ„: 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: ์ฝ”๋“œ ์ตœ์ ํ™”๋กœ ์„ฑ๋Šฅ ๊ฐœ์„  ๊ธฐ๋Œ€ --- .../src/hooks/usePrevious.js | 18 ++- .../src/hooks/usePrevious.test.js | 114 +++++++++--------- .../src/hooks/usePreviousExample.jsx | 13 +- 3 files changed, 71 insertions(+), 74 deletions(-) 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 (

์˜ˆ์‹œ 1: ๋‹จ์ผ ๊ฐ’ ์ถ”์  (์นด์šดํ„ฐ)

ํ˜„์žฌ๊ฐ’: {count}

-

์ด์ „๊ฐ’: {prevCount !== undefined ? prevCount : '์ดˆ๊ธฐ๊ฐ’'}

+

์ด์ „๊ฐ’: {prevCountRef.current !== undefined ? prevCountRef.current : '์ดˆ๊ธฐ๊ฐ’'}

@@ -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 (
@@ -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,