diff --git a/packages/app/src/components/toast/ToastProvider.tsx b/packages/app/src/components/toast/ToastProvider.tsx index 0aed9451..31713e2c 100644 --- a/packages/app/src/components/toast/ToastProvider.tsx +++ b/packages/app/src/components/toast/ToastProvider.tsx @@ -2,51 +2,52 @@ 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 { createActions, initialState, toastReducer, type ToastState, type ToastType } from "./toastReducer"; import { debounce } from "../../utils"; +import { useAutoCallback, useMemo } from "@hanghae-plus/lib/src/hooks"; 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 = (...args) => { + const showWithHide: ShowToast = useAutoCallback((...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)} + + ); }); 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/equals/deepEquals.ts b/packages/lib/src/equals/deepEquals.ts index f5f4ebd5..71053b99 100644 --- a/packages/lib/src/equals/deepEquals.ts +++ b/packages/lib/src/equals/deepEquals.ts @@ -1,3 +1,22 @@ -export const deepEquals = (a: unknown, b: unknown) => { +import { getObjectKeys, isArray, isObject, isPrimitive } from "./utils"; + +const comparePrimitive = (a: unknown, b: unknown) => Object.is(a, b); + +export const deepEquals = (a: unknown, b: unknown): boolean => { + if (isPrimitive(a) && isPrimitive(b)) { + return comparePrimitive(a, b); + } + + if (isArray(a) && isArray(b)) { + 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); + + return keysA.length === keysB.length && keysA.every((key) => deepEquals(a[key], b[key])); + } + return a === b; }; diff --git a/packages/lib/src/equals/shallowEquals.ts b/packages/lib/src/equals/shallowEquals.ts index 1cf22130..d11f3260 100644 --- a/packages/lib/src/equals/shallowEquals.ts +++ b/packages/lib/src/equals/shallowEquals.ts @@ -1,3 +1,22 @@ +import { getObjectKeys, isArray, isObject, isPrimitive } from "./utils"; + +const comparePrimitive = (a: unknown, b: unknown) => Object.is(a, b); + export const shallowEquals = (a: unknown, b: unknown) => { + if (isPrimitive(a) && isPrimitive(b)) { + return comparePrimitive(a, b); + } + + if (isArray(a) && isArray(b)) { + 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); + + return keysA.length === keysB.length && keysA.every((key) => comparePrimitive(a[key], b[key])); + } + return a === b; }; diff --git a/packages/lib/src/equals/utils.ts b/packages/lib/src/equals/utils.ts new file mode 100644 index 00000000..b03e1b3f --- /dev/null +++ b/packages/lib/src/equals/utils.ts @@ -0,0 +1,14 @@ +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)[]; + +// 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/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; } 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 712bc898..1863b958 100644 --- a/packages/lib/src/hooks/useCallback.ts +++ b/packages/lib/src/hooks/useCallback.ts @@ -1,7 +1,10 @@ -/* eslint-disable @typescript-eslint/no-unused-vars,@typescript-eslint/no-unsafe-function-type */ +/* eslint-disable react-hooks/exhaustive-deps */ import type { DependencyList } from "react"; +import { useMemo } from "./useMemo"; +import type { AnyFunction } from "../types"; -export function useCallback(factory: T, _deps: DependencyList) { - // 직접 작성한 useMemo를 통해서 만들어보세요. - return factory as T; +export function useCallback(factory: T, deps: DependencyList): T { + const memoized = useMemo(() => factory, [...deps]); + + return memoized; } 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 e80692e2..30dc161a 100644 --- a/packages/lib/src/hooks/useMemo.ts +++ b/packages/lib/src/hooks/useMemo.ts @@ -1,8 +1,17 @@ -/* eslint-disable @typescript-eslint/no-unused-vars */ import type { DependencyList } from "react"; +import { useRef } from "./useRef"; import { shallowEquals } from "../equals"; -export function useMemo(factory: () => T, _deps: DependencyList, _equals = shallowEquals): T { - // 직접 작성한 useRef를 통해서 만들어보세요. - return factory(); +export function useMemo(factory: () => T, deps: DependencyList, _equals = shallowEquals): T { + const memoized = useRef(undefined); + const prevDeps = useRef(deps); + const isInitial = useRef(true); + + if (isInitial.current || !_equals(prevDeps.current, deps)) { + isInitial.current = false; + memoized.current = factory(); + prevDeps.current = deps; + } + + return memoized.current!; } 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; } 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; }; 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 7b589748..603e394a 100644 --- a/packages/lib/src/hooks/useShallowState.ts +++ b/packages/lib/src/hooks/useShallowState.ts @@ -1,7 +1,19 @@ -import { useState } from "react"; +/* eslint-disable react-hooks/exhaustive-deps */ +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); +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; }; 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; }; 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; };