| null;
}
export interface EffectHook {
kind: HookType["EFFECT"];
- deps: unknown[] | null;
+ deps: unknown[] | null | undefined;
cleanup: (() => void) | null;
effect: () => (() => void) | void;
}
diff --git a/packages/react/src/hocs/memo.ts b/packages/react/src/hocs/memo.ts
index 24569ce4..00b25990 100644
--- a/packages/react/src/hocs/memo.ts
+++ b/packages/react/src/hocs/memo.ts
@@ -1,7 +1,15 @@
-import { useRef } from "../hooks";
import { type FunctionComponent, type VNode } from "../core";
import { shallowEquals } from "../utils";
+type MemoConfig = {
+ equals: (prev: P, next: P) => boolean;
+ render: FunctionComponent
;
+};
+
+type MemoizedComponentType
= FunctionComponent
& {
+ __memoConfig?: MemoConfig
;
+};
+
/**
* 컴포넌트의 props가 변경되지 않았을 경우, 마지막 렌더링 결과를 재사용하여
* 리렌더링을 방지하는 고차 컴포넌트(HOC)입니다.
@@ -11,11 +19,11 @@ import { shallowEquals } from "../utils";
* @returns 메모이제이션이 적용된 새로운 컴포넌트
*/
export function memo
(Component: FunctionComponent
, equals = shallowEquals) {
- const MemoizedComponent: FunctionComponent
= (props) => {
- // 여기를 구현하세요.
- // useRef를 사용하여 이전 props와 렌더링 결과를 저장해야 합니다.
- // equals 함수로 이전 props와 현재 props를 비교하여 렌더링 여부를 결정합니다.
- return Component(props);
+ const MemoizedComponent: MemoizedComponentType
= (props) => Component(props) as VNode;
+
+ MemoizedComponent.__memoConfig = {
+ equals,
+ render: Component,
};
MemoizedComponent.displayName = `Memo(${Component.displayName || Component.name})`;
diff --git a/packages/react/src/hooks/useAutoCallback.ts b/packages/react/src/hooks/useAutoCallback.ts
index 19d48f72..482387cb 100644
--- a/packages/react/src/hooks/useAutoCallback.ts
+++ b/packages/react/src/hooks/useAutoCallback.ts
@@ -9,7 +9,13 @@ import { useRef } from "./useRef";
* @returns 참조가 안정적인 콜백 함수
*/
export const useAutoCallback = (fn: T): T => {
- // 여기를 구현하세요.
- // useRef와 useCallback을 조합하여 구현해야 합니다.
- return fn;
+ const fnRef = useRef(fn);
+ fnRef.current = fn;
+
+ return useCallback(
+ ((...args: Parameters) => {
+ return fnRef.current(...args);
+ }) as T,
+ [],
+ );
};
diff --git a/packages/react/src/hooks/useCallback.ts b/packages/react/src/hooks/useCallback.ts
index c0043993..f9ee06d0 100644
--- a/packages/react/src/hooks/useCallback.ts
+++ b/packages/react/src/hooks/useCallback.ts
@@ -1,3 +1,4 @@
+import type { AnyFunction } from "../types";
import { DependencyList } from "./types";
import { useMemo } from "./useMemo";
@@ -9,8 +10,6 @@ import { useMemo } from "./useMemo";
* @param deps - 의존성 배열
* @returns 메모이제이션된 콜백 함수
*/
-export const useCallback = any>(callback: T, deps: DependencyList): T => {
- // 여기를 구현하세요.
- // useMemo를 사용하여 구현할 수 있습니다.
- return callback;
+export const useCallback = (callback: T, deps: DependencyList): T => {
+ return useMemo(() => callback, deps);
};
diff --git a/packages/react/src/hooks/useDeepMemo.ts b/packages/react/src/hooks/useDeepMemo.ts
index f968d05a..4bd8ae36 100644
--- a/packages/react/src/hooks/useDeepMemo.ts
+++ b/packages/react/src/hooks/useDeepMemo.ts
@@ -6,7 +6,5 @@ import { useMemo } from "./useMemo";
* `deepEquals`를 사용하여 의존성을 깊게 비교하는 `useMemo` 훅입니다.
*/
export const useDeepMemo = (factory: () => T, deps: DependencyList): T => {
- // 여기를 구현하세요.
- // useMemo와 deepEquals 함수를 사용해야 합니다.
- return factory();
+ return useMemo(factory, deps, deepEquals);
};
diff --git a/packages/react/src/hooks/useMemo.ts b/packages/react/src/hooks/useMemo.ts
index c275d0e1..ab8a9b6c 100644
--- a/packages/react/src/hooks/useMemo.ts
+++ b/packages/react/src/hooks/useMemo.ts
@@ -2,18 +2,47 @@ import { DependencyList } from "./types";
import { useRef } from "./useRef";
import { shallowEquals } from "../utils";
+interface MemoCache {
+ deps: DependencyList;
+ value: T;
+}
+
/**
* 계산 비용이 큰 함수의 결과를 메모이제이션합니다.
- * 의존성 배열(deps)의 값이 변경될 때만 함수를 다시 실행합니다.
+ * 의존성 배열의 값이 변경될 때만 factory 함수를 다시 실행하여 값을 재계산합니다.
+ *
+ * @template T - 메모이제이션할 값의 타입
+ * @param {() => T} factory - 값을 생성하는 팩토리 함수. 렌더링 중에 실행됩니다.
+ * @param {DependencyList} deps - 의존성 배열. 이 배열의 요소가 변경되면 factory가 재실행됩니다.
+ * @param {(prev: DependencyList, next: DependencyList) => boolean} [equals=shallowEquals]
+ * @returns {T} 메모이제이션된 값 (최신 계산값 또는 캐시된 값)
*
- * @param factory - 메모이제이션할 값을 생성하는 함수
- * @param deps - 의존성 배열
- * @param equals - 의존성을 비교할 함수 (기본값: shallowEquals)
- * @returns 메모이제이션된 값
+ * @example
+ * const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);
*/
export const useMemo = (factory: () => T, deps: DependencyList, equals = shallowEquals): T => {
- // 여기를 구현하세요.
- // useRef를 사용하여 이전 의존성 배열과 계산된 값을 저장해야 합니다.
- // equals 함수로 의존성을 비교하여 factory 함수를 재실행할지 결정합니다.
- return factory();
+ // 1. 이전 의존성과 값을 저장할 Ref 생성 (초기값 null)
+ const ref = useRef | null>(null);
+
+ const currentCache = ref.current;
+
+ // 2. 재계산 조건 확인
+ // - 첫 렌더링이거나
+ // - 의존성이 변경되었을 경우
+ if (!currentCache || !equals(currentCache.deps, deps)) {
+ // 3. 값 재계산 및 캐시 갱신
+ // factory 함수를 실행하여 새로운 값을 얻고, 현재 의존성과 함께 저장합니다.
+ // useRef의 값을 바꿔도 리렌더링이 발생하지 않으므로 안전합니다.
+ const newValue = factory();
+
+ ref.current = {
+ deps,
+ value: newValue,
+ };
+
+ return newValue;
+ }
+
+ // 4. 캐시된 값 반환 (재계산 건너뜀)
+ return currentCache.value;
};
diff --git a/packages/react/src/hooks/useRef.ts b/packages/react/src/hooks/useRef.ts
index d5521ca1..d27856ec 100644
--- a/packages/react/src/hooks/useRef.ts
+++ b/packages/react/src/hooks/useRef.ts
@@ -1,4 +1,8 @@
-import { useState } from "../core";
+import { context } from "../core/context";
+interface RefHook {
+ kind: "ref";
+ value: { current: T };
+}
/**
* 리렌더링되어도 변경되지 않는 참조(reference) 객체를 반환합니다.
@@ -7,8 +11,36 @@ import { useState } from "../core";
* @param initialValue - ref 객체의 초기 .current 값
* @returns `{ current: T }` 형태의 ref 객체
*/
+//state 말고 ref 객체를 따로 만들면 더 가벼울려나 ? 일단 통과하고 해보자
export const useRef = (initialValue: T): { current: T } => {
- // 여기를 구현하세요.
- // useState를 사용하여 ref 객체를 한 번만 생성하도록 해야 합니다.
- return { current: initialValue };
+ const path = context.hooks.currentPath;
+ const cursor = context.hooks.currentCursor;
+ const hooks = context.hooks.currentHooks;
+
+ // 현재 커서 위치의 훅을 가져옵니다. (타입 캐스팅)
+ let hook = hooks[cursor] as RefHook | undefined;
+
+ // 1. 초기화: 첫 렌더링 시에만 객체를 생성합니다.
+ if (!hook) {
+ // { current: value } 객체 자체를 생성하여 저장합니다.
+ const value = { current: initialValue };
+ hook = { kind: "ref", value };
+ hooks.push(hook);
+ } else if (hook.kind !== "ref") {
+ console.error(
+ `훅 타입 불일치가 감지되었습니다. (경로: "${path}", 커서: ${cursor})\n` +
+ `예상 타입: "ref", 실제 타입: "${hook.kind}"\n` +
+ `이 오류는 주로 조건문, 반복문, 또는 중첩 함수 내부에서 훅을 호출했을 때 발생합니다.`,
+ );
+ const value = { current: initialValue };
+ hook = { kind: "ref", value };
+ hooks[cursor] = hook;
+ }
+
+ // 2. 커서 이동
+ context.hooks.cursor.set(path, cursor + 1);
+
+ // 3. 저장된 객체 자체를 반환합니다.
+ // 객체 참조(Reference)가 유지되므로, 컴포넌트가 리렌더링되어도 이 객체는 동일합니다.
+ return hook.value;
};
diff --git a/packages/react/src/utils/enqueue.ts b/packages/react/src/utils/enqueue.ts
index a4957d53..4c48ff0d 100644
--- a/packages/react/src/utils/enqueue.ts
+++ b/packages/react/src/utils/enqueue.ts
@@ -1,19 +1,34 @@
+// utils/enqueue.ts
import type { AnyFunction } from "../types";
-/**
- * 작업을 마이크로태스크 큐에 추가하여 비동기적으로 실행합니다.
- * 브라우저의 `queueMicrotask` 또는 `Promise.resolve().then()`을 사용합니다.
- */
export const enqueue = (callback: () => void) => {
- // 여기를 구현하세요.
+ if (typeof queueMicrotask === "function") {
+ queueMicrotask(callback);
+ } else {
+ Promise.resolve()
+ .then(callback)
+ .catch((err) => {
+ console.error(err);
+ });
+ }
};
-/**
- * 함수가 여러 번 호출되더라도 실제 실행은 한 번만 스케줄링되도록 보장하는 고차 함수입니다.
- * 렌더링이나 이펙트 실행과 같은 작업의 중복을 방지하는 데 사용됩니다.
- */
export const withEnqueue = (fn: AnyFunction) => {
- // 여기를 구현하세요.
- // scheduled 플래그를 사용하여 fn이 한 번만 예약되도록 구현합니다.
- return () => {};
+ let scheduled = false;
+
+ const run = () => {
+ try {
+ fn();
+ } finally {
+ scheduled = false;
+ }
+ };
+
+ return () => {
+ if (scheduled) {
+ return;
+ }
+ scheduled = true;
+ enqueue(run);
+ };
};
diff --git a/packages/react/src/utils/equals.ts b/packages/react/src/utils/equals.ts
index 31ec4ba5..1a58d93d 100644
--- a/packages/react/src/utils/equals.ts
+++ b/packages/react/src/utils/equals.ts
@@ -3,9 +3,57 @@
* 객체와 배열은 1단계 깊이까지만 비교합니다.
*/
export const shallowEquals = (a: unknown, b: unknown): boolean => {
- // 여기를 구현하세요.
- // Object.is(), Array.isArray(), Object.keys() 등을 활용하여 1단계 깊이의 비교를 구현합니다.
- return a === b;
+ // 1. 참조가 같으면(NaN, +0, -0 포함) 즉시 true
+ if (Object.is(a, b)) {
+ return true;
+ }
+
+ // 2. 둘 중 하나라도 객체가 아니거나 null이면, 참조가 달랐으므로 false
+ if (typeof a !== "object" || a === null || typeof b !== "object" || b === null) {
+ return false;
+ }
+
+ // 3. 배열 비교
+ if (Array.isArray(a) && Array.isArray(b)) {
+ // 길이가 다르면 false
+ if (a.length !== b.length) {
+ return false;
+ }
+ // 각 요소를 Object.is로 1단계 비교
+ for (let i = 0; i < a.length; i++) {
+ if (!Object.is(a[i], b[i])) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ // 4. 배열 vs 객체 비교 방지
+ if (Array.isArray(a) !== Array.isArray(b)) {
+ return false;
+ }
+
+ // 5. 객체 비교
+ const keysA = Object.keys(a);
+ const keysB = Object.keys(b);
+
+ // 키의 개수가 다르면 false
+ if (keysA.length !== keysB.length) {
+ return false;
+ }
+
+ // 각 키의 값을 Object.is로 1단계 비교
+ for (const key of keysA) {
+ // b에 해당 키가 없거나 값이 다르면 false
+ if (
+ !Object.prototype.hasOwnProperty.call(b, key) ||
+ !Object.is(a[key as keyof typeof a], b[key as keyof typeof b])
+ ) {
+ return false;
+ }
+ }
+
+ return true;
};
/**
@@ -13,7 +61,52 @@ export const shallowEquals = (a: unknown, b: unknown): boolean => {
* 객체와 배열의 모든 중첩된 속성을 재귀적으로 비교합니다.
*/
export const deepEquals = (a: unknown, b: unknown): boolean => {
- // 여기를 구현하세요.
- // 재귀적으로 deepEquals를 호출하여 중첩된 구조를 비교해야 합니다.
- return a === b;
+ // 1. 참조가 같으면 true
+ if (Object.is(a, b)) {
+ return true;
+ }
+
+ // 2. 둘 중 하나라도 객체가 아니거나 null이면 false
+ if (typeof a !== "object" || a === null || typeof b !== "object" || b === null) {
+ return false;
+ }
+
+ // 3. 배열 비교 (재귀)
+ if (Array.isArray(a) && Array.isArray(b)) {
+ if (a.length !== b.length) {
+ return false;
+ }
+ // 각 요소를 재귀적으로 deepEquals 호출
+ for (let i = 0; i < a.length; i++) {
+ if (!deepEquals(a[i], b[i])) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ // 4. 배열 vs 객체 비교 방지
+ if (Array.isArray(a) !== Array.isArray(b)) {
+ return false;
+ }
+
+ // 5. 객체 비교 (재귀)
+ const keysA = Object.keys(a);
+ const keysB = Object.keys(b);
+
+ if (keysA.length !== keysB.length) {
+ return false;
+ }
+
+ for (const key of keysA) {
+ // b에 해당 키가 없거나, 값이 재귀적으로 다르면 false
+ if (
+ !Object.prototype.hasOwnProperty.call(b, key) ||
+ !deepEquals(a[key as keyof typeof a], b[key as keyof typeof b])
+ ) {
+ return false;
+ }
+ }
+
+ return true;
};
diff --git a/packages/react/src/utils/validators.ts b/packages/react/src/utils/validators.ts
index da81b3dd..30557966 100644
--- a/packages/react/src/utils/validators.ts
+++ b/packages/react/src/utils/validators.ts
@@ -1,11 +1,9 @@
/**
* VNode가 렌더링되지 않아야 하는 값인지 확인합니다.
* (예: null, undefined, boolean)
- *
- * @param value - 확인할 값
- * @returns 렌더링되지 않아야 하면 true, 그렇지 않으면 false
*/
export const isEmptyValue = (value: unknown): boolean => {
- // 여기를 구현하세요.
- return false;
+ // 1. value == null 은 null과 undefined를 모두 체크합니다.
+ // 2. typeof value === 'boolean' 은 true와 false를 모두 체크합니다.
+ return value === undefined || value === null || typeof value === "boolean";
};