From 773bec11bce91676a0cc7d9aa34d954e6b598e66 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=ED=98=84=EC=A7=80=EC=88=98?= Date: Tue, 22 Jul 2025 01:35:27 +0900 Subject: [PATCH 01/17] =?UTF-8?q?feat:=20shallowEquals=20=ED=95=A8?= =?UTF-8?q?=EC=88=98=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/shallowEquals.ts | 38 ++++++++++++++++++++++-- 1 file changed, 35 insertions(+), 3 deletions(-) diff --git a/packages/lib/src/equals/shallowEquals.ts b/packages/lib/src/equals/shallowEquals.ts index 1cf22130..ecf69bd2 100644 --- a/packages/lib/src/equals/shallowEquals.ts +++ b/packages/lib/src/equals/shallowEquals.ts @@ -1,3 +1,35 @@ -export const shallowEquals = (a: unknown, b: unknown) => { - return a === b; -}; +/** + * 두 값의 얕은 비교를 수행합니다. (1단계 깊이만 비교) + * + * @param objA - 비교할 첫 번째 값 (모든 타입 허용) + * @param objB - 비교할 두 번째 값 (모든 타입 허용) + * @returns 두 값이 얕은 수준에서 동일하면 true, 다르면 false + */ +export function shallowEquals(objA: unknown, objB: unknown): boolean { + // 1단계: 참조 동일성 체크 + if (objA === objB) return true; + + // 2단계: 타입 체크 + const typeA = typeof objA; + const typeB = typeof objB; + + if (typeA !== typeB) return false; + if (typeA !== "object") return false; // 원시 타입은 종료 + if (objA === null || objB === null) return false; // null 체크 + + // 3단계: 객체로 타입 단언 + const recordA = objA as Record; + const recordB = objB as Record; + + // 4단계: 키 개수 비교 + const keysA = Object.keys(recordA); + if (keysA.length !== Object.keys(recordB).length) return false; + + // 5단계: 키별 얕은 비교 + for (const key of keysA) { + if (!(key in recordB)) return false; // 키가 없으면 종료 + if (recordA[key] !== recordB[key]) return false; // 값이 다르면 종료 + } + + return true; +} From c7061410cd75ab4450bae643bccdd295f4c39dc5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=ED=98=84=EC=A7=80=EC=88=98?= Date: Tue, 22 Jul 2025 13:38:50 +0900 Subject: [PATCH 02/17] =?UTF-8?q?refactor:=20shallowEquals=20null=20?= =?UTF-8?q?=EC=B2=B4=ED=81=AC=20=EC=88=9C=EC=84=9C=20=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 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/lib/src/equals/shallowEquals.ts b/packages/lib/src/equals/shallowEquals.ts index ecf69bd2..62f33f9a 100644 --- a/packages/lib/src/equals/shallowEquals.ts +++ b/packages/lib/src/equals/shallowEquals.ts @@ -14,8 +14,8 @@ export function shallowEquals(objA: unknown, objB: unknown): boolean { const typeB = typeof objB; if (typeA !== typeB) return false; - if (typeA !== "object") return false; // 원시 타입은 종료 if (objA === null || objB === null) return false; // null 체크 + if (typeA !== "object") return false; // 원시 타입은 종료 // 3단계: 객체로 타입 단언 const recordA = objA as Record; From 8d9b1ce4bad7686b3a4b1c7ef1b8bfe4ad6c573d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=ED=98=84=EC=A7=80=EC=88=98?= Date: Tue, 22 Jul 2025 16:03:05 +0900 Subject: [PATCH 03/17] =?UTF-8?q?feat:=20deepEquals=20=ED=95=A8=EC=88=98?= =?UTF-8?q?=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 | 37 ++++++++++++++++++++++-- packages/lib/src/equals/shallowEquals.ts | 22 ++++++-------- 2 files changed, 43 insertions(+), 16 deletions(-) diff --git a/packages/lib/src/equals/deepEquals.ts b/packages/lib/src/equals/deepEquals.ts index f5f4ebd5..022ae35d 100644 --- a/packages/lib/src/equals/deepEquals.ts +++ b/packages/lib/src/equals/deepEquals.ts @@ -1,3 +1,34 @@ -export const deepEquals = (a: unknown, b: unknown) => { - return a === b; -}; +/** + * 두 값의 깊은 비교를 수행합니다. (모든 중첩 레벨에서 재귀적으로 비교) + * + * @param objA - 비교할 첫 번째 값 (모든 타입 허용) + * @param objB - 비교할 두 번째 값 (모든 타입 허용) + * @returns 두 값이 깊은 수준에서 동일하면 true, 다르면 false + */ +export function deepEquals(objA: unknown, objB: unknown): boolean { + // 1. 참조 동일성 체크 + if (objA === objB) return true; + + // 2. 타입 및 null 검증 + if (typeof objA !== typeof objB) return false; + if (objA === null || objB === null) return false; + if (typeof objA !== "object") return false; + + // 3. 배열 타입 일치성 확인 + if (Array.isArray(objA) !== Array.isArray(objB)) return false; + + const recordA = objA as Record; + const recordB = objB as Record; + + // 4. 키 개수 비교 + const keysA = Object.keys(recordA); + if (keysA.length !== Object.keys(recordB).length) return false; + + // 5. 키별 재귀적 깊은 비교 + for (const key of keysA) { + if (!(key in recordB)) return false; + if (!deepEquals(recordA[key], recordB[key])) return false; + } + + return true; +} diff --git a/packages/lib/src/equals/shallowEquals.ts b/packages/lib/src/equals/shallowEquals.ts index 62f33f9a..58558aee 100644 --- a/packages/lib/src/equals/shallowEquals.ts +++ b/packages/lib/src/equals/shallowEquals.ts @@ -6,29 +6,25 @@ * @returns 두 값이 얕은 수준에서 동일하면 true, 다르면 false */ export function shallowEquals(objA: unknown, objB: unknown): boolean { - // 1단계: 참조 동일성 체크 + // 1. 참조 동일성 체크 if (objA === objB) return true; - // 2단계: 타입 체크 - const typeA = typeof objA; - const typeB = typeof objB; + // 2. 타입 및 null 검증 + if (typeof objA !== typeof objB) return false; + if (objA === null || objB === null) return false; + if (typeof objA !== "object") return false; - if (typeA !== typeB) return false; - if (objA === null || objB === null) return false; // null 체크 - if (typeA !== "object") return false; // 원시 타입은 종료 - - // 3단계: 객체로 타입 단언 const recordA = objA as Record; const recordB = objB as Record; - // 4단계: 키 개수 비교 + // 3. 키 개수 비교 const keysA = Object.keys(recordA); if (keysA.length !== Object.keys(recordB).length) return false; - // 5단계: 키별 얕은 비교 + // 4. 키별 얕은 비교 for (const key of keysA) { - if (!(key in recordB)) return false; // 키가 없으면 종료 - if (recordA[key] !== recordB[key]) return false; // 값이 다르면 종료 + if (!(key in recordB)) return false; + if (recordA[key] !== recordB[key]) return false; } return true; From d2385f58ed448f4bcc2f89afb65e61593a81316b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=ED=98=84=EC=A7=80=EC=88=98?= Date: Wed, 23 Jul 2025 00:35:27 +0900 Subject: [PATCH 04/17] =?UTF-8?q?feat:=20useRef=20=ED=9B=85=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/useRef.ts | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/packages/lib/src/hooks/useRef.ts b/packages/lib/src/hooks/useRef.ts index 285d4ae7..3af6d1d0 100644 --- a/packages/lib/src/hooks/useRef.ts +++ b/packages/lib/src/hooks/useRef.ts @@ -1,4 +1,12 @@ +import { useState } from "react"; + +/** + * 렌더링 사이에 값을 유지하는 가변 ref 객체를 생성합니다. + * + * @param initialValue - ref 객체의 초기값 (모든 타입 허용) + * @returns current 속성을 가진 가변 객체 ({ current: T }) + */ export function useRef(initialValue: T): { current: T } { - // useState를 이용해서 만들어보세요. - return { current: initialValue }; + const [ref] = useState(() => ({ current: initialValue })); + return ref; } From 86bab8f6077013b81c21d6dfc0ac834c98ff3afc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=ED=98=84=EC=A7=80=EC=88=98?= Date: Wed, 23 Jul 2025 12:17:43 +0900 Subject: [PATCH 05/17] =?UTF-8?q?feat:=20useMemo=20=ED=9B=85=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/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..b99fecef 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"; -export function useMemo(factory: () => T, _deps: DependencyList, _equals = shallowEquals): T { - // 직접 작성한 useRef를 통해서 만들어보세요. - return factory(); +/** + * 계산 비용이 높은 값을 메모이제이션합니다. + * + * @param factory - 메모이제이션할 값을 계산하는 함수 + * @param deps - 의존성 배열 + * @param equals - 의존성 비교에 사용할 함수 (기본값: shallowEquals) + * @returns 메모이제이션된 값 + */ +export function useMemo(factory: () => T, deps: DependencyList, equals = shallowEquals): T { + // 이전 의존성과 계산 결과를 저장할 ref + const ref = useRef<{ deps: DependencyList; value: T } | null>(null); + + // 첫 호출이거나 의존성이 변경된 경우 재계산 + if (ref.current === null || !equals(ref.current.deps, deps)) { + const value = factory(); + ref.current = { deps, value }; // 새 의존성과 결과 저장 + } + + // 메모이제이션된 값 반환 + return ref.current.value; } From b0e9c04528b557005e5056d5ffaf59cffb9d18fb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=ED=98=84=EC=A7=80=EC=88=98?= Date: Wed, 23 Jul 2025 14:06:37 +0900 Subject: [PATCH 06/17] =?UTF-8?q?feat:=20useCallback=20=ED=9B=85=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/hooks/useCallback.ts | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/packages/lib/src/hooks/useCallback.ts b/packages/lib/src/hooks/useCallback.ts index 712bc898..b4b406ec 100644 --- a/packages/lib/src/hooks/useCallback.ts +++ b/packages/lib/src/hooks/useCallback.ts @@ -1,7 +1,15 @@ /* 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; +/** + * 함수를 메모이제이션합니다. + * + * @param factory - 메모이제이션할 함수 + * @param deps - 의존성 배열 + * @returns 메모이제이션된 함수 + */ +export function useCallback(factory: T, deps: DependencyList): T { + // useMemo를 사용해서 함수 자체를 메모이제이션 + return useMemo(() => factory, deps); } From 1c31c75e2f956c3ae7e27a25bc28c14ec0603443 Mon Sep 17 00:00:00 2001 From: hyunzsu Date: Thu, 24 Jul 2025 03:44:31 +0900 Subject: [PATCH 07/17] =?UTF-8?q?useShallowState=20=ED=9B=85=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 | 38 ++++++++++++++++++++--- 1 file changed, 34 insertions(+), 4 deletions(-) diff --git a/packages/lib/src/hooks/useShallowState.ts b/packages/lib/src/hooks/useShallowState.ts index 7b589748..dce5b665 100644 --- a/packages/lib/src/hooks/useShallowState.ts +++ b/packages/lib/src/hooks/useShallowState.ts @@ -1,7 +1,37 @@ -import { useState } from "react"; +import { useState, useCallback } from "react"; import { shallowEquals } from "../equals"; -export const useShallowState = (initialValue: Parameters>[0]) => { - // useState를 사용하여 상태를 관리하고, shallowEquals를 사용하여 상태 변경을 감지하는 훅을 구현합니다. - return useState(initialValue); +/** + * 얕은 비교를 통해 불필요한 리렌더링을 방지하는 useState의 개선 버전입니다. + * + * 새로운 상태값이 이전 상태값과 얕은 비교(shallowEquals)에서 동일하다면 + * 리렌더링을 발생시키지 않습니다. + * + * @param initialValue - 초기 상태값 또는 초기값을 반환하는 함수 + * @returns [state, setState] 튜플 + * - state: 현재 상태값 + * - setState: 상태 업데이트 함수 (직접 값 또는 함수형 업데이트 지원) + */ +export const useShallowState = (initialValue: T | (() => T)) => { + const [state, setState] = useState(initialValue); + + const setShallowState = useCallback((newValue: T | ((prev: T) => T)) => { + setState((prevState) => { + let nextState: T; + + // newValue 타입에 따라 분기 처리 + if (typeof newValue === "function") { + // 1. 함수형 업데이트: prevState를 인자로 전달하여 새로운 상태 계산 + nextState = (newValue as (prev: T) => T)(prevState); + } else { + // 2. 직접 값: 그대로 사용 + nextState = newValue; + } + + // 얕은 비교로 실제 변경 여부 확인 + return shallowEquals(prevState, nextState) ? prevState : nextState; + }); + }, []); + + return [state, setShallowState] as const; }; From e0728dc8c746938b0ffecfe383813c3fb8cdfcb2 Mon Sep 17 00:00:00 2001 From: hyunzsu Date: Thu, 24 Jul 2025 04:20:04 +0900 Subject: [PATCH 08/17] =?UTF-8?q?useAutoCallback=20=ED=9B=85=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 | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/packages/lib/src/hooks/useAutoCallback.ts b/packages/lib/src/hooks/useAutoCallback.ts index 23100162..98dffd33 100644 --- a/packages/lib/src/hooks/useAutoCallback.ts +++ b/packages/lib/src/hooks/useAutoCallback.ts @@ -2,6 +2,22 @@ import type { AnyFunction } from "../types"; import { useCallback } from "./useCallback"; import { useRef } from "./useRef"; +/** + * 항상 최신 클로저를 참조하면서도 안정된 함수 참조를 제공하는 훅입니다. + * + * @param fn - 메모이제이션할 함수 + * @returns 항상 동일한 참조를 가지지만 최신 클로저를 사용하는 함수 + */ export const useAutoCallback = (fn: T): T => { - return fn; + // 최신 함수를 저장할 ref + const fnRef = useRef(fn); + + // 매 렌더링마다 최신 함수로 업데이트 + fnRef.current = fn; + + // 항상 동일한 참조를 가진 wrapper 함수 반환 + return useCallback((...args: Parameters) => { + // 호출 시점에 최신 함수 실행 + return fnRef.current(...args); + }, []) as T; }; From 24f9de22a6903e2e38be91b7f9897e3f4055faa8 Mon Sep 17 00:00:00 2001 From: hyunzsu Date: Thu, 24 Jul 2025 04:44:01 +0900 Subject: [PATCH 09/17] =?UTF-8?q?memo=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 | 27 ++++++++++++++++++++++++--- 1 file changed, 24 insertions(+), 3 deletions(-) diff --git a/packages/lib/src/hocs/memo.ts b/packages/lib/src/hocs/memo.ts index 532c3a5c..91eb3227 100644 --- a/packages/lib/src/hocs/memo.ts +++ b/packages/lib/src/hocs/memo.ts @@ -1,6 +1,27 @@ -import { type FunctionComponent } from "react"; +import { type FunctionComponent, type ReactNode } from "react"; import { shallowEquals } from "../equals"; +import { useRef } from "../hooks/useRef"; -export function memo

(Component: FunctionComponent

, equals = shallowEquals) { - return Component; +/** + * 컴포넌트의 props를 얕은 비교하여 불필요한 리렌더링을 방지 하는 HOC입니다. + * + * @param Component - 메모이제이션할 함수형 컴포넌트 + * @param equals - props 비교에 사용할 함수 (기본값: shallowEquals) + * @returns 메모이제이션된 컴포넌트 + */ +export function memo

(Component: FunctionComponent

, equals = shallowEquals): FunctionComponent

{ + return function MemoizedComponent(props: P) { + // 이전 props와 렌더링 결과를 저장할 ref들 + const prevPropsRef = useRef

(null); + const memoizedResultRef = useRef | null>(null); + + // 첫 렌더링이거나 props가 변경된 경우에만 새로 렌더링 + if (prevPropsRef.current === null || !equals(prevPropsRef.current, props)) { + memoizedResultRef.current = Component(props); + prevPropsRef.current = props; + } + + // 캐시된 결과 반환 + return memoizedResultRef.current!; + }; } From b7fb439f6c2941948ccaaba38fa2b9eb0628369d Mon Sep 17 00:00:00 2001 From: hyunzsu Date: Thu, 24 Jul 2025 04:45:15 +0900 Subject: [PATCH 10/17] =?UTF-8?q?deepMemo=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/deepMemo.ts | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/packages/lib/src/hocs/deepMemo.ts b/packages/lib/src/hocs/deepMemo.ts index 13f7f18e..1235647c 100644 --- a/packages/lib/src/hocs/deepMemo.ts +++ b/packages/lib/src/hocs/deepMemo.ts @@ -1,5 +1,12 @@ -import type { FunctionComponent } from "react"; +import { memo, type FunctionComponent } from "react"; +import { deepEquals } from "../equals"; +/** + * 컴포넌트의 props를 깊은 비교하여 불필요한 리렌더링을 방지하는 HOC입니다. + * + * @param Component - 메모이제이션할 함수형 컴포넌트 + * @returns 메모이제이션된 컴포넌트 + */ export function deepMemo

(Component: FunctionComponent

) { - return Component; + return memo(Component, deepEquals); } From 4ed7d4504f185d8effc9027e662abeb14b9ed96b Mon Sep 17 00:00:00 2001 From: hyunzsu Date: Thu, 24 Jul 2025 17:25:35 +0900 Subject: [PATCH 11/17] =?UTF-8?q?createObserver=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/lib/src/createObserver.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/lib/src/createObserver.ts b/packages/lib/src/createObserver.ts index fe97bf6b..efa16976 100644 --- a/packages/lib/src/createObserver.ts +++ b/packages/lib/src/createObserver.ts @@ -6,10 +6,8 @@ export const createObserver = () => { // useSyncExternalStore 에서 활용할 수 있도록 subscribe 함수를 수정합니다. const subscribe = (fn: Listener) => { listeners.add(fn); - }; - const unsubscribe = (fn: Listener) => { - listeners.delete(fn); + return () => listeners.delete(fn); }; const notify = () => listeners.forEach((listener) => listener()); From cdb9336de45d2c54feaa5f78e5163fb8031222b2 Mon Sep 17 00:00:00 2001 From: hyunzsu Date: Thu, 24 Jul 2025 17:26:07 +0900 Subject: [PATCH 12/17] =?UTF-8?q?useShallowSelector=20=ED=9B=85=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/hooks/useShallowSelector.ts | 27 +++++++++++++++++--- 1 file changed, 24 insertions(+), 3 deletions(-) diff --git a/packages/lib/src/hooks/useShallowSelector.ts b/packages/lib/src/hooks/useShallowSelector.ts index b75ed791..88788f89 100644 --- a/packages/lib/src/hooks/useShallowSelector.ts +++ b/packages/lib/src/hooks/useShallowSelector.ts @@ -1,9 +1,30 @@ -import { useRef } from "react"; +import { useCallback, useRef } from "react"; import { shallowEquals } from "../equals"; type Selector = (state: T) => S; +/** + * shallow comparison을 사용하여 selector 함수를 메모이제이션하는 Hook입니다. + * + * @template T 입력 상태의 타입 + * @template S selector 결과의 타입 + * @param selector 상태에서 값을 선택하는 함수 + * @returns 메모이제이션된 selector 함수 + */ export const useShallowSelector = (selector: Selector) => { - // 이전 상태를 저장하고, shallowEquals를 사용하여 상태가 변경되었는지 확인하는 훅을 구현합니다. - return (state: T): S => selector(state); + const prevResult = useRef(undefined); + + return useCallback( + (state: T): S => { + const nextResult = selector(state); + + // 첫 호출이거나 내용이 다른 경우에만 업데이트 + if (prevResult.current === undefined || !shallowEquals(prevResult.current, nextResult)) { + prevResult.current = nextResult; + } + + return prevResult.current; + }, + [selector], + ); }; From beea35d35d168e3ba67009bb90d31d7e97485c34 Mon Sep 17 00:00:00 2001 From: hyunzsu Date: Thu, 24 Jul 2025 17:49:55 +0900 Subject: [PATCH 13/17] =?UTF-8?q?useStore=20=ED=9B=85=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/useStore.ts | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/packages/lib/src/hooks/useStore.ts b/packages/lib/src/hooks/useStore.ts index acf3ad79..9e2565ae 100644 --- a/packages/lib/src/hooks/useStore.ts +++ b/packages/lib/src/hooks/useStore.ts @@ -6,8 +6,20 @@ type Store = ReturnType>; const defaultSelector = (state: T) => state as unknown as S; +/** + * store의 상태를 구독하고 선택된 값을 반환하는 Hook입니다. + * + * @template T store 상태의 타입 + * @template S selector 결과의 타입 + * @param store createStore로 생성된 store 인스턴스 + * @param selector 상태에서 값을 선택하는 함수 (기본: 전체 상태) + * @returns 선택된 상태 값 (shallow comparison으로 최적화됨) + */ 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()), // getSnapshot 함수 + ); }; From e18ebf8f948ee2a7aa2047120722a96f76bacf1a Mon Sep 17 00:00:00 2001 From: hyunzsu Date: Thu, 24 Jul 2025 17:50:26 +0900 Subject: [PATCH 14/17] =?UTF-8?q?useStorage=20=ED=9B=85=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/useStorage.ts | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/packages/lib/src/hooks/useStorage.ts b/packages/lib/src/hooks/useStorage.ts index fdc97a6f..4fc39c1a 100644 --- a/packages/lib/src/hooks/useStorage.ts +++ b/packages/lib/src/hooks/useStorage.ts @@ -3,7 +3,16 @@ import type { createStorage } from "../createStorage"; type Storage = ReturnType>; +/** + * storage의 상태를 구독하고 현재 값을 반환하는 Hook입니다. + * + * @template T storage에 저장되는 값의 타입 + * @param storage createStorage로 생성된 storage 인스턴스 + * @returns 현재 storage 값 (T | null) + */ export const useStorage = (storage: Storage) => { - // useSyncExternalStore를 사용해서 storage의 상태를 구독하고 가져오는 훅을 구현해보세요. - return storage.get(); + return useSyncExternalStore( + storage.subscribe, // 구독 함수 + storage.get, // getSnapshot 함수 + ); }; From 19cbe5d17cd6d7c2ef7a72b20921aebc355c2a86 Mon Sep 17 00:00:00 2001 From: hyunzsu Date: Thu, 24 Jul 2025 17:50:53 +0900 Subject: [PATCH 15/17] =?UTF-8?q?useRouter=20=ED=9B=85=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/useRouter.ts | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/packages/lib/src/hooks/useRouter.ts b/packages/lib/src/hooks/useRouter.ts index a18ae01f..835ea179 100644 --- a/packages/lib/src/hooks/useRouter.ts +++ b/packages/lib/src/hooks/useRouter.ts @@ -5,8 +5,20 @@ import { useShallowSelector } from "./useShallowSelector"; const defaultSelector = (state: T) => state as unknown as S; +/** + * router의 상태를 구독하고 선택된 값을 반환하는 Hook입니다. + * + * @template T RouterInstance의 타입 + * @template S selector 결과의 타입 + * @param router Router 인스턴스 + * @param selector router에서 값을 선택하는 함수 (기본: 전체 router) + * @returns 선택된 router 상태 값 + */ export const useRouter = , S>(router: T, selector = defaultSelector) => { - // useSyncExternalStore를 사용하여 router의 상태를 구독하고 가져오는 훅을 구현합니다. const shallowSelector = useShallowSelector(selector); - return shallowSelector(router); + + return useSyncExternalStore( + router.subscribe, // 구독 등록 + () => shallowSelector(router), // 현재 경로 반환 + ); }; From ce3948d349edf67370a514550751e45cc5a8c8f2 Mon Sep 17 00:00:00 2001 From: hyunzsu Date: Thu, 24 Jul 2025 19:02:45 +0900 Subject: [PATCH 16/17] =?UTF-8?q?toast=20=EB=A6=AC=EB=A0=8C=EB=8D=94?= =?UTF-8?q?=EB=A7=81=20=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/components/toast/ToastProvider.tsx | 70 ++++++++++++------- 1 file changed, 46 insertions(+), 24 deletions(-) diff --git a/packages/app/src/components/toast/ToastProvider.tsx b/packages/app/src/components/toast/ToastProvider.tsx index 0aed9451..1b452bf2 100644 --- a/packages/app/src/components/toast/ToastProvider.tsx +++ b/packages/app/src/components/toast/ToastProvider.tsx @@ -1,5 +1,5 @@ /* eslint-disable react-refresh/only-export-components */ -import { createContext, memo, type PropsWithChildren, useContext, useReducer } from "react"; +import { createContext, memo, type PropsWithChildren, useContext, useReducer, useMemo, useCallback } from "react"; import { createPortal } from "react-dom"; import { Toast } from "./Toast"; import { createActions, initialState, toastReducer, type ToastType } from "./toastReducer"; @@ -8,45 +8,67 @@ import { debounce } from "../../utils"; type ShowToast = (message: string, type: ToastType) => void; type Hide = () => void; -const ToastContext = createContext<{ - message: string; - type: ToastType; +// Command Context +const ToastCommandContext = createContext<{ show: ShowToast; hide: Hide; }>({ - ...initialState, show: () => null, hide: () => null, }); +// State Context +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 actions = useMemo(() => createActions(dispatch), []); + const { show, hide } = actions; + + const hideAfter = useMemo(() => debounce(hide, DEFAULT_DELAY), [hide]); + + const showWithHide = useCallback( + (...args) => { + show(...args); + hideAfter(); + }, + [show, hideAfter], + ); - const showWithHide: ShowToast = (...args) => { - show(...args); - hideAfter(); - }; + const commandValue = useMemo( + () => ({ + show: showWithHide, + hide, + }), + [showWithHide, 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 c5ca529ea0fa476f3cd419ddccf9d0b30c14c39a Mon Sep 17 00:00:00 2001 From: hyunzsu Date: Fri, 25 Jul 2025 00:18:06 +0900 Subject: [PATCH 17/17] =?UTF-8?q?404.html=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- 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..a52cb5db --- /dev/null +++ b/packages/app/404.html @@ -0,0 +1,26 @@ + + + + + + 상품 쇼핑몰 + + + + + +

+ + +