;
+
+ // 3. 키 개수 비교
+ const keysA = Object.keys(recordA);
+ if (keysA.length !== Object.keys(recordB).length) return false;
+
+ // 4. 키별 얕은 비교
+ for (const key of keysA) {
+ if (!(key in recordB)) return false;
+ if (recordA[key] !== recordB[key]) return false;
+ }
+
+ return true;
+}
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);
}
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!;
+ };
}
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;
};
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);
}
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;
}
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;
}
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), // 현재 경로 반환
+ );
};
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],
+ );
};
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;
};
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 함수
+ );
};
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 함수
+ );
};