,
+ 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]));
+}
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);
}
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;
}
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;
};
diff --git a/packages/lib/src/hooks/useCallback.ts b/packages/lib/src/hooks/useCallback.ts
index 712bc898..5d58131f 100644
--- a/packages/lib/src/hooks/useCallback.ts
+++ b/packages/lib/src/hooks/useCallback.ts
@@ -1,7 +1,13 @@
-/* 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);
}
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);
}
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;
}
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;
}
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),
+ );
};
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);
+ };
};
diff --git a/packages/lib/src/hooks/useShallowState.ts b/packages/lib/src/hooks/useShallowState.ts
index 7b589748..5df79fc1 100644
--- a/packages/lib/src/hooks/useShallowState.ts
+++ b/packages/lib/src/hooks/useShallowState.ts
@@ -1,7 +1,29 @@
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를 사용하여 상태 변경을 감지하는 훅을 구현합니다.
- 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];
};
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);
};
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()),
+ );
};