From f8045aab5885487e5dd987fee2211119ed2398cb Mon Sep 17 00:00:00 2001 From: yong Date: Mon, 21 Jul 2025 21:45:09 +0900 Subject: [PATCH 01/19] =?UTF-8?q?feat:=20shallowEquals=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/lib/src/equals/shallowEquals.ts | 39 ++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/packages/lib/src/equals/shallowEquals.ts b/packages/lib/src/equals/shallowEquals.ts index 1cf22130..0a9ba629 100644 --- a/packages/lib/src/equals/shallowEquals.ts +++ b/packages/lib/src/equals/shallowEquals.ts @@ -1,3 +1,42 @@ +type Primitive = string | number | boolean | null | undefined | bigint | symbol; + export const shallowEquals = (a: unknown, b: unknown) => { + if (typeof a !== typeof b) return false; + + if (isPrimitive(a) && isPrimitive(b)) { + return a === b; + } + + if (isArray(a) && isArray(b)) { + if (a.length !== b.length) return false; + + for (let i = 0; i < a.length; i++) { + if (a[i] !== b[i]) return false; + } + + return true; + } + + if (isObject(a) && isObject(b)) { + const keysA = getObjectKeys(a); + const keysB = getObjectKeys(b); + + if (keysA.length !== keysB.length) return false; + + for (const key of keysA) { + if (a[key] !== b[key]) return false; + } + + return true; + } + return a === b; }; + +const isArray = (value: unknown): value is unknown[] => Array.isArray(value); + +const isObject = (value: unknown): value is object => typeof value === "object" && value !== null; + +const isPrimitive = (value: unknown): value is Primitive => typeof value !== "object" || value === null; + +const getObjectKeys = (o: T): (keyof T)[] => Object.keys(o) as (keyof T)[]; From 6b6a524d87849ee166be3a1a76aa15f2f98bba93 Mon Sep 17 00:00:00 2001 From: yong Date: Mon, 21 Jul 2025 22:02:26 +0900 Subject: [PATCH 02/19] =?UTF-8?q?feat:=20deepEquals=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/lib/src/equals/deepEquals.ts | 31 ++++++++++++++++++++++++ packages/lib/src/equals/shallowEquals.ts | 10 +------- packages/lib/src/equals/utils.ts | 9 +++++++ 3 files changed, 41 insertions(+), 9 deletions(-) create mode 100644 packages/lib/src/equals/utils.ts diff --git a/packages/lib/src/equals/deepEquals.ts b/packages/lib/src/equals/deepEquals.ts index f5f4ebd5..61487f5f 100644 --- a/packages/lib/src/equals/deepEquals.ts +++ b/packages/lib/src/equals/deepEquals.ts @@ -1,3 +1,34 @@ +import { getObjectKeys, isArray, isObject, isPrimitive } from "./utils"; + export const deepEquals = (a: unknown, b: unknown) => { + if (typeof a !== typeof b) return false; + + if (isPrimitive(a) && isPrimitive(b)) { + return a === b; + } + + if (isArray(a) && isArray(b)) { + if (a.length !== b.length) return false; + + for (let i = 0; i < a.length; i++) { + if (!deepEquals(a[i], b[i])) return false; + } + + return true; + } + + if (isObject(a) && isObject(b)) { + const keysA = getObjectKeys(a); + const keysB = getObjectKeys(b); + + if (keysA.length !== keysB.length) return false; + + for (const key of keysA) { + if (!deepEquals(a[key], b[key])) return false; + } + + return true; + } + return a === b; }; diff --git a/packages/lib/src/equals/shallowEquals.ts b/packages/lib/src/equals/shallowEquals.ts index 0a9ba629..786ff072 100644 --- a/packages/lib/src/equals/shallowEquals.ts +++ b/packages/lib/src/equals/shallowEquals.ts @@ -1,4 +1,4 @@ -type Primitive = string | number | boolean | null | undefined | bigint | symbol; +import { getObjectKeys, isArray, isObject, isPrimitive } from "./utils"; export const shallowEquals = (a: unknown, b: unknown) => { if (typeof a !== typeof b) return false; @@ -32,11 +32,3 @@ export const shallowEquals = (a: unknown, b: unknown) => { return a === b; }; - -const isArray = (value: unknown): value is unknown[] => Array.isArray(value); - -const isObject = (value: unknown): value is object => typeof value === "object" && value !== null; - -const isPrimitive = (value: unknown): value is Primitive => typeof value !== "object" || value === null; - -const getObjectKeys = (o: T): (keyof T)[] => Object.keys(o) as (keyof T)[]; diff --git a/packages/lib/src/equals/utils.ts b/packages/lib/src/equals/utils.ts new file mode 100644 index 00000000..ab402ada --- /dev/null +++ b/packages/lib/src/equals/utils.ts @@ -0,0 +1,9 @@ +export type Primitive = string | number | boolean | null | undefined | bigint | symbol; + +export const isArray = (value: unknown): value is unknown[] => Array.isArray(value); + +export const isObject = (value: unknown): value is object => typeof value === "object" && value !== null; + +export const isPrimitive = (value: unknown): value is Primitive => typeof value !== "object" || value === null; + +export const getObjectKeys = (o: T): (keyof T)[] => Object.keys(o) as (keyof T)[]; From d313784ef3438981335ee9e3b3b22caed6981dee Mon Sep 17 00:00:00 2001 From: yong Date: Mon, 21 Jul 2025 22:05:38 +0900 Subject: [PATCH 03/19] =?UTF-8?q?feat:=20useState=EB=A5=BC=20=EC=9D=B4?= =?UTF-8?q?=EC=9A=A9=ED=95=9C=20useRef=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/lib/src/hooks/useRef.ts | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/packages/lib/src/hooks/useRef.ts b/packages/lib/src/hooks/useRef.ts index 285d4ae7..a0ca84ca 100644 --- a/packages/lib/src/hooks/useRef.ts +++ b/packages/lib/src/hooks/useRef.ts @@ -1,4 +1,9 @@ +import { useState } from "react"; + export function useRef(initialValue: T): { current: T } { - // useState를 이용해서 만들어보세요. - return { current: initialValue }; + const [ref] = useState(() => ({ + current: initialValue, + })); + + return ref; } From a683aa330cf1d216fa90f3715df719817ea8c150 Mon Sep 17 00:00:00 2001 From: yong Date: Mon, 21 Jul 2025 22:09:16 +0900 Subject: [PATCH 04/19] =?UTF-8?q?feat:=20useMemo=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/lib/src/hooks/useMemo.ts | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/packages/lib/src/hooks/useMemo.ts b/packages/lib/src/hooks/useMemo.ts index e80692e2..524be6aa 100644 --- a/packages/lib/src/hooks/useMemo.ts +++ b/packages/lib/src/hooks/useMemo.ts @@ -1,8 +1,16 @@ /* eslint-disable @typescript-eslint/no-unused-vars */ import type { DependencyList } from "react"; import { shallowEquals } from "../equals"; +import { useRef } from "./useRef"; export function useMemo(factory: () => T, _deps: DependencyList, _equals = shallowEquals): T { - // 직접 작성한 useRef를 통해서 만들어보세요. - return factory(); + const memoized = useRef(factory()); + const prevDeps = useRef(_deps); + + if (!_equals(prevDeps.current, _deps)) { + memoized.current = factory(); + prevDeps.current = _deps; + } + + return memoized.current; } From 40dc996d2c4f34ad965abfa5a969830a9bd0a4dc Mon Sep 17 00:00:00 2001 From: yong Date: Mon, 21 Jul 2025 22:15:36 +0900 Subject: [PATCH 05/19] =?UTF-8?q?feat:=20useCallback=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/lib/src/hooks/useCallback.ts | 9 +++++---- packages/lib/src/hooks/useMemo.ts | 1 - 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/lib/src/hooks/useCallback.ts b/packages/lib/src/hooks/useCallback.ts index 712bc898..1700a30b 100644 --- a/packages/lib/src/hooks/useCallback.ts +++ b/packages/lib/src/hooks/useCallback.ts @@ -1,7 +1,8 @@ -/* eslint-disable @typescript-eslint/no-unused-vars,@typescript-eslint/no-unsafe-function-type */ import type { DependencyList } from "react"; +import { useMemo } from "./useMemo"; -export function useCallback(factory: T, _deps: DependencyList) { - // 직접 작성한 useMemo를 통해서 만들어보세요. - return factory as T; +export function useCallback unknown>(factory: T, deps: DependencyList) { + const memoized = useMemo(() => factory, [...deps, factory]); + + return memoized; } diff --git a/packages/lib/src/hooks/useMemo.ts b/packages/lib/src/hooks/useMemo.ts index 524be6aa..5cfa4ee4 100644 --- a/packages/lib/src/hooks/useMemo.ts +++ b/packages/lib/src/hooks/useMemo.ts @@ -1,4 +1,3 @@ -/* eslint-disable @typescript-eslint/no-unused-vars */ import type { DependencyList } from "react"; import { shallowEquals } from "../equals"; import { useRef } from "./useRef"; From 58e135f78e04110bcc12165cc5e7205269c525c4 Mon Sep 17 00:00:00 2001 From: yong Date: Mon, 21 Jul 2025 22:43:57 +0900 Subject: [PATCH 06/19] =?UTF-8?q?feat:=20shallowEquals=20=EB=B9=84?= =?UTF-8?q?=EA=B5=90=20=EC=97=B0=EC=82=B0=EC=9D=84=20Object.is=EB=A1=9C=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/lib/src/equals/shallowEquals.ts | 22 +++++----------------- 1 file changed, 5 insertions(+), 17 deletions(-) diff --git a/packages/lib/src/equals/shallowEquals.ts b/packages/lib/src/equals/shallowEquals.ts index 786ff072..d11f3260 100644 --- a/packages/lib/src/equals/shallowEquals.ts +++ b/packages/lib/src/equals/shallowEquals.ts @@ -1,33 +1,21 @@ import { getObjectKeys, isArray, isObject, isPrimitive } from "./utils"; -export const shallowEquals = (a: unknown, b: unknown) => { - if (typeof a !== typeof b) return false; +const comparePrimitive = (a: unknown, b: unknown) => Object.is(a, b); +export const shallowEquals = (a: unknown, b: unknown) => { if (isPrimitive(a) && isPrimitive(b)) { - return a === b; + return comparePrimitive(a, b); } if (isArray(a) && isArray(b)) { - if (a.length !== b.length) return false; - - for (let i = 0; i < a.length; i++) { - if (a[i] !== b[i]) return false; - } - - return true; + return a.length === b.length && a.every((item, index) => comparePrimitive(item, b[index])); } if (isObject(a) && isObject(b)) { const keysA = getObjectKeys(a); const keysB = getObjectKeys(b); - if (keysA.length !== keysB.length) return false; - - for (const key of keysA) { - if (a[key] !== b[key]) return false; - } - - return true; + return keysA.length === keysB.length && keysA.every((key) => comparePrimitive(a[key], b[key])); } return a === b; From bb47b7cbbe57b4a3d2158f80d5c861fc9b6fb788 Mon Sep 17 00:00:00 2001 From: yong Date: Mon, 21 Jul 2025 22:47:54 +0900 Subject: [PATCH 07/19] =?UTF-8?q?feat:=20deepEquals=20=EB=B9=84=EA=B5=90?= =?UTF-8?q?=20=EC=97=B0=EC=82=B0=EC=9D=84=20Object.is=EB=A1=9C=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/lib/src/equals/deepEquals.ts | 22 +++++----------------- 1 file changed, 5 insertions(+), 17 deletions(-) diff --git a/packages/lib/src/equals/deepEquals.ts b/packages/lib/src/equals/deepEquals.ts index 61487f5f..71053b99 100644 --- a/packages/lib/src/equals/deepEquals.ts +++ b/packages/lib/src/equals/deepEquals.ts @@ -1,33 +1,21 @@ import { getObjectKeys, isArray, isObject, isPrimitive } from "./utils"; -export const deepEquals = (a: unknown, b: unknown) => { - if (typeof a !== typeof b) return false; +const comparePrimitive = (a: unknown, b: unknown) => Object.is(a, b); +export const deepEquals = (a: unknown, b: unknown): boolean => { if (isPrimitive(a) && isPrimitive(b)) { - return a === b; + return comparePrimitive(a, b); } if (isArray(a) && isArray(b)) { - if (a.length !== b.length) return false; - - for (let i = 0; i < a.length; i++) { - if (!deepEquals(a[i], b[i])) return false; - } - - return true; + return a.length === b.length && a.every((item, index) => deepEquals(item, b[index])); } if (isObject(a) && isObject(b)) { const keysA = getObjectKeys(a); const keysB = getObjectKeys(b); - if (keysA.length !== keysB.length) return false; - - for (const key of keysA) { - if (!deepEquals(a[key], b[key])) return false; - } - - return true; + return keysA.length === keysB.length && keysA.every((key) => deepEquals(a[key], b[key])); } return a === b; From c2d526d1ecfac866ceb4bd718104842028660493 Mon Sep 17 00:00:00 2001 From: yong Date: Mon, 21 Jul 2025 23:02:05 +0900 Subject: [PATCH 08/19] =?UTF-8?q?feat:=20useMemo=20=EC=B4=88=EA=B8=B0?= =?UTF-8?q?=ED=99=94=20=EB=A1=9C=EC=A7=81=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/lib/src/hooks/useMemo.ts | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/packages/lib/src/hooks/useMemo.ts b/packages/lib/src/hooks/useMemo.ts index 5cfa4ee4..1fb1f715 100644 --- a/packages/lib/src/hooks/useMemo.ts +++ b/packages/lib/src/hooks/useMemo.ts @@ -1,14 +1,16 @@ import type { DependencyList } from "react"; -import { shallowEquals } from "../equals"; import { useRef } from "./useRef"; +import { shallowEquals } from "../equals"; -export function useMemo(factory: () => T, _deps: DependencyList, _equals = shallowEquals): T { - const memoized = useRef(factory()); - const prevDeps = useRef(_deps); +export function useMemo(factory: () => T, deps: DependencyList, _equals = shallowEquals): T | undefined { + const memoized = useRef(undefined); + const prevDeps = useRef(deps); + const isInitial = useRef(true); - if (!_equals(prevDeps.current, _deps)) { + if (isInitial.current || !_equals(prevDeps.current, deps)) { + isInitial.current = false; memoized.current = factory(); - prevDeps.current = _deps; + prevDeps.current = deps; } return memoized.current; From ec04970c3181b36ab2c17388304527a7259f3ffa Mon Sep 17 00:00:00 2001 From: yong Date: Mon, 21 Jul 2025 23:05:20 +0900 Subject: [PATCH 09/19] =?UTF-8?q?fix:=20useMemo=20dependency=20=EB=B0=B0?= =?UTF-8?q?=EC=97=B4=EC=97=90=20factory=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/lib/src/hooks/useCallback.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/lib/src/hooks/useCallback.ts b/packages/lib/src/hooks/useCallback.ts index 1700a30b..208c5c26 100644 --- a/packages/lib/src/hooks/useCallback.ts +++ b/packages/lib/src/hooks/useCallback.ts @@ -2,7 +2,7 @@ import type { DependencyList } from "react"; import { useMemo } from "./useMemo"; export function useCallback unknown>(factory: T, deps: DependencyList) { - const memoized = useMemo(() => factory, [...deps, factory]); + const memoized = useMemo(() => factory, [...deps]); return memoized; } From 1e41ca7217f02970869dbc1e2802748cf15d5b89 Mon Sep 17 00:00:00 2001 From: yong Date: Mon, 21 Jul 2025 23:35:04 +0900 Subject: [PATCH 10/19] =?UTF-8?q?feat:=20useShallowState=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/lib/src/equals/utils.ts | 5 +++++ packages/lib/src/hooks/useShallowState.ts | 20 ++++++++++++++++---- 2 files changed, 21 insertions(+), 4 deletions(-) diff --git a/packages/lib/src/equals/utils.ts b/packages/lib/src/equals/utils.ts index ab402ada..b03e1b3f 100644 --- a/packages/lib/src/equals/utils.ts +++ b/packages/lib/src/equals/utils.ts @@ -7,3 +7,8 @@ export const isObject = (value: unknown): value is object => typeof value === "o export const isPrimitive = (value: unknown): value is Primitive => typeof value !== "object" || value === null; export const getObjectKeys = (o: T): (keyof T)[] => Object.keys(o) as (keyof T)[]; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export function isFunction(value: any): value is (...args: any[]) => any { + return typeof value === "function"; +} diff --git a/packages/lib/src/hooks/useShallowState.ts b/packages/lib/src/hooks/useShallowState.ts index 7b589748..28d1c440 100644 --- a/packages/lib/src/hooks/useShallowState.ts +++ b/packages/lib/src/hooks/useShallowState.ts @@ -1,7 +1,19 @@ -import { useState } from "react"; +import { useCallback, useState } from "react"; import { shallowEquals } from "../equals"; +import { isFunction } from "../equals/utils"; -export const useShallowState = (initialValue: Parameters>[0]) => { - // useState를 사용하여 상태를 관리하고, shallowEquals를 사용하여 상태 변경을 감지하는 훅을 구현합니다. - return useState(initialValue); +// useState를 사용하여 상태를 관리하고, shallowEquals를 사용하여 상태 변경을 감지하는 훅을 구현합니다. + +export const useShallowState = (initialValue: T | (() => T)) => { + const [state, setState] = useState(initialValue); + const setShallowState = useCallback((value: T | ((prev: T) => T)) => { + const nextValue = isFunction(value) ? value(state) : value; + if (shallowEquals(state, nextValue)) { + return; + } + + setState(nextValue); + }, []); + + return [state, setShallowState] as const; }; From e0021cf0db5ea9b10b565eb6b4975ff3aeb4254f Mon Sep 17 00:00:00 2001 From: yong Date: Mon, 21 Jul 2025 23:38:25 +0900 Subject: [PATCH 11/19] =?UTF-8?q?feat:=20useDeepMemo=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/lib/src/hooks/useCallback.ts | 1 + packages/lib/src/hooks/useDeepMemo.ts | 1 - packages/lib/src/hooks/useMemo.ts | 4 ++-- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/lib/src/hooks/useCallback.ts b/packages/lib/src/hooks/useCallback.ts index 208c5c26..e64642e2 100644 --- a/packages/lib/src/hooks/useCallback.ts +++ b/packages/lib/src/hooks/useCallback.ts @@ -1,3 +1,4 @@ +/* eslint-disable react-hooks/exhaustive-deps */ import type { DependencyList } from "react"; import { useMemo } from "./useMemo"; diff --git a/packages/lib/src/hooks/useDeepMemo.ts b/packages/lib/src/hooks/useDeepMemo.ts index 3157958c..9a94cb02 100644 --- a/packages/lib/src/hooks/useDeepMemo.ts +++ b/packages/lib/src/hooks/useDeepMemo.ts @@ -4,6 +4,5 @@ import { useMemo } from "./useMemo"; import { deepEquals } from "../equals"; export function useDeepMemo(factory: () => T, deps: DependencyList): T { - // 직접 작성한 useMemo를 참고해서 만들어보세요. return useMemo(factory, deps, deepEquals); } diff --git a/packages/lib/src/hooks/useMemo.ts b/packages/lib/src/hooks/useMemo.ts index 1fb1f715..30dc161a 100644 --- a/packages/lib/src/hooks/useMemo.ts +++ b/packages/lib/src/hooks/useMemo.ts @@ -2,7 +2,7 @@ import type { DependencyList } from "react"; import { useRef } from "./useRef"; import { shallowEquals } from "../equals"; -export function useMemo(factory: () => T, deps: DependencyList, _equals = shallowEquals): T | undefined { +export function useMemo(factory: () => T, deps: DependencyList, _equals = shallowEquals): T { const memoized = useRef(undefined); const prevDeps = useRef(deps); const isInitial = useRef(true); @@ -13,5 +13,5 @@ export function useMemo(factory: () => T, deps: DependencyList, _equals = sha prevDeps.current = deps; } - return memoized.current; + return memoized.current!; } From 7f2441ffe1b5f7f2f084670143b1644d56245d18 Mon Sep 17 00:00:00 2001 From: yong Date: Tue, 22 Jul 2025 00:18:23 +0900 Subject: [PATCH 12/19] =?UTF-8?q?feat:=20useAutoCallback=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/lib/src/hooks/useAutoCallback.ts | 5 ++++- packages/lib/src/hooks/useCallback.ts | 3 ++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/lib/src/hooks/useAutoCallback.ts b/packages/lib/src/hooks/useAutoCallback.ts index 23100162..c1fe460c 100644 --- a/packages/lib/src/hooks/useAutoCallback.ts +++ b/packages/lib/src/hooks/useAutoCallback.ts @@ -3,5 +3,8 @@ import { useCallback } from "./useCallback"; import { useRef } from "./useRef"; export const useAutoCallback = (fn: T): T => { - return fn; + const fnRef = useRef(fn); + fnRef.current = fn; + + return useCallback((...args: Parameters) => fnRef.current(...args), []) as T; }; diff --git a/packages/lib/src/hooks/useCallback.ts b/packages/lib/src/hooks/useCallback.ts index e64642e2..1863b958 100644 --- a/packages/lib/src/hooks/useCallback.ts +++ b/packages/lib/src/hooks/useCallback.ts @@ -1,8 +1,9 @@ /* eslint-disable react-hooks/exhaustive-deps */ import type { DependencyList } from "react"; import { useMemo } from "./useMemo"; +import type { AnyFunction } from "../types"; -export function useCallback unknown>(factory: T, deps: DependencyList) { +export function useCallback(factory: T, deps: DependencyList): T { const memoized = useMemo(() => factory, [...deps]); return memoized; From 66fe47b5d045e6e423ac99377694ef63276e1a51 Mon Sep 17 00:00:00 2001 From: yong Date: Tue, 22 Jul 2025 00:56:29 +0900 Subject: [PATCH 13/19] =?UTF-8?q?feat:=20memo,=20deepMemo=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/lib/src/hocs/deepMemo.ts | 20 ++++++++++++++++++-- packages/lib/src/hocs/memo.ts | 19 +++++++++++++++++-- 2 files changed, 35 insertions(+), 4 deletions(-) diff --git a/packages/lib/src/hocs/deepMemo.ts b/packages/lib/src/hocs/deepMemo.ts index 13f7f18e..18c97c33 100644 --- a/packages/lib/src/hocs/deepMemo.ts +++ b/packages/lib/src/hocs/deepMemo.ts @@ -1,5 +1,21 @@ -import type { FunctionComponent } from "react"; +import { useRef, type FunctionComponent } from "react"; +import { deepEquals } from "../equals"; export function deepMemo

(Component: FunctionComponent

) { - return Component; + const MemoizedComponent = (props: P) => { + const prevPropsRef = useRef

(null); + const prevResultRef = useRef | null>(null); + + if (prevPropsRef.current && deepEquals(prevPropsRef.current, props)) { + return prevResultRef.current!; + } + + const result = Component(props); + prevPropsRef.current = props; + prevResultRef.current = result; + + return result; + }; + + return MemoizedComponent; } diff --git a/packages/lib/src/hocs/memo.ts b/packages/lib/src/hocs/memo.ts index 532c3a5c..18f5139f 100644 --- a/packages/lib/src/hocs/memo.ts +++ b/packages/lib/src/hocs/memo.ts @@ -1,6 +1,21 @@ -import { type FunctionComponent } from "react"; +import { useRef, type FunctionComponent } from "react"; import { shallowEquals } from "../equals"; export function memo

(Component: FunctionComponent

, equals = shallowEquals) { - return Component; + const MemoizedComponent = (props: P) => { + const prevPropsRef = useRef

(null); + const prevResultRef = useRef | null>(null); + + if (prevPropsRef.current && equals(prevPropsRef.current, props)) { + return prevResultRef.current!; + } + + const result = Component(props); + prevPropsRef.current = props; + prevResultRef.current = result; + + return result; + }; + + return MemoizedComponent; } From ce447b92b852fc4b05200ea61d2f9d852d1dc918 Mon Sep 17 00:00:00 2001 From: yong Date: Tue, 22 Jul 2025 22:17:34 +0900 Subject: [PATCH 14/19] =?UTF-8?q?feat:=20useStorage=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/lib/src/createObserver.ts | 4 +++- packages/lib/src/hooks/useStorage.ts | 5 +++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/packages/lib/src/createObserver.ts b/packages/lib/src/createObserver.ts index fe97bf6b..d55b82ec 100644 --- a/packages/lib/src/createObserver.ts +++ b/packages/lib/src/createObserver.ts @@ -6,6 +6,8 @@ export const createObserver = () => { // useSyncExternalStore 에서 활용할 수 있도록 subscribe 함수를 수정합니다. const subscribe = (fn: Listener) => { listeners.add(fn); + + return () => unsubscribe(fn); }; const unsubscribe = (fn: Listener) => { @@ -14,5 +16,5 @@ export const createObserver = () => { const notify = () => listeners.forEach((listener) => listener()); - return { subscribe, notify }; + return { subscribe, notify, unsubscribe }; }; diff --git a/packages/lib/src/hooks/useStorage.ts b/packages/lib/src/hooks/useStorage.ts index fdc97a6f..f7252d78 100644 --- a/packages/lib/src/hooks/useStorage.ts +++ b/packages/lib/src/hooks/useStorage.ts @@ -4,6 +4,7 @@ import type { createStorage } from "../createStorage"; type Storage = ReturnType>; export const useStorage = (storage: Storage) => { - // useSyncExternalStore를 사용해서 storage의 상태를 구독하고 가져오는 훅을 구현해보세요. - return storage.get(); + const state = useSyncExternalStore(storage.subscribe, storage.get); + + return state; }; From d6506508ab992b484e8ba588b02ed8016b75bf02 Mon Sep 17 00:00:00 2001 From: yong Date: Wed, 23 Jul 2025 23:05:03 +0900 Subject: [PATCH 15/19] =?UTF-8?q?feat:=20useStore=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/lib/src/hooks/useShallowSelector.ts | 13 +++++++++++-- packages/lib/src/hooks/useShallowState.ts | 4 ++-- packages/lib/src/hooks/useStore.ts | 4 +++- 3 files changed, 16 insertions(+), 5 deletions(-) diff --git a/packages/lib/src/hooks/useShallowSelector.ts b/packages/lib/src/hooks/useShallowSelector.ts index b75ed791..c2123dbf 100644 --- a/packages/lib/src/hooks/useShallowSelector.ts +++ b/packages/lib/src/hooks/useShallowSelector.ts @@ -4,6 +4,15 @@ import { shallowEquals } from "../equals"; type Selector = (state: T) => S; export const useShallowSelector = (selector: Selector) => { - // 이전 상태를 저장하고, shallowEquals를 사용하여 상태가 변경되었는지 확인하는 훅을 구현합니다. - return (state: T): S => selector(state); + const prevResultRef = useRef(null); + + return (state: T): S => { + const currentResult = selector(state); + if (shallowEquals(prevResultRef.current, currentResult)) { + return prevResultRef.current!; + } + + prevResultRef.current = currentResult; + return currentResult; + }; }; diff --git a/packages/lib/src/hooks/useShallowState.ts b/packages/lib/src/hooks/useShallowState.ts index 28d1c440..603e394a 100644 --- a/packages/lib/src/hooks/useShallowState.ts +++ b/packages/lib/src/hooks/useShallowState.ts @@ -1,11 +1,11 @@ +/* eslint-disable react-hooks/exhaustive-deps */ import { useCallback, useState } from "react"; import { shallowEquals } from "../equals"; import { isFunction } from "../equals/utils"; -// useState를 사용하여 상태를 관리하고, shallowEquals를 사용하여 상태 변경을 감지하는 훅을 구현합니다. - export const useShallowState = (initialValue: T | (() => T)) => { const [state, setState] = useState(initialValue); + const setShallowState = useCallback((value: T | ((prev: T) => T)) => { const nextValue = isFunction(value) ? value(state) : value; if (shallowEquals(state, nextValue)) { diff --git a/packages/lib/src/hooks/useStore.ts b/packages/lib/src/hooks/useStore.ts index acf3ad79..5a302640 100644 --- a/packages/lib/src/hooks/useStore.ts +++ b/packages/lib/src/hooks/useStore.ts @@ -9,5 +9,7 @@ const defaultSelector = (state: T) => state as unknown as S; export const useStore = (store: Store, selector: (state: T) => S = defaultSelector) => { // useSyncExternalStore와 useShallowSelector를 사용해서 store의 상태를 구독하고 가져오는 훅을 구현해보세요. const shallowSelector = useShallowSelector(selector); - return shallowSelector(store.getState()); + const state = useSyncExternalStore(store.subscribe, () => shallowSelector(store.getState())); + + return state; }; From e80b88461c868dd47046f00693e03343dee5cc94 Mon Sep 17 00:00:00 2001 From: yong Date: Wed, 23 Jul 2025 23:11:49 +0900 Subject: [PATCH 16/19] =?UTF-8?q?feat:=20useRouter=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/lib/src/hooks/useRouter.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/lib/src/hooks/useRouter.ts b/packages/lib/src/hooks/useRouter.ts index a18ae01f..8d368504 100644 --- a/packages/lib/src/hooks/useRouter.ts +++ b/packages/lib/src/hooks/useRouter.ts @@ -8,5 +8,7 @@ const defaultSelector = (state: T) => state as unknown as S; export const useRouter = , S>(router: T, selector = defaultSelector) => { // useSyncExternalStore를 사용하여 router의 상태를 구독하고 가져오는 훅을 구현합니다. const shallowSelector = useShallowSelector(selector); - return shallowSelector(router); + const state = useSyncExternalStore(router.subscribe, () => shallowSelector(router)); + + return state; }; From 9a288da6607a529f90d6eb431fbb0f7f4c7e32d9 Mon Sep 17 00:00:00 2001 From: yong Date: Thu, 24 Jul 2025 01:44:58 +0900 Subject: [PATCH 17/19] =?UTF-8?q?feat:=20ToastProvider=20=EB=A9=94?= =?UTF-8?q?=EB=AA=A8=EC=9D=B4=EC=A0=9C=EC=9D=B4=EC=85=98=20=EC=B5=9C?= =?UTF-8?q?=EC=A0=81=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/components/toast/ToastProvider.tsx | 57 ++++++++++--------- 1 file changed, 30 insertions(+), 27 deletions(-) diff --git a/packages/app/src/components/toast/ToastProvider.tsx b/packages/app/src/components/toast/ToastProvider.tsx index 0aed9451..aaecbd8f 100644 --- a/packages/app/src/components/toast/ToastProvider.tsx +++ b/packages/app/src/components/toast/ToastProvider.tsx @@ -1,52 +1,55 @@ /* eslint-disable react-refresh/only-export-components */ -import { createContext, memo, type PropsWithChildren, useContext, useReducer } from "react"; +import { createContext, memo, type PropsWithChildren, useCallback, useContext, useMemo, useReducer } from "react"; import { createPortal } from "react-dom"; import { Toast } from "./Toast"; -import { createActions, initialState, toastReducer, type ToastType } from "./toastReducer"; +import { createActions, initialState, toastReducer, type ToastState, type ToastType } from "./toastReducer"; import { debounce } from "../../utils"; type ShowToast = (message: string, type: ToastType) => void; type Hide = () => void; -const ToastContext = createContext<{ - message: string; - type: ToastType; - show: ShowToast; - hide: Hide; -}>({ - ...initialState, - show: () => null, - hide: () => null, -}); +const ToastStateContext = createContext(initialState); +const ToastCommandContext = createContext<{ show: ShowToast; hide: Hide }>({ show: () => null, hide: () => null }); const DEFAULT_DELAY = 3000; -const useToastContext = () => useContext(ToastContext); +const useToastStateContext = () => useContext(ToastStateContext); +const useToastCommandContext = () => useContext(ToastCommandContext); export const useToastCommand = () => { - const { show, hide } = useToastContext(); - return { show, hide }; + const { show, hide } = useToastCommandContext(); + + return useMemo(() => ({ show, hide }), [show, hide]); }; export const useToastState = () => { - const { message, type } = useToastContext(); - return { message, type }; + const { message, type } = useToastStateContext(); + + return useMemo(() => ({ message, type }), [message, type]); }; export const ToastProvider = memo(({ children }: PropsWithChildren) => { const [state, dispatch] = useReducer(toastReducer, initialState); - const { show, hide } = createActions(dispatch); + const { show, hide } = useMemo(() => createActions(dispatch), []); const visible = state.message !== ""; - const hideAfter = debounce(hide, DEFAULT_DELAY); + const hideAfter = useMemo(() => debounce(hide, DEFAULT_DELAY), [hide]); + + const showWithHide: ShowToast = useCallback( + (...args) => { + show(...args); + hideAfter(); + }, + [show, hideAfter], + ); - const showWithHide: ShowToast = (...args) => { - show(...args); - hideAfter(); - }; + const commands = useMemo(() => ({ show: showWithHide, hide }), [showWithHide, hide]); + const value = useMemo(() => ({ ...state }), [state]); return ( - - {children} - {visible && createPortal(, document.body)} - + + + {children} + {visible && createPortal(, document.body)} + + ); }); From 395c38a08c7673efd7d356e4c1ae320076575663 Mon Sep 17 00:00:00 2001 From: yong Date: Thu, 24 Jul 2025 22:34:00 +0900 Subject: [PATCH 18/19] =?UTF-8?q?feat:=20ToastProvider=20useMemo,=20useCal?= =?UTF-8?q?lback=20import=20=EA=B2=BD=EB=A1=9C=EB=A5=BC=20=EC=A7=81?= =?UTF-8?q?=EC=A0=91=20=EB=A7=8C=EB=93=A0=20=EB=AA=A8=EB=93=88=EB=A1=9C=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/app/src/components/toast/ToastProvider.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/app/src/components/toast/ToastProvider.tsx b/packages/app/src/components/toast/ToastProvider.tsx index aaecbd8f..49e517d4 100644 --- a/packages/app/src/components/toast/ToastProvider.tsx +++ b/packages/app/src/components/toast/ToastProvider.tsx @@ -1,9 +1,10 @@ /* eslint-disable react-refresh/only-export-components */ -import { createContext, memo, type PropsWithChildren, useCallback, useContext, useMemo, useReducer } from "react"; +import { createContext, memo, type PropsWithChildren, useContext, useReducer } from "react"; import { createPortal } from "react-dom"; import { Toast } from "./Toast"; import { createActions, initialState, toastReducer, type ToastState, type ToastType } from "./toastReducer"; import { debounce } from "../../utils"; +import { useCallback, useMemo } from "@hanghae-plus/lib/src/hooks"; type ShowToast = (message: string, type: ToastType) => void; type Hide = () => void; From 062575fa7b1a3a34e1f9053f11b0ff071d669436 Mon Sep 17 00:00:00 2001 From: yong Date: Thu, 24 Jul 2025 22:42:57 +0900 Subject: [PATCH 19/19] =?UTF-8?q?feat:=20ToastProvider=EC=97=90=20useCallb?= =?UTF-8?q?ack=EC=9D=84=20useAutoCallback=EC=9C=BC=EB=A1=9C=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/app/src/components/toast/ToastProvider.tsx | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/packages/app/src/components/toast/ToastProvider.tsx b/packages/app/src/components/toast/ToastProvider.tsx index 49e517d4..31713e2c 100644 --- a/packages/app/src/components/toast/ToastProvider.tsx +++ b/packages/app/src/components/toast/ToastProvider.tsx @@ -4,7 +4,7 @@ import { createPortal } from "react-dom"; import { Toast } from "./Toast"; import { createActions, initialState, toastReducer, type ToastState, type ToastType } from "./toastReducer"; import { debounce } from "../../utils"; -import { useCallback, useMemo } from "@hanghae-plus/lib/src/hooks"; +import { useAutoCallback, useMemo } from "@hanghae-plus/lib/src/hooks"; type ShowToast = (message: string, type: ToastType) => void; type Hide = () => void; @@ -34,13 +34,10 @@ export const ToastProvider = memo(({ children }: PropsWithChildren) => { const hideAfter = useMemo(() => debounce(hide, DEFAULT_DELAY), [hide]); - const showWithHide: ShowToast = useCallback( - (...args) => { - show(...args); - hideAfter(); - }, - [show, hideAfter], - ); + const showWithHide: ShowToast = useAutoCallback((...args) => { + show(...args); + hideAfter(); + }); const commands = useMemo(() => ({ show: showWithHide, hide }), [showWithHide, hide]); const value = useMemo(() => ({ ...state }), [state]);