From d96801fd041ee089abb2e2b2f686228d395978f2 Mon Sep 17 00:00:00 2001 From: Legitgoons Date: Mon, 21 Jul 2025 15:45:35 +0900 Subject: [PATCH 01/21] =?UTF-8?q?=EB=B9=88=20=EC=BB=A4=EB=B0=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit From a0c12622f7d053d06260c1b7781e53ea4adb5557 Mon Sep 17 00:00:00 2001 From: Legitgoons Date: Tue, 22 Jul 2025 21:57:46 +0900 Subject: [PATCH 02/21] =?UTF-8?q?feat:=20=ED=83=80=EC=9E=85=20=EA=B0=80?= =?UTF-8?q?=EB=93=9C=EB=A5=BC=20=EC=82=AC=EC=9A=A9=ED=95=B4=20isObject=20?= =?UTF-8?q?=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/util.ts | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 packages/lib/src/equals/util.ts diff --git a/packages/lib/src/equals/util.ts b/packages/lib/src/equals/util.ts new file mode 100644 index 00000000..d068f21d --- /dev/null +++ b/packages/lib/src/equals/util.ts @@ -0,0 +1,8 @@ +/** + * 주어진 값이 객체인지 확인 + * @param {unknown} value - 객체인지 확인할 값 + * @returns {value is Record} 객체인지 확인 결과 + */ +export function isObject(value?: unknown): value is Record { + return value !== null && typeof value === "object"; +} From ea8d4236b1ef6de87896fc6af63c41d037368080 Mon Sep 17 00:00:00 2001 From: Legitgoons Date: Tue, 22 Jul 2025 21:57:57 +0900 Subject: [PATCH 03/21] =?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 | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/packages/lib/src/equals/shallowEquals.ts b/packages/lib/src/equals/shallowEquals.ts index 1cf22130..ad88728a 100644 --- a/packages/lib/src/equals/shallowEquals.ts +++ b/packages/lib/src/equals/shallowEquals.ts @@ -1,3 +1,23 @@ +import { isObject } from "./util"; + +/** + * 두 값의 얕은 비교 결과를 반환 + * @param {unknown} a 비교할 첫 번째 값 + * @param {unknown} b 비교할 두 번째 값 + * @returns {boolean} 두 값이 얕은 비교 결과 + */ export const shallowEquals = (a: unknown, b: unknown) => { - return a === b; + // 1. 두 값이 정확히 같은지 확인 (참조가 같은 경우) + if (a === b) return true; + + // 2. 둘 중 하나라도 객체가 아닌 경우 처리 + if (!isObject(a) || !isObject(b)) return false; + + // 3. 객체의 키 개수가 다른 경우 처리 + const keysA = Object.keys(a); + const keysB = Object.keys(b); + if (keysA.length !== keysB.length) return false; + + // 4. 모든 키에 대해 얕은 비교 수행 + return keysA.every((key) => a[key] === b[key]); }; From 0ed217c1f93758094a0e32fd8a037665f9a9886f Mon Sep 17 00:00:00 2001 From: Legitgoons Date: Tue, 22 Jul 2025 21:58:16 +0900 Subject: [PATCH 04/21] =?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 | 25 +++++++++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/packages/lib/src/equals/deepEquals.ts b/packages/lib/src/equals/deepEquals.ts index f5f4ebd5..6460381c 100644 --- a/packages/lib/src/equals/deepEquals.ts +++ b/packages/lib/src/equals/deepEquals.ts @@ -1,3 +1,24 @@ -export const deepEquals = (a: unknown, b: unknown) => { - return a === b; +import { isObject } from "./util"; + +/** + * 두 값의 깊은 비교 결과를 반환 + * @param {unknown} a 비교할 첫 번째 값 + * @param {unknown} b 비교할 두 번째 값 + * @returns {boolean} 두 값의 깊은 비교 결과 + */ +export const deepEquals = (a: unknown, b: unknown): boolean => { + // 1. 기본 타입이거나 null인 경우 처리 + if (a === b) return true; + + // 2. 둘 다 객체인 경우: + // - 배열인지 확인 + // - 객체의 키 개수가 다른 경우 처리 + // - 재귀적으로 각 속성에 대해 deepEquals 호출 + if (!isObject(a) || !isObject(b)) return false; + + const keysA = Object.keys(a); + const keysB = Object.keys(b); + if (keysA.length !== keysB.length) return false; + + return keysA.every((key) => deepEquals(a[key], b[key])); }; From 813506517b4a20d129aac9a44010da9c53e0154e Mon Sep 17 00:00:00 2001 From: Legitgoons Date: Wed, 23 Jul 2025 04:07:37 +0900 Subject: [PATCH 05/21] =?UTF-8?q?feat:=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 | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/packages/lib/src/hooks/useRef.ts b/packages/lib/src/hooks/useRef.ts index 285d4ae7..ec043915 100644 --- a/packages/lib/src/hooks/useRef.ts +++ b/packages/lib/src/hooks/useRef.ts @@ -1,4 +1,16 @@ +import { useState } from "react"; + +/** + * 렌더링 사이에 값을 유지하는 가변 ref 객체를 생성 + * - 반환된 ref 객체는 컴포넌트의 전체 생명주기 동안 유지됩니다. + * - ref 객체의 .current 속성을 변경해도 리렌더링이 트리거되지 않습니다. + * - DOM 요소에 접근하거나 이전 상태를 저장하는 등 다양한 용도로 사용될 수 있습니다. + * @param initialValue 초기값 + * @returns ref 객체 + */ + export function useRef(initialValue: T): { current: T } { - // useState를 이용해서 만들어보세요. - return { current: initialValue }; + //useState를 사용하여 초기값을 저장하고, 이후 값을 변경할 때 리렌더링을 트리거하지 않도록 합니다. + const [refObject] = useState(() => ({ current: initialValue })); + return refObject; } From ea12e82639f2877c8219e180a49ebe44a90c170e Mon Sep 17 00:00:00 2001 From: Legitgoons Date: Wed, 23 Jul 2025 04:08:22 +0900 Subject: [PATCH 06/21] =?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 | 24 +++++++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/packages/lib/src/hooks/useMemo.ts b/packages/lib/src/hooks/useMemo.ts index e80692e2..988c15d1 100644 --- a/packages/lib/src/hooks/useMemo.ts +++ b/packages/lib/src/hooks/useMemo.ts @@ -1,8 +1,26 @@ -/* eslint-disable @typescript-eslint/no-unused-vars */ import type { DependencyList } from "react"; import { shallowEquals } from "../equals"; +import { useRef } from "./useRef"; +/** + * useMemo 훅은 의존성이 변경되었을 때만 함수를 실행하여 메모이제이션된 값을 반환합니다. + * @param factory 메모이제이션할 함수 + * @param _deps 의존성 배열 + * @param _equals 비교 함수 + * @returns 메모이제이션된 값 + */ export function useMemo(factory: () => T, _deps: DependencyList, _equals = shallowEquals): T { - // 직접 작성한 useRef를 통해서 만들어보세요. - return factory(); + // 1. 이전 의존성과 결과를 저장할 ref 생성 + const ref = useRef<{ deps: DependencyList; result: T } | null>(null); + if (!ref.current) ref.current = { deps: _deps, result: factory() }; + + // 2. 현재 의존성과 이전 의존성 비교 + if (!_equals(ref.current.deps, _deps)) { + // 3. 의존성이 변경된 경우 factory 함수 실행 및 결과 저장 + ref.current.result = factory(); + ref.current.deps = _deps; + } + + // 4. 메모이제이션된 값 반환 + return ref.current.result; } From c3a7b2c5407d1cf23f48ab7d0fa6e47a50188ef5 Mon Sep 17 00:00:00 2001 From: Legitgoons Date: Wed, 23 Jul 2025 04:09:12 +0900 Subject: [PATCH 07/21] =?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/useDeepMemo.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/packages/lib/src/hooks/useDeepMemo.ts b/packages/lib/src/hooks/useDeepMemo.ts index 3157958c..6cd79d33 100644 --- a/packages/lib/src/hooks/useDeepMemo.ts +++ b/packages/lib/src/hooks/useDeepMemo.ts @@ -3,7 +3,13 @@ import type { DependencyList } from "react"; import { useMemo } from "./useMemo"; import { deepEquals } from "../equals"; +/** + * useDeepMemo 훅은 의존성이 변경되었을 때만 함수를 실행하여 깊은 비교를 수행한 메모이제이션된 값을 반환합니다. + * @param factory 메모이제이션할 함수 + * @param deps 의존성 배열 + * @returns 메모이제이션된 값 + */ export function useDeepMemo(factory: () => T, deps: DependencyList): T { - // 직접 작성한 useMemo를 참고해서 만들어보세요. + // 1. useMemo를 사용하되, 비교 함수로 deepEquals를 사용 return useMemo(factory, deps, deepEquals); } From 99c6b9049b4e37a6efdfdca3b8e01ba47038af21 Mon Sep 17 00:00:00 2001 From: Legitgoons Date: Wed, 23 Jul 2025 04:09:32 +0900 Subject: [PATCH 08/21] =?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 | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/packages/lib/src/hooks/useCallback.ts b/packages/lib/src/hooks/useCallback.ts index 712bc898..cb7a4ce4 100644 --- a/packages/lib/src/hooks/useCallback.ts +++ b/packages/lib/src/hooks/useCallback.ts @@ -1,7 +1,14 @@ -/* eslint-disable @typescript-eslint/no-unused-vars,@typescript-eslint/no-unsafe-function-type */ +/* eslint-disable @typescript-eslint/no-unsafe-function-type */ import type { DependencyList } from "react"; +import { useMemo } from "./useMemo"; +/** + * 콜백 함수를 메모이제이션하는 훅 + * @param factory 메모이제이션할 콜백 함수 + * @param _deps 의존성 배열 + * @returns 메모이제이션된 콜백 함수 + */ export function useCallback(factory: T, _deps: DependencyList) { // 직접 작성한 useMemo를 통해서 만들어보세요. - return factory as T; + return useMemo(() => factory, _deps); } From b03e2c83500e96c729f4d06937316f4fef854b65 Mon Sep 17 00:00:00 2001 From: Legitgoons Date: Wed, 23 Jul 2025 04:09:53 +0900 Subject: [PATCH 09/21] =?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 | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/packages/lib/src/hooks/useAutoCallback.ts b/packages/lib/src/hooks/useAutoCallback.ts index 23100162..ace2ab56 100644 --- a/packages/lib/src/hooks/useAutoCallback.ts +++ b/packages/lib/src/hooks/useAutoCallback.ts @@ -2,6 +2,18 @@ import type { AnyFunction } from "../types"; import { useCallback } from "./useCallback"; import { useRef } from "./useRef"; +/** + * 콜백 함수를 메모이제이션하는 훅 + * @param fn 메모이제이션할 콜백 함수 + * @returns 메모이제이션된 콜백 함수 + */ export const useAutoCallback = (fn: T): T => { - return fn; + // 1. 콜백함수가 참조하는 값은 항상 렌더링 시점에 최신화 되어야한다. ← 이 부분을 어떻게 해결할 수 있을지 고민해보세요! + const fnRef = useRef(fn); + fnRef.current = fn; + + // 2. 대신 항상 동일한 참조를 유지해야 한다 (useCallback 활용) + return useCallback((...args: unknown[]) => { + return fnRef.current(...args); + }, []) as T; }; From 6fad527af4c0eff470a89cefdb6ffdf6c96afeb1 Mon Sep 17 00:00:00 2001 From: Legitgoons Date: Wed, 23 Jul 2025 04:10:09 +0900 Subject: [PATCH 10/21] =?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/hooks/useShallowState.ts | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/packages/lib/src/hooks/useShallowState.ts b/packages/lib/src/hooks/useShallowState.ts index 7b589748..8f8ee391 100644 --- a/packages/lib/src/hooks/useShallowState.ts +++ b/packages/lib/src/hooks/useShallowState.ts @@ -1,7 +1,25 @@ import { useState } from "react"; import { shallowEquals } from "../equals"; +import { useCallback } from "./useCallback"; export const useShallowState = (initialValue: Parameters>[0]) => { // useState를 사용하여 상태를 관리하고, shallowEquals를 사용하여 상태 변경을 감지하는 훅을 구현합니다. - return useState(initialValue); + const [state, setState] = useState(initialValue); + + // 얕은 비교를 수행하는 커스텀 setState 함수 + const setShallowState = useCallback((action: T | ((prevState: T) => T)) => { + setState((prevState) => { + // 최초 호출 시 초기값 반환 + if (prevState === undefined) return initialValue; + + // 함수형 업데이트인지 확인 + const nextValue = typeof action === "function" ? (action as (prev: T) => T)(prevState) : action; + + // shallowEquals로 비교 후 업데이트 결정 + return shallowEquals(prevState, nextValue) ? prevState : nextValue; + }); + }, []); + + // useState와 동일한 API 반환 + return [state, setShallowState]; }; From c6da682b262a8c31b00d6149c54cb699612f3056 Mon Sep 17 00:00:00 2001 From: Legitgoons Date: Wed, 23 Jul 2025 04:10:27 +0900 Subject: [PATCH 11/21] =?UTF-8?q?feat:=20memo=20HOC=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/hocs/memo.ts | 26 ++++++++++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/packages/lib/src/hocs/memo.ts b/packages/lib/src/hocs/memo.ts index 532c3a5c..f604817e 100644 --- a/packages/lib/src/hocs/memo.ts +++ b/packages/lib/src/hocs/memo.ts @@ -1,6 +1,28 @@ -import { type FunctionComponent } from "react"; +import { type FunctionComponent, type JSX } from "react"; import { shallowEquals } from "../equals"; +import { useRef } from "../hooks"; +/** + * memo HOC는 컴포넌트의 props를 얕은 비교하여 불필요한 리렌더링을 방지합니다. + * @param Component 메모이제이션할 컴포넌트 + * @param equals 비교 함수 + * @returns 메모이제이션된 컴포넌트 + */ export function memo

(Component: FunctionComponent

, equals = shallowEquals) { - return Component; + const MemoizedComponent = (props: P) => { + // Hook을 컴포넌트 내부에서 호출 + const prevPropsRef = useRef

(null); + const prevResultRef = useRef(null); + + const shouldUpdate = prevPropsRef.current === null || !equals(prevPropsRef.current, props); + + if (shouldUpdate) { + prevPropsRef.current = props; + prevResultRef.current = Component(props) as JSX.Element; + } + + return prevResultRef.current!; + }; + + return MemoizedComponent; } From 08eed21e6e217cd2595847493f1de9eb11935ab7 Mon Sep 17 00:00:00 2001 From: Legitgoons Date: Wed, 23 Jul 2025 04:10:40 +0900 Subject: [PATCH 12/21] =?UTF-8?q?feat:=20deepMemo=20HOC=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 | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/packages/lib/src/hocs/deepMemo.ts b/packages/lib/src/hocs/deepMemo.ts index 13f7f18e..b5c18a95 100644 --- a/packages/lib/src/hocs/deepMemo.ts +++ b/packages/lib/src/hocs/deepMemo.ts @@ -1,5 +1,14 @@ import type { FunctionComponent } from "react"; +import { memo } from "./memo"; +import { deepEquals } from "../equals"; +/** + * deepMemo HOC는 컴포넌트의 props를 깊은 비교하여 불필요한 리렌더링을 방지합니다. + * @param Component 메모이제이션할 컴포넌트 + * @returns 메모이제이션된 컴포넌트 + */ export function deepMemo

(Component: FunctionComponent

) { - return Component; + // deepEquals 함수를 사용하여 props 비교 + // 앞에서 만든 memo를 사용 + return memo(Component, deepEquals); } From aa560698c5d6a28df6d8758e0278b98cf9ab2b24 Mon Sep 17 00:00:00 2001 From: Legitgoons Date: Fri, 25 Jul 2025 01:08:56 +0900 Subject: [PATCH 13/21] feat: useShallowSelector --- packages/lib/src/hooks/useShallowSelector.ts | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/packages/lib/src/hooks/useShallowSelector.ts b/packages/lib/src/hooks/useShallowSelector.ts index b75ed791..ef8da9e1 100644 --- a/packages/lib/src/hooks/useShallowSelector.ts +++ b/packages/lib/src/hooks/useShallowSelector.ts @@ -3,7 +3,17 @@ import { shallowEquals } from "../equals"; type Selector = (state: T) => S; +/** + * 이전 상태를 저장하고, shallowEquals를 사용하여 상태가 변경되었는지 확인하는 훅 + * @reference https://github.com/pmndrs/zustand/blob/main/src/react/shallow.ts + * @param selector 선택자 함수 + * @returns 상태가 변경되었을 때만 선택자 함수의 결과를 반환 + */ export const useShallowSelector = (selector: Selector) => { - // 이전 상태를 저장하고, shallowEquals를 사용하여 상태가 변경되었는지 확인하는 훅을 구현합니다. - return (state: T): S => selector(state); + const prev = useRef(undefined); + + return (state: T) => { + const next = selector(state); + return shallowEquals(prev.current, next) ? (prev.current as S) : (prev.current = next); + }; }; From 5a3e7a58fc655f13f51835cc6ed821d4636789de Mon Sep 17 00:00:00 2001 From: Legitgoons Date: Fri, 25 Jul 2025 01:09:17 +0900 Subject: [PATCH 14/21] feat: useStore --- packages/lib/src/hooks/useStore.ts | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/packages/lib/src/hooks/useStore.ts b/packages/lib/src/hooks/useStore.ts index acf3ad79..d99dd098 100644 --- a/packages/lib/src/hooks/useStore.ts +++ b/packages/lib/src/hooks/useStore.ts @@ -6,8 +6,18 @@ type Store = ReturnType>; const defaultSelector = (state: T) => state as unknown as S; +/** + * useSyncExternalStore와 useShallowSelector를 사용해서 store의 상태를 구독하고 가져오는 훅 + * @param store 스토어 + * @param selector 선택자 함수 + * @returns 스토어의 상태 + */ export const useStore = (store: Store, selector: (state: T) => S = defaultSelector) => { - // useSyncExternalStore와 useShallowSelector를 사용해서 store의 상태를 구독하고 가져오는 훅을 구현해보세요. const shallowSelector = useShallowSelector(selector); - return shallowSelector(store.getState()); + + return useSyncExternalStore( + store.subscribe, + () => shallowSelector(store.getState()), + () => shallowSelector(store.getState()), + ); }; From 8ff8ed0e7d1038134ab0b3a4f00276bca340b12d Mon Sep 17 00:00:00 2001 From: Legitgoons Date: Fri, 25 Jul 2025 01:09:33 +0900 Subject: [PATCH 15/21] feat: createObserver --- packages/lib/src/createObserver.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/lib/src/createObserver.ts b/packages/lib/src/createObserver.ts index fe97bf6b..259e90dd 100644 --- a/packages/lib/src/createObserver.ts +++ b/packages/lib/src/createObserver.ts @@ -3,9 +3,11 @@ type Listener = () => void; export const createObserver = () => { const listeners = new Set(); - // useSyncExternalStore 에서 활용할 수 있도록 subscribe 함수를 수정합니다. const subscribe = (fn: Listener) => { listeners.add(fn); + return () => { + listeners.delete(fn); + }; }; const unsubscribe = (fn: Listener) => { From 3c7e2f8241f4713a0a83868504812f6882716703 Mon Sep 17 00:00:00 2001 From: Legitgoons Date: Fri, 25 Jul 2025 01:10:09 +0900 Subject: [PATCH 16/21] feat: useStorage --- packages/lib/src/hooks/useStorage.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/lib/src/hooks/useStorage.ts b/packages/lib/src/hooks/useStorage.ts index fdc97a6f..e295ee5a 100644 --- a/packages/lib/src/hooks/useStorage.ts +++ b/packages/lib/src/hooks/useStorage.ts @@ -3,7 +3,11 @@ import type { createStorage } from "../createStorage"; type Storage = ReturnType>; +/** + * useSyncExternalStore를 사용해서 storage의 상태를 구독하고 가져오는 훅 + * @param storage 스토리지 + * @returns 스토리지의 상태 + */ export const useStorage = (storage: Storage) => { - // useSyncExternalStore를 사용해서 storage의 상태를 구독하고 가져오는 훅을 구현해보세요. - return storage.get(); + return useSyncExternalStore(storage.subscribe, storage.get, storage.get); }; From f636215293e58ba7e605cfe47a28a591b5f93b32 Mon Sep 17 00:00:00 2001 From: Legitgoons Date: Fri, 25 Jul 2025 01:10:21 +0900 Subject: [PATCH 17/21] feat: useRouter --- packages/lib/src/hooks/useRouter.ts | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/packages/lib/src/hooks/useRouter.ts b/packages/lib/src/hooks/useRouter.ts index a18ae01f..3afc88da 100644 --- a/packages/lib/src/hooks/useRouter.ts +++ b/packages/lib/src/hooks/useRouter.ts @@ -5,8 +5,17 @@ import { useShallowSelector } from "./useShallowSelector"; const defaultSelector = (state: T) => state as unknown as S; +/** + * useSyncExternalStore를 사용해서 router의 상태를 구독하고 가져오는 훅 + * @param router 라우터 + * @param selector 선택자 함수 + * @returns 라우터의 상태 + */ export const useRouter = , S>(router: T, selector = defaultSelector) => { - // useSyncExternalStore를 사용하여 router의 상태를 구독하고 가져오는 훅을 구현합니다. const shallowSelector = useShallowSelector(selector); - return shallowSelector(router); + return useSyncExternalStore( + router.subscribe, + () => shallowSelector(router), + () => shallowSelector(router), + ); }; From 3f8813c12a244141c42278afabf5817186645405 Mon Sep 17 00:00:00 2001 From: Legitgoons Date: Fri, 25 Jul 2025 01:10:45 +0900 Subject: [PATCH 18/21] =?UTF-8?q?feat:=20ToastProvider=20Context=20?= =?UTF-8?q?=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/components/toast/ToastProvider.tsx | 59 +++++++++++-------- 1 file changed, 34 insertions(+), 25 deletions(-) diff --git a/packages/app/src/components/toast/ToastProvider.tsx b/packages/app/src/components/toast/ToastProvider.tsx index 0aed9451..3b5ecf93 100644 --- a/packages/app/src/components/toast/ToastProvider.tsx +++ b/packages/app/src/components/toast/ToastProvider.tsx @@ -1,52 +1,61 @@ -/* eslint-disable react-refresh/only-export-components */ import { createContext, memo, type PropsWithChildren, useContext, useReducer } from "react"; import { createPortal } from "react-dom"; import { Toast } from "./Toast"; -import { createActions, initialState, toastReducer, type ToastType } from "./toastReducer"; +import { initialState, toastReducer, 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; -const ToastContext = createContext<{ - message: string; - type: ToastType; +const ToastCommandContext = createContext<{ show: ShowToast; hide: Hide; }>({ - ...initialState, show: () => null, hide: () => null, }); +const ToastStateContext = createContext<{ + message: string; + type: ToastType; +}>({ + ...initialState, +}); + const DEFAULT_DELAY = 3000; -const useToastContext = () => useContext(ToastContext); -export const useToastCommand = () => { - const { show, hide } = useToastContext(); - return { show, hide }; -}; -export const useToastState = () => { - const { message, type } = useToastContext(); - return { message, type }; -}; +export const useToastCommand = () => useContext(ToastCommandContext); + +export const useToastState = () => useContext(ToastStateContext); export const ToastProvider = memo(({ children }: PropsWithChildren) => { const [state, dispatch] = useReducer(toastReducer, initialState); - const { show, hide } = createActions(dispatch); const visible = state.message !== ""; - const hideAfter = debounce(hide, DEFAULT_DELAY); + const hide = useCallback(() => { + dispatch({ type: "hide" }); + }, []); + + const hideAfter = useMemo(() => debounce(hide, DEFAULT_DELAY), [hide]); + + const show = useCallback( + (message: string, type: ToastType) => { + dispatch({ type: "show", payload: { message, type } }); + hideAfter(); + }, + [hideAfter], + ); - const showWithHide: ShowToast = (...args) => { - show(...args); - hideAfter(); - }; + const commandValue = useMemo(() => ({ show, hide }), [show, hide]); + const stateValue = useMemo(() => ({ message: state.message, type: state.type }), [state.message, state.type]); return ( - - {children} - {visible && createPortal(, document.body)} - + + + {children} + {visible && createPortal(, document.body)} + + ); }); From 133926f91779ff8c6ef66a63c78d8337ee844854 Mon Sep 17 00:00:00 2001 From: Legitgoons Date: Fri, 25 Jul 2025 01:30:21 +0900 Subject: [PATCH 19/21] feat: 404page --- packages/app/404.html | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 packages/app/404.html diff --git a/packages/app/404.html b/packages/app/404.html new file mode 100644 index 00000000..40c0a77f --- /dev/null +++ b/packages/app/404.html @@ -0,0 +1,26 @@ + + + + + + 상품 쇼핑몰 + + + + + +

+ + + \ No newline at end of file From 35fbac0e14f93a78b389261f790dbf641b27f2c9 Mon Sep 17 00:00:00 2001 From: Legitgoons Date: Fri, 25 Jul 2025 09:15:01 +0900 Subject: [PATCH 20/21] =?UTF-8?q?docs:=20=EC=A3=BC=EC=84=9D=20=EC=9E=91?= =?UTF-8?q?=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/lib/src/createObserver.ts | 4 ++++ packages/lib/src/equals/util.ts | 3 ++- packages/lib/src/hooks/useCallback.ts | 1 - packages/lib/src/hooks/useShallowState.ts | 6 +++++- 4 files changed, 11 insertions(+), 3 deletions(-) diff --git a/packages/lib/src/createObserver.ts b/packages/lib/src/createObserver.ts index 259e90dd..36a31be9 100644 --- a/packages/lib/src/createObserver.ts +++ b/packages/lib/src/createObserver.ts @@ -1,5 +1,9 @@ type Listener = () => void; +/** + * 옵저버 패턴을 사용하여 상태 변경을 관리하는 함수 + * @returns 옵저버 객체 + */ export const createObserver = () => { const listeners = new Set(); diff --git a/packages/lib/src/equals/util.ts b/packages/lib/src/equals/util.ts index d068f21d..2aeb77d6 100644 --- a/packages/lib/src/equals/util.ts +++ b/packages/lib/src/equals/util.ts @@ -1,5 +1,6 @@ /** - * 주어진 값이 객체인지 확인 + * 주어진 값이 객체인지 확인하는 함수 + * @reference https://github.com/toss/es-toolkit/blob/main/src/compat/predicate/isObject.ts * @param {unknown} value - 객체인지 확인할 값 * @returns {value is Record} 객체인지 확인 결과 */ diff --git a/packages/lib/src/hooks/useCallback.ts b/packages/lib/src/hooks/useCallback.ts index cb7a4ce4..5d58131f 100644 --- a/packages/lib/src/hooks/useCallback.ts +++ b/packages/lib/src/hooks/useCallback.ts @@ -9,6 +9,5 @@ import { useMemo } from "./useMemo"; * @returns 메모이제이션된 콜백 함수 */ export function useCallback(factory: T, _deps: DependencyList) { - // 직접 작성한 useMemo를 통해서 만들어보세요. return useMemo(() => factory, _deps); } diff --git a/packages/lib/src/hooks/useShallowState.ts b/packages/lib/src/hooks/useShallowState.ts index 8f8ee391..5df79fc1 100644 --- a/packages/lib/src/hooks/useShallowState.ts +++ b/packages/lib/src/hooks/useShallowState.ts @@ -2,8 +2,12 @@ import { useState } from "react"; import { shallowEquals } from "../equals"; import { useCallback } from "./useCallback"; +/** + * useState를 사용하여 상태를 관리하고, shallowEquals를 사용하여 상태 변경을 감지하는 훅 + * @param initialValue 초기값 + * @returns 상태와 상태 변경 함수 + */ export const useShallowState = (initialValue: Parameters>[0]) => { - // useState를 사용하여 상태를 관리하고, shallowEquals를 사용하여 상태 변경을 감지하는 훅을 구현합니다. const [state, setState] = useState(initialValue); // 얕은 비교를 수행하는 커스텀 setState 함수 From e88d6f9adc9b2f4965423eb658d269819b264183 Mon Sep 17 00:00:00 2001 From: Legitgoons Date: Fri, 25 Jul 2025 10:59:23 +0900 Subject: [PATCH 21/21] =?UTF-8?q?refactor:=20equals/util=EC=97=90=20helper?= =?UTF-8?q?=ED=95=A8=EC=88=98=20compareObject=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 | 8 ++------ packages/lib/src/equals/shallowEquals.ts | 12 +++--------- packages/lib/src/equals/util.ts | 20 ++++++++++++++++++++ 3 files changed, 25 insertions(+), 15 deletions(-) diff --git a/packages/lib/src/equals/deepEquals.ts b/packages/lib/src/equals/deepEquals.ts index 6460381c..00546051 100644 --- a/packages/lib/src/equals/deepEquals.ts +++ b/packages/lib/src/equals/deepEquals.ts @@ -1,4 +1,4 @@ -import { isObject } from "./util"; +import { compareObject, isObject } from "./util"; /** * 두 값의 깊은 비교 결과를 반환 @@ -16,9 +16,5 @@ export const deepEquals = (a: unknown, b: unknown): boolean => { // - 재귀적으로 각 속성에 대해 deepEquals 호출 if (!isObject(a) || !isObject(b)) return false; - const keysA = Object.keys(a); - const keysB = Object.keys(b); - if (keysA.length !== keysB.length) return false; - - return keysA.every((key) => deepEquals(a[key], b[key])); + return compareObject(a, b, deepEquals); }; diff --git a/packages/lib/src/equals/shallowEquals.ts b/packages/lib/src/equals/shallowEquals.ts index ad88728a..6e915660 100644 --- a/packages/lib/src/equals/shallowEquals.ts +++ b/packages/lib/src/equals/shallowEquals.ts @@ -1,4 +1,4 @@ -import { isObject } from "./util"; +import { compareObject, isObject } from "./util"; /** * 두 값의 얕은 비교 결과를 반환 @@ -8,16 +8,10 @@ import { isObject } from "./util"; */ export const shallowEquals = (a: unknown, b: unknown) => { // 1. 두 값이 정확히 같은지 확인 (참조가 같은 경우) - if (a === b) return true; + if (Object.is(a, b)) return true; // 2. 둘 중 하나라도 객체가 아닌 경우 처리 if (!isObject(a) || !isObject(b)) return false; - // 3. 객체의 키 개수가 다른 경우 처리 - const keysA = Object.keys(a); - const keysB = Object.keys(b); - if (keysA.length !== keysB.length) return false; - - // 4. 모든 키에 대해 얕은 비교 수행 - return keysA.every((key) => a[key] === b[key]); + return compareObject(a, b); }; diff --git a/packages/lib/src/equals/util.ts b/packages/lib/src/equals/util.ts index 2aeb77d6..1da12bec 100644 --- a/packages/lib/src/equals/util.ts +++ b/packages/lib/src/equals/util.ts @@ -7,3 +7,23 @@ export function isObject(value?: unknown): value is Record { return value !== null && typeof value === "object"; } + +/** + * 두 객체를 비교하는 함수 + * @param a 비교할 첫 번째 객체 + * @param b 비교할 두 번째 객체 + * @returns 두 객체가 같은지 확인 결과 + */ +export function compareObject( + a: Record, + b: Record, + compareFn: (a: unknown, b: unknown) => boolean = (a, b) => a === b, +): boolean { + // 객체의 키 개수가 다른 경우 처리 + const keysA = Object.keys(a); + const keysB = Object.keys(b); + if (keysA.length !== keysB.length) return false; + + // 모든 키에 대해 비교 수행 + return keysA.every((key) => compareFn(a[key], b[key])); +}