From b4ff75d8ef0fc122366585b4badc8e43d2f897c7 Mon Sep 17 00:00:00 2001 From: chan9yu Date: Sun, 20 Jul 2025 14:20:09 +0900 Subject: [PATCH 01/23] =?UTF-8?q?chore:=20=EB=B0=B0=ED=8F=AC=EB=A5=BC=20?= =?UTF-8?q?=EC=9C=84=ED=95=9C=20=EC=84=A4=EC=A0=95=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - spa 우회하기 위한 404.html 추가 - deploy workflow 추가 --- .github/workflows/deploy.yml | 40 ++++++++++++++++++++++++ packages/app/404.html | 18 +++++++++++ packages/app/index.html | 60 +++++++++++++++++++++++------------- 3 files changed, 97 insertions(+), 21 deletions(-) create mode 100644 .github/workflows/deploy.yml create mode 100644 packages/app/404.html diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml new file mode 100644 index 00000000..34b83811 --- /dev/null +++ b/.github/workflows/deploy.yml @@ -0,0 +1,40 @@ +name: Deploy to GitHub Pages + +on: + push: + branches: [main] + pull_request: + branches: [main] + +jobs: + deploy: + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version-file: ".nvmrc" + + - name: Setup pnpm + uses: pnpm/action-setup@v2 + with: + version: latest + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Build + run: pnpm build + env: + NODE_ENV: production + + - name: Deploy to GitHub Pages + uses: peaceiris/actions-gh-pages@v3 + if: github.ref == 'refs/heads/main' + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + publish_dir: ./packages/app/dist \ No newline at end of file diff --git a/packages/app/404.html b/packages/app/404.html new file mode 100644 index 00000000..59a7be40 --- /dev/null +++ b/packages/app/404.html @@ -0,0 +1,18 @@ + \ No newline at end of file diff --git a/packages/app/index.html b/packages/app/index.html index 7e663448..395f12c8 100644 --- a/packages/app/index.html +++ b/packages/app/index.html @@ -1,26 +1,44 @@ - - - - 상품 쇼핑몰 - - - + - - -
- - + }; + + + + + +
+ + From 6f42747ad68783002fbe3d0bdd2c183617af8ea9 Mon Sep 17 00:00:00 2001 From: chan9yu Date: Sun, 20 Jul 2025 16:59:57 +0900 Subject: [PATCH 02/23] =?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 | 57 +++++++++++++++++++++++- 1 file changed, 56 insertions(+), 1 deletion(-) diff --git a/packages/lib/src/equals/shallowEquals.ts b/packages/lib/src/equals/shallowEquals.ts index 1cf22130..8e800133 100644 --- a/packages/lib/src/equals/shallowEquals.ts +++ b/packages/lib/src/equals/shallowEquals.ts @@ -1,3 +1,58 @@ +/** + * 두 값의 얕은 비교를 수행 + * + * - 기본 타입 값들을 정확히 비교해야 한다 + * - 배열을 얕게 비교해야 한다 + * - 중첩된 구조를 깊게 비교하지 않아야 한다 + */ export const shallowEquals = (a: unknown, b: unknown) => { - return a === b; + // 두 값이 정확히 같은지 확인 (참조가 같은 경우) + if (Object.is(a, b)) return true; + + // 둘 다 객체가 아니면 false + if (typeof a !== "object" || typeof b !== "object" || a === null || b === null) return false; + + const isArrayA = Array.isArray(a); + const isArrayB = Array.isArray(b); + + // 서로다른 타입을 받을 경우 false (ex: a: [], b: {} 인 경우 false) + if (isArrayA !== isArrayB) return false; + + // 둘 다 배열이면 배열 비교 + if (isArrayA && isArrayB) { + if (a.length !== b.length) { + return false; + } + + for (let i = 0; i < a.length; i++) { + if (!Object.is(a[i], b[i])) { + return false; + } + } + + return true; + } + + // 둘 다 객체면 객체 비교 + const aObj = a as Record; + const bObj = b as Record; + + const aKeys = Object.keys(aObj); + const bKeys = Object.keys(bObj); + + if (aKeys.length !== bKeys.length) { + return false; + } + + for (const key of aKeys) { + if (!(key in bObj)) { + return false; + } + + if (!Object.is(aObj[key], bObj[key])) { + return false; + } + } + + return true; }; From 3e8210378454ee6db200f64b764ccad6984f75c7 Mon Sep 17 00:00:00 2001 From: chan9yu Date: Sun, 20 Jul 2025 17:02:47 +0900 Subject: [PATCH 03/23] =?UTF-8?q?refactor:=20shallowEquals=20=ED=95=A8?= =?UTF-8?q?=EC=88=98=EB=A5=BC=20=EC=84=A0=EC=96=B8=EC=A0=81=20=EB=B0=A9?= =?UTF-8?q?=EC=8B=9D=EC=9C=BC=EB=A1=9C=20=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/lib/src/equals/shallowEquals.ts | 65 ++++++++++-------------- 1 file changed, 26 insertions(+), 39 deletions(-) diff --git a/packages/lib/src/equals/shallowEquals.ts b/packages/lib/src/equals/shallowEquals.ts index 8e800133..594920dc 100644 --- a/packages/lib/src/equals/shallowEquals.ts +++ b/packages/lib/src/equals/shallowEquals.ts @@ -1,3 +1,25 @@ +const isArray = (value: unknown) => { + return Array.isArray(value); +}; + +const isObject = (value: unknown) => { + return typeof value === "object" && value !== null; +}; + +const compareArrays = (a: unknown[], b: unknown[]) => { + return a.length === b.length && a.every((item, index) => Object.is(item, b[index])); +}; + +const compareObjects = (a: object, b: object) => { + const aObj = a as Record; + const bObj = b as Record; + + const keysA = Object.keys(a); + const keysB = Object.keys(b); + + return keysA.length === keysB.length && keysA.every((key) => key in bObj && Object.is(aObj[key], bObj[key])); +}; + /** * 두 값의 얕은 비교를 수행 * @@ -10,49 +32,14 @@ export const shallowEquals = (a: unknown, b: unknown) => { if (Object.is(a, b)) return true; // 둘 다 객체가 아니면 false - if (typeof a !== "object" || typeof b !== "object" || a === null || b === null) return false; - - const isArrayA = Array.isArray(a); - const isArrayB = Array.isArray(b); + if (!isObject(a) || !isObject(b)) return false; // 서로다른 타입을 받을 경우 false (ex: a: [], b: {} 인 경우 false) - if (isArrayA !== isArrayB) return false; + if (isArray(a) !== isArray(b)) return false; // 둘 다 배열이면 배열 비교 - if (isArrayA && isArrayB) { - if (a.length !== b.length) { - return false; - } - - for (let i = 0; i < a.length; i++) { - if (!Object.is(a[i], b[i])) { - return false; - } - } - - return true; - } + if (isArray(a) && isArray(b)) return compareArrays(a, b); // 둘 다 객체면 객체 비교 - const aObj = a as Record; - const bObj = b as Record; - - const aKeys = Object.keys(aObj); - const bKeys = Object.keys(bObj); - - if (aKeys.length !== bKeys.length) { - return false; - } - - for (const key of aKeys) { - if (!(key in bObj)) { - return false; - } - - if (!Object.is(aObj[key], bObj[key])) { - return false; - } - } - - return true; + return compareObjects(a, b); }; From 2d9b76275808a8907076871f8ac89c8a67632992 Mon Sep 17 00:00:00 2001 From: chan9yu Date: Sun, 20 Jul 2025 17:46:53 +0900 Subject: [PATCH 04/23] =?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 | 47 +++++++++++++++++++++++++-- 1 file changed, 45 insertions(+), 2 deletions(-) diff --git a/packages/lib/src/equals/deepEquals.ts b/packages/lib/src/equals/deepEquals.ts index f5f4ebd5..6398ab01 100644 --- a/packages/lib/src/equals/deepEquals.ts +++ b/packages/lib/src/equals/deepEquals.ts @@ -1,3 +1,46 @@ -export const deepEquals = (a: unknown, b: unknown) => { - return a === b; +const isArray = (value: unknown) => { + return Array.isArray(value); +}; + +const isObject = (value: unknown) => { + return typeof value === "object" && value !== null; +}; + +const compareArraysDeep = (a: unknown[], b: unknown[]) => { + return a.length === b.length && a.every((item, index) => deepEquals(item, b[index])); +}; + +const compareObjectsDeep = (a: object, b: object) => { + const aObj = a as Record; + const bObj = b as Record; + + const keysA = Object.keys(aObj); + const keysB = Object.keys(bObj); + + return keysA.length === keysB.length && keysA.every((key) => key in bObj && deepEquals(aObj[key], bObj[key])); +}; + +/** + * 두 값의 깊은 비교를 수행 (리팩토링 버전) + * + * - 기본 타입 값들을 정확히 비교해야 한다 + * - 배열을 정확히 비교해야 한다 + * - 객체를 정확히 비교해야 한다 + * - 중첩된 구조를 정확히 비교해야 한다 + */ +export const deepEquals = (a: unknown, b: unknown): boolean => { + // 두 값이 정확히 같은지 확인 (참조가 같은 경우) + if (Object.is(a, b)) return true; + + // 둘 다 객체가 아니면 false + if (!isObject(a) || !isObject(b)) return false; + + // 서로 다른 타입을 받을 경우 false (ex: a: [], b: {} 인 경우 false) + if (isArray(a) !== isArray(b)) return false; + + // 둘 다 배열이면 배열 비교 + if (isArray(a) && isArray(b)) return compareArraysDeep(a, b); + + // 둘 다 객체면 객체 비교 + return compareObjectsDeep(a, b); }; From 968fb25a1600d58e5ca55d45cea968b4d71b9ebc Mon Sep 17 00:00:00 2001 From: chan9yu Date: Sun, 20 Jul 2025 17:56:32 +0900 Subject: [PATCH 05/23] =?UTF-8?q?refactor:=20=EA=B3=B5=ED=86=B5=20?= =?UTF-8?q?=ED=97=AC=ED=8D=BC=EB=A1=9C=EC=A7=81=20=EC=9C=A0=ED=8B=B8=20?= =?UTF-8?q?=EB=94=94=EB=A0=89=ED=86=A0=EB=A6=AC=20=EB=B6=84=EB=A6=AC,=20?= =?UTF-8?q?=EC=9D=B4=EA=B4=80=EC=9E=91=EC=97=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/lib/src/equals/deepEquals.ts | 28 ++++-------------------- packages/lib/src/equals/shallowEquals.ts | 22 +------------------ packages/lib/src/utils/helper.ts | 21 ++++++++++++++++++ packages/lib/src/utils/index.ts | 1 + 4 files changed, 27 insertions(+), 45 deletions(-) create mode 100644 packages/lib/src/utils/helper.ts create mode 100644 packages/lib/src/utils/index.ts diff --git a/packages/lib/src/equals/deepEquals.ts b/packages/lib/src/equals/deepEquals.ts index 6398ab01..452918d0 100644 --- a/packages/lib/src/equals/deepEquals.ts +++ b/packages/lib/src/equals/deepEquals.ts @@ -1,24 +1,4 @@ -const isArray = (value: unknown) => { - return Array.isArray(value); -}; - -const isObject = (value: unknown) => { - return typeof value === "object" && value !== null; -}; - -const compareArraysDeep = (a: unknown[], b: unknown[]) => { - return a.length === b.length && a.every((item, index) => deepEquals(item, b[index])); -}; - -const compareObjectsDeep = (a: object, b: object) => { - const aObj = a as Record; - const bObj = b as Record; - - const keysA = Object.keys(aObj); - const keysB = Object.keys(bObj); - - return keysA.length === keysB.length && keysA.every((key) => key in bObj && deepEquals(aObj[key], bObj[key])); -}; +import { compareArrays, compareObjects, isArray, isObject } from "../utils"; /** * 두 값의 깊은 비교를 수행 (리팩토링 버전) @@ -28,7 +8,7 @@ const compareObjectsDeep = (a: object, b: object) => { * - 객체를 정확히 비교해야 한다 * - 중첩된 구조를 정확히 비교해야 한다 */ -export const deepEquals = (a: unknown, b: unknown): boolean => { +export const deepEquals = (a: unknown, b: unknown) => { // 두 값이 정확히 같은지 확인 (참조가 같은 경우) if (Object.is(a, b)) return true; @@ -39,8 +19,8 @@ export const deepEquals = (a: unknown, b: unknown): boolean => { if (isArray(a) !== isArray(b)) return false; // 둘 다 배열이면 배열 비교 - if (isArray(a) && isArray(b)) return compareArraysDeep(a, b); + if (isArray(a) && isArray(b)) return compareArrays(a, b, deepEquals); // 둘 다 객체면 객체 비교 - return compareObjectsDeep(a, b); + return compareObjects(a, b, deepEquals); }; diff --git a/packages/lib/src/equals/shallowEquals.ts b/packages/lib/src/equals/shallowEquals.ts index 594920dc..963c4968 100644 --- a/packages/lib/src/equals/shallowEquals.ts +++ b/packages/lib/src/equals/shallowEquals.ts @@ -1,24 +1,4 @@ -const isArray = (value: unknown) => { - return Array.isArray(value); -}; - -const isObject = (value: unknown) => { - return typeof value === "object" && value !== null; -}; - -const compareArrays = (a: unknown[], b: unknown[]) => { - return a.length === b.length && a.every((item, index) => Object.is(item, b[index])); -}; - -const compareObjects = (a: object, b: object) => { - const aObj = a as Record; - const bObj = b as Record; - - const keysA = Object.keys(a); - const keysB = Object.keys(b); - - return keysA.length === keysB.length && keysA.every((key) => key in bObj && Object.is(aObj[key], bObj[key])); -}; +import { compareArrays, compareObjects, isArray, isObject } from "../utils"; /** * 두 값의 얕은 비교를 수행 diff --git a/packages/lib/src/utils/helper.ts b/packages/lib/src/utils/helper.ts new file mode 100644 index 00000000..455c4ed5 --- /dev/null +++ b/packages/lib/src/utils/helper.ts @@ -0,0 +1,21 @@ +export function isArray(value: unknown) { + return Array.isArray(value); +} + +export function isObject(value: unknown) { + return typeof value === "object" && value !== null; +} + +export function compareArrays(a: unknown[], b: unknown[], compareFn = Object.is) { + return a.length === b.length && a.every((item, index) => compareFn(item, b[index])); +} + +export function compareObjects(a: object, b: object, compareFn = Object.is) { + const aObj = a as Record; + const bObj = b as Record; + + const keysA = Object.keys(a); + const keysB = Object.keys(b); + + return keysA.length === keysB.length && keysA.every((key) => key in bObj && compareFn(aObj[key], bObj[key])); +} diff --git a/packages/lib/src/utils/index.ts b/packages/lib/src/utils/index.ts new file mode 100644 index 00000000..b9755e78 --- /dev/null +++ b/packages/lib/src/utils/index.ts @@ -0,0 +1 @@ +export * from "./helper"; From 5eb891bfc35a8a862ccf88a8e34788bac35470c7 Mon Sep 17 00:00:00 2001 From: chan9yu Date: Sun, 20 Jul 2025 22:03:08 +0900 Subject: [PATCH 06/23] =?UTF-8?q?feat:=20useRef=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/useRef.ts | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/packages/lib/src/hooks/useRef.ts b/packages/lib/src/hooks/useRef.ts index 285d4ae7..de88a0e9 100644 --- a/packages/lib/src/hooks/useRef.ts +++ b/packages/lib/src/hooks/useRef.ts @@ -1,4 +1,14 @@ -export function useRef(initialValue: T): { current: T } { - // useState를 이용해서 만들어보세요. - return { current: initialValue }; +import { useState } from "react"; + +type RefObject = { + current: T; +}; + +/** + * React의 useRef 훅을 구현 + * - useState를 이용해서 리렌더링 간에 동일한 참조를 유지하는 ref 객체를 생성 + */ +export function useRef(initialValue: T) { + const [refObject] = useState>({ current: initialValue }); + return refObject; } From 4a6d4e61f82225074bd1bf0cf7387fe342465e62 Mon Sep 17 00:00:00 2001 From: chan9yu Date: Sun, 20 Jul 2025 23:25:20 +0900 Subject: [PATCH 07/23] =?UTF-8?q?feat:=20useMemo=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/useMemo.ts | 24 +++++++++++++++++++----- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/packages/lib/src/hooks/useMemo.ts b/packages/lib/src/hooks/useMemo.ts index e80692e2..c7dec633 100644 --- a/packages/lib/src/hooks/useMemo.ts +++ b/packages/lib/src/hooks/useMemo.ts @@ -1,8 +1,22 @@ -/* eslint-disable @typescript-eslint/no-unused-vars */ -import type { DependencyList } from "react"; +import { type DependencyList } from "react"; + import { shallowEquals } from "../equals"; +import { useRef } from "./useRef"; + +type MemoRef = { + deps: DependencyList; + value: T; +}; + +export function useMemo(factory: () => T, _deps: DependencyList, _equals = shallowEquals) { + // useRef로 메모이제이션 상태 저장 + const memoRef = useRef | null>(null); + + // 의존성 배열이 없거나, 이전과 다르면 새로 계산 + if (!memoRef.current || !_equals(memoRef.current.deps, _deps)) { + const value = factory(); + memoRef.current = { deps: _deps, value }; + } -export function useMemo(factory: () => T, _deps: DependencyList, _equals = shallowEquals): T { - // 직접 작성한 useRef를 통해서 만들어보세요. - return factory(); + return memoRef.current.value; } From d04db1a5a5fcb6efe0140a6970dbaf6f2e3b5783 Mon Sep 17 00:00:00 2001 From: chan9yu Date: Sun, 20 Jul 2025 23:51:45 +0900 Subject: [PATCH 08/23] =?UTF-8?q?feat:=20useCallback=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/useCallback.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/lib/src/hooks/useCallback.ts b/packages/lib/src/hooks/useCallback.ts index 712bc898..1919eee2 100644 --- a/packages/lib/src/hooks/useCallback.ts +++ b/packages/lib/src/hooks/useCallback.ts @@ -1,7 +1,8 @@ -/* eslint-disable @typescript-eslint/no-unused-vars,@typescript-eslint/no-unsafe-function-type */ +/* eslint-disable @typescript-eslint/no-unsafe-function-type, react-hooks/exhaustive-deps */ + import type { DependencyList } from "react"; +import { useMemo } from "./useMemo"; export function useCallback(factory: T, _deps: DependencyList) { - // 직접 작성한 useMemo를 통해서 만들어보세요. - return factory as T; + return useMemo(() => factory, _deps); } From 279080c14298c8dd8bdd4126349c6bcfad84364b Mon Sep 17 00:00:00 2001 From: chan9yu Date: Sun, 20 Jul 2025 23:53:01 +0900 Subject: [PATCH 09/23] =?UTF-8?q?refactor:=20useRef=20=EC=A3=BC=EC=84=9D?= =?UTF-8?q?=20=EC=A0=95=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/lib/src/hooks/useRef.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/packages/lib/src/hooks/useRef.ts b/packages/lib/src/hooks/useRef.ts index de88a0e9..2bdaae7a 100644 --- a/packages/lib/src/hooks/useRef.ts +++ b/packages/lib/src/hooks/useRef.ts @@ -4,11 +4,8 @@ type RefObject = { current: T; }; -/** - * React의 useRef 훅을 구현 - * - useState를 이용해서 리렌더링 간에 동일한 참조를 유지하는 ref 객체를 생성 - */ export function useRef(initialValue: T) { + // useState를 이용해서 리렌더링 간에 동일한 참조를 유지하는 ref 객체를 생성 const [refObject] = useState>({ current: initialValue }); return refObject; } From 9418a647c797d4fdde4c4b46f7ff4f66d47700f4 Mon Sep 17 00:00:00 2001 From: chan9yu Date: Mon, 21 Jul 2025 13:28:19 +0900 Subject: [PATCH 10/23] =?UTF-8?q?refactor:=20dispatchWithCondition=20?= =?UTF-8?q?=ED=95=A8=EC=88=98=EB=A5=BC=20=ED=99=9C=EC=9A=A9=ED=95=9C=20dee?= =?UTF-8?q?pEquals,=20shallowEquals=20=EB=A6=AC=ED=8C=A9=ED=86=A0=EB=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/lib/src/equals/deepEquals.ts | 30 +++++++++++------------- packages/lib/src/equals/shallowEquals.ts | 28 ++++++++++------------ packages/lib/src/utils/condition.ts | 18 ++++++++++++++ packages/lib/src/utils/index.ts | 1 + 4 files changed, 46 insertions(+), 31 deletions(-) create mode 100644 packages/lib/src/utils/condition.ts diff --git a/packages/lib/src/equals/deepEquals.ts b/packages/lib/src/equals/deepEquals.ts index 452918d0..d530dcb5 100644 --- a/packages/lib/src/equals/deepEquals.ts +++ b/packages/lib/src/equals/deepEquals.ts @@ -1,7 +1,7 @@ -import { compareArrays, compareObjects, isArray, isObject } from "../utils"; +import { compareArrays, compareObjects, dispatchWithCondition, isArray, isObject } from "../utils"; /** - * 두 값의 깊은 비교를 수행 (리팩토링 버전) + * 두 값의 깊은 비교를 수행 * * - 기본 타입 값들을 정확히 비교해야 한다 * - 배열을 정확히 비교해야 한다 @@ -9,18 +9,16 @@ import { compareArrays, compareObjects, isArray, isObject } from "../utils"; * - 중첩된 구조를 정확히 비교해야 한다 */ export const deepEquals = (a: unknown, b: unknown) => { - // 두 값이 정확히 같은지 확인 (참조가 같은 경우) - if (Object.is(a, b)) return true; - - // 둘 다 객체가 아니면 false - if (!isObject(a) || !isObject(b)) return false; - - // 서로 다른 타입을 받을 경우 false (ex: a: [], b: {} 인 경우 false) - if (isArray(a) !== isArray(b)) return false; - - // 둘 다 배열이면 배열 비교 - if (isArray(a) && isArray(b)) return compareArrays(a, b, deepEquals); - - // 둘 다 객체면 객체 비교 - return compareObjects(a, b, deepEquals); + return dispatchWithCondition<[typeof a, typeof b], boolean>( + // 두 값이 정확히 같은지 확인 (참조가 같은 경우) + [([a, b]) => Object.is(a, b), () => true], + // 둘 다 객체가 아니면 false + [([a, b]) => !isObject(a) || !isObject(b), () => false], + // 서로 다른 타입을 받을 경우 false (ex: a: [], b: {} 인 경우 false) + [([a, b]) => isArray(a) !== isArray(b), () => false], + // 둘 다 배열이면 배열 비교 + [([a, b]) => isArray(a) && isArray(b), ([a, b]) => compareArrays(a as unknown[], b as unknown[], deepEquals)], + // 둘 다 객체면 객체 비교 + ([a, b]) => compareObjects(a as object, b as object, deepEquals), + )([a, b]); }; diff --git a/packages/lib/src/equals/shallowEquals.ts b/packages/lib/src/equals/shallowEquals.ts index 963c4968..500ceb19 100644 --- a/packages/lib/src/equals/shallowEquals.ts +++ b/packages/lib/src/equals/shallowEquals.ts @@ -1,4 +1,4 @@ -import { compareArrays, compareObjects, isArray, isObject } from "../utils"; +import { compareArrays, compareObjects, dispatchWithCondition, isArray, isObject } from "../utils"; /** * 두 값의 얕은 비교를 수행 @@ -8,18 +8,16 @@ import { compareArrays, compareObjects, isArray, isObject } from "../utils"; * - 중첩된 구조를 깊게 비교하지 않아야 한다 */ export const shallowEquals = (a: unknown, b: unknown) => { - // 두 값이 정확히 같은지 확인 (참조가 같은 경우) - if (Object.is(a, b)) return true; - - // 둘 다 객체가 아니면 false - if (!isObject(a) || !isObject(b)) return false; - - // 서로다른 타입을 받을 경우 false (ex: a: [], b: {} 인 경우 false) - if (isArray(a) !== isArray(b)) return false; - - // 둘 다 배열이면 배열 비교 - if (isArray(a) && isArray(b)) return compareArrays(a, b); - - // 둘 다 객체면 객체 비교 - return compareObjects(a, b); + return dispatchWithCondition<[typeof a, typeof b], boolean>( + // 두 값이 정확히 같은지 확인 (참조가 같은 경우) + [([a, b]) => Object.is(a, b), () => true], + // 둘 다 객체가 아니면 false + [([a, b]) => !isObject(a) || !isObject(b), () => false], + // 서로 다른 타입을 받을 경우 false (ex: a: [], b: {} 인 경우 false) + [([a, b]) => isArray(a) !== isArray(b), () => false], + // 둘 다 배열이면 배열 비교 + [([a, b]) => isArray(a) && isArray(b), ([a, b]) => compareArrays(a as unknown[], b as unknown[])], + // 둘 다 객체면 객체 비교 + ([a, b]) => compareObjects(a as object, b as object), + )([a, b]); }; diff --git a/packages/lib/src/utils/condition.ts b/packages/lib/src/utils/condition.ts new file mode 100644 index 00000000..5802debe --- /dev/null +++ b/packages/lib/src/utils/condition.ts @@ -0,0 +1,18 @@ +type Condition = (param: T) => boolean; +type Handler = (param: T) => R; +type ConditionHandlerPair = [condition: Condition, handler: Handler]; + +export function dispatchWithCondition(...args: [...ConditionHandlerPair[], Handler]) { + const pairs = args.slice(0, -1) as ConditionHandlerPair[]; + const defaultHandler = args[args.length - 1] as Handler; + + return (param: T) => { + for (const [condition, handler] of pairs) { + if (condition(param)) { + return handler(param); + } + } + + return defaultHandler(param); + }; +} diff --git a/packages/lib/src/utils/index.ts b/packages/lib/src/utils/index.ts index b9755e78..c14d2877 100644 --- a/packages/lib/src/utils/index.ts +++ b/packages/lib/src/utils/index.ts @@ -1 +1,2 @@ +export * from "./condition"; export * from "./helper"; From 53233b8356e10f875104d53620cff27c5c92d7a4 Mon Sep 17 00:00:00 2001 From: chan9yu Date: Mon, 21 Jul 2025 16:09:26 +0900 Subject: [PATCH 11/23] =?UTF-8?q?refactor:=20useCallback=20=ED=83=80?= =?UTF-8?q?=EC=9E=85=20=EC=A0=95=EB=A6=AC=20=EB=B0=8F=20=EB=A6=B0=ED=8A=B8?= =?UTF-8?q?=20=EA=B2=BD=EA=B3=A0=20=EC=9A=B0=ED=9A=8C=20=EC=B2=98=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/lib/src/hooks/useCallback.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/lib/src/hooks/useCallback.ts b/packages/lib/src/hooks/useCallback.ts index 1919eee2..c78e048e 100644 --- a/packages/lib/src/hooks/useCallback.ts +++ b/packages/lib/src/hooks/useCallback.ts @@ -1,8 +1,8 @@ -/* eslint-disable @typescript-eslint/no-unsafe-function-type, react-hooks/exhaustive-deps */ - import type { DependencyList } from "react"; -import { useMemo } from "./useMemo"; -export function useCallback(factory: T, _deps: DependencyList) { - return useMemo(() => factory, _deps); +import type { AnyFunction } from "../types"; +import { useMemo as _useMemo } from "./useMemo"; // 린트 경고 방지를 위한 alias + +export function useCallback(factory: T, _deps: DependencyList) { + return _useMemo(() => factory, _deps); } From de04cb46fb7f758f036878e2da34f4486a3f82da Mon Sep 17 00:00:00 2001 From: chan9yu Date: Mon, 21 Jul 2025 16:09:59 +0900 Subject: [PATCH 12/23] =?UTF-8?q?feat:=20useDeepMemo=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/useDeepMemo.ts | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/packages/lib/src/hooks/useDeepMemo.ts b/packages/lib/src/hooks/useDeepMemo.ts index 3157958c..af563987 100644 --- a/packages/lib/src/hooks/useDeepMemo.ts +++ b/packages/lib/src/hooks/useDeepMemo.ts @@ -1,9 +1,8 @@ -/* eslint-disable react-hooks/exhaustive-deps */ import type { DependencyList } from "react"; -import { useMemo } from "./useMemo"; + import { deepEquals } from "../equals"; +import { useMemo as _useMemo } from "./useMemo"; // 린트 경고 방지를 위한 alias -export function useDeepMemo(factory: () => T, deps: DependencyList): T { - // 직접 작성한 useMemo를 참고해서 만들어보세요. - return useMemo(factory, deps, deepEquals); +export function useDeepMemo(factory: () => T, deps: DependencyList) { + return _useMemo(factory, deps, deepEquals); } From e34ee9365d9ae8a44869cb45ce9bae3c27a72e5d Mon Sep 17 00:00:00 2001 From: chan9yu Date: Mon, 21 Jul 2025 16:10:21 +0900 Subject: [PATCH 13/23] =?UTF-8?q?feat:=20useShallowState=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 | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/packages/lib/src/hooks/useShallowState.ts b/packages/lib/src/hooks/useShallowState.ts index 7b589748..7d197b7e 100644 --- a/packages/lib/src/hooks/useShallowState.ts +++ b/packages/lib/src/hooks/useShallowState.ts @@ -1,7 +1,17 @@ -import { useState } from "react"; +import { useState, type Dispatch, type SetStateAction } from "react"; + import { shallowEquals } from "../equals"; +import { useCallback } from "./useCallback"; + +export const useShallowState = (initialValue: T): [T, Dispatch>] => { + const [value, setValue] = useState(initialValue); + + const setShallow = useCallback((newValue: SetStateAction) => { + setValue((prev) => { + const nextValue = typeof newValue === "function" ? (newValue as (prevValue: T) => T)(prev) : newValue; + return shallowEquals(prev, nextValue) ? prev : nextValue; + }); + }, []); -export const useShallowState = (initialValue: Parameters>[0]) => { - // useState를 사용하여 상태를 관리하고, shallowEquals를 사용하여 상태 변경을 감지하는 훅을 구현합니다. - return useState(initialValue); + return [value, setShallow]; }; From 3217a54cfc719aad36771637b585982b98a572a6 Mon Sep 17 00:00:00 2001 From: chan9yu Date: Mon, 21 Jul 2025 16:12:02 +0900 Subject: [PATCH 14/23] =?UTF-8?q?feat:=20useAutoCallback=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 | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/packages/lib/src/hooks/useAutoCallback.ts b/packages/lib/src/hooks/useAutoCallback.ts index 23100162..41fc8787 100644 --- a/packages/lib/src/hooks/useAutoCallback.ts +++ b/packages/lib/src/hooks/useAutoCallback.ts @@ -2,6 +2,15 @@ import type { AnyFunction } from "../types"; import { useCallback } from "./useCallback"; import { useRef } from "./useRef"; -export const useAutoCallback = (fn: T): T => { - return fn; +export const useAutoCallback = (fn: T) => { + const fnRef = useRef(fn); + + // 매 렌더링마다 최신 함수로 업데이트 + fnRef.current = fn; + + const autoCallback = useCallback((...args: Parameters) => { + return fnRef.current(...args); + }, []); + + return autoCallback; }; From d132c714fafeab888cb2719860597fbfdfab5e80 Mon Sep 17 00:00:00 2001 From: chan9yu Date: Mon, 21 Jul 2025 17:17:54 +0900 Subject: [PATCH 15/23] =?UTF-8?q?feat:=20memo,=20deepMemo=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/hocs/deepMemo.ts | 5 ++++- packages/lib/src/hocs/memo.ts | 21 ++++++++++++++++++++- 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/packages/lib/src/hocs/deepMemo.ts b/packages/lib/src/hocs/deepMemo.ts index 13f7f18e..3ecc01d0 100644 --- a/packages/lib/src/hocs/deepMemo.ts +++ b/packages/lib/src/hocs/deepMemo.ts @@ -1,5 +1,8 @@ import type { FunctionComponent } from "react"; +import { deepEquals } from "../equals"; +import { memo } from "./memo"; + 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..01b5be30 100644 --- a/packages/lib/src/hocs/memo.ts +++ b/packages/lib/src/hocs/memo.ts @@ -1,6 +1,25 @@ import { type FunctionComponent } from "react"; + import { shallowEquals } from "../equals"; +import { useRef } from "../hooks"; + +type CacheContainer

= { + prevProps: P | null; + lastResult: ReturnType> | null; +}; export function memo

(Component: FunctionComponent

, equals = shallowEquals) { - return Component; + const MemoizedComponent: FunctionComponent

= (props) => { + const cache = useRef>({ lastResult: null, prevProps: null }); + + const shouldRender = !equals(cache.current.prevProps, props); + if (shouldRender) { + cache.current.lastResult = Component(props); + cache.current.prevProps = props; + } + + return cache.current.lastResult; + }; + + return MemoizedComponent; } From 0b3db6c0a5c8c6f44d6e85757c2bc8f0e3e028e1 Mon Sep 17 00:00:00 2001 From: chan9yu Date: Tue, 22 Jul 2025 19:45:49 +0900 Subject: [PATCH 16/23] =?UTF-8?q?feat:=20useSyncExternalStore=20=ED=98=B8?= =?UTF-8?q?=ED=99=98=EC=9D=84=20=EC=9C=84=ED=95=9C=20createObserver=20?= =?UTF-8?q?=EB=A6=AC=ED=8C=A9=ED=86=A0=EB=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/lib/src/createObserver.ts | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/packages/lib/src/createObserver.ts b/packages/lib/src/createObserver.ts index fe97bf6b..50759bf0 100644 --- a/packages/lib/src/createObserver.ts +++ b/packages/lib/src/createObserver.ts @@ -3,16 +3,24 @@ type Listener = () => void; export const createObserver = () => { const listeners = new Set(); - // useSyncExternalStore 에서 활용할 수 있도록 subscribe 함수를 수정합니다. const subscribe = (fn: Listener) => { listeners.add(fn); + + return () => { + unsubscribe(fn); + }; }; const unsubscribe = (fn: Listener) => { listeners.delete(fn); }; - const notify = () => listeners.forEach((listener) => listener()); + const notify = () => { + listeners.forEach((listener) => listener()); + }; - return { subscribe, notify }; + return { + subscribe, + notify, + }; }; From 54999d0cd84258ec7b3ff5ca89a65e634a649860 Mon Sep 17 00:00:00 2001 From: chan9yu Date: Tue, 22 Jul 2025 19:46:22 +0900 Subject: [PATCH 17/23] =?UTF-8?q?feat:=20useShallowSelector=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/useShallowSelector.ts | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/packages/lib/src/hooks/useShallowSelector.ts b/packages/lib/src/hooks/useShallowSelector.ts index b75ed791..8c36c57f 100644 --- a/packages/lib/src/hooks/useShallowSelector.ts +++ b/packages/lib/src/hooks/useShallowSelector.ts @@ -1,9 +1,22 @@ -import { useRef } from "react"; import { shallowEquals } from "../equals"; +import { useRef } from "./useRef"; type Selector = (state: T) => S; export const useShallowSelector = (selector: Selector) => { - // 이전 상태를 저장하고, shallowEquals를 사용하여 상태가 변경되었는지 확인하는 훅을 구현합니다. - return (state: T): S => selector(state); + const prevResult = useRef(null); + + const memoizedSelector = (state: T) => { + const result = selector(state); + + // 이전 결과가 있고, shallow 비교에서 동일하면 이전 결과 반환 + if (prevResult.current && shallowEquals(prevResult.current, result)) { + return prevResult.current; + } + + prevResult.current = result; + return result; + }; + + return memoizedSelector; }; From 3a81f187ed5c1730abb098c646820bd3122eb080 Mon Sep 17 00:00:00 2001 From: chan9yu Date: Tue, 22 Jul 2025 19:46:51 +0900 Subject: [PATCH 18/23] =?UTF-8?q?feat:=20useStore=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 | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/packages/lib/src/hooks/useStore.ts b/packages/lib/src/hooks/useStore.ts index acf3ad79..33fcd409 100644 --- a/packages/lib/src/hooks/useStore.ts +++ b/packages/lib/src/hooks/useStore.ts @@ -1,13 +1,15 @@ -import type { createStore } from "../createStore"; import { useSyncExternalStore } from "react"; + +import type { createStore } from "../createStore"; import { useShallowSelector } from "./useShallowSelector"; type Store = ReturnType>; -const defaultSelector = (state: T) => state as unknown as S; +const defaultSelector = (state: T) => state as unknown as S; -export const useStore = (store: Store, selector: (state: T) => S = defaultSelector) => { - // useSyncExternalStore와 useShallowSelector를 사용해서 store의 상태를 구독하고 가져오는 훅을 구현해보세요. +export const useStore = (store: Store, selector = defaultSelector) => { const shallowSelector = useShallowSelector(selector); - return shallowSelector(store.getState()); + const getSnapshot = () => shallowSelector(store.getState()); + + return useSyncExternalStore(store.subscribe, getSnapshot); }; From 132024280aa7c91401dca8b48b71287ea91f9012 Mon Sep 17 00:00:00 2001 From: chan9yu Date: Tue, 22 Jul 2025 19:47:31 +0900 Subject: [PATCH 19/23] =?UTF-8?q?feat:=20useStorage=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/useStorage.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/lib/src/hooks/useStorage.ts b/packages/lib/src/hooks/useStorage.ts index fdc97a6f..ab94e2fb 100644 --- a/packages/lib/src/hooks/useStorage.ts +++ b/packages/lib/src/hooks/useStorage.ts @@ -4,6 +4,6 @@ import type { createStorage } from "../createStorage"; type Storage = ReturnType>; export const useStorage = (storage: Storage) => { - // useSyncExternalStore를 사용해서 storage의 상태를 구독하고 가져오는 훅을 구현해보세요. - return storage.get(); + const storageStore = useSyncExternalStore(storage.subscribe, storage.get); + return storageStore; }; From 8fa19a380d9683ded8a268768299fa3da37a310b Mon Sep 17 00:00:00 2001 From: chan9yu Date: Tue, 22 Jul 2025 19:47:50 +0900 Subject: [PATCH 20/23] =?UTF-8?q?feat:=20useRouter=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/useRouter.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/packages/lib/src/hooks/useRouter.ts b/packages/lib/src/hooks/useRouter.ts index a18ae01f..846a1cb5 100644 --- a/packages/lib/src/hooks/useRouter.ts +++ b/packages/lib/src/hooks/useRouter.ts @@ -1,12 +1,14 @@ +import { useSyncExternalStore } from "react"; + import type { RouterInstance } from "../Router"; import type { AnyFunction } from "../types"; -import { useSyncExternalStore } from "react"; import { useShallowSelector } from "./useShallowSelector"; 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 getSnapshot = () => shallowSelector(router); + + return useSyncExternalStore(router.subscribe, getSnapshot); }; From 83dac2ba8b6635ae4b5889a40866d8779decbaae Mon Sep 17 00:00:00 2001 From: chan9yu Date: Thu, 24 Jul 2025 16:41:28 +0900 Subject: [PATCH 21/23] =?UTF-8?q?feat:=20ToastProvider=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 --- .../src/components/toast/ToastProvider.tsx | 83 +++++++++++++------ 1 file changed, 59 insertions(+), 24 deletions(-) diff --git a/packages/app/src/components/toast/ToastProvider.tsx b/packages/app/src/components/toast/ToastProvider.tsx index 0aed9451..24fc7ed2 100644 --- a/packages/app/src/components/toast/ToastProvider.tsx +++ b/packages/app/src/components/toast/ToastProvider.tsx @@ -1,52 +1,87 @@ /* eslint-disable react-refresh/only-export-components */ -import { createContext, memo, type PropsWithChildren, useContext, useReducer } from "react"; + +import { useAutoCallback } from "@hanghae-plus/lib"; +import { createContext, memo, type PropsWithChildren, useContext, useMemo, useReducer } from "react"; import { createPortal } from "react-dom"; + +import { debounce } from "../../utils"; import { Toast } from "./Toast"; import { createActions, initialState, toastReducer, type ToastType } from "./toastReducer"; -import { debounce } from "../../utils"; -type ShowToast = (message: string, type: ToastType) => void; -type Hide = () => void; +type ToastCommandContextValue = { + hide: () => void; + show: (message: string, type: ToastType) => void; +}; -const ToastContext = createContext<{ +type ToastStateContextValue = { message: string; type: ToastType; - show: ShowToast; - hide: Hide; -}>({ - ...initialState, +}; + +const ToastCommandContext = createContext({ show: () => null, hide: () => null, }); +const ToastStateContext = createContext({ + ...initialState, +}); + const DEFAULT_DELAY = 3000; -const useToastContext = () => useContext(ToastContext); export const useToastCommand = () => { - const { show, hide } = useToastContext(); - return { show, hide }; + const context = useContext(ToastCommandContext); + if (!context) { + throw new Error("ToastProvider 내에서 useToastCommand을 사용해야 합니다!"); + } + + return context; }; + export const useToastState = () => { - const { message, type } = useToastContext(); - return { message, type }; + const context = useContext(ToastStateContext); + if (!context) { + throw new Error("ToastProvider 내에서 useToastState을 사용해야 합니다!"); + } + + return context; }; 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 { show, hide } = useMemo(() => createActions(dispatch), [dispatch]); + const hideAfter = useMemo(() => debounce(hide, DEFAULT_DELAY), [hide]); + + const visible = state.message !== ""; - const showWithHide: ShowToast = (...args) => { - show(...args); + const showWithHide = useAutoCallback((message: string, type: ToastType) => { + show(message, type); hideAfter(); - }; + }); + + const toastCommandContextValue: ToastCommandContextValue = useMemo( + () => ({ + hide, + show: showWithHide, + }), + [hide, showWithHide], + ); + + const toastStateContextValue: ToastStateContextValue = useMemo( + () => ({ + message: state.message, + type: state.type, + }), + [state.message, state.type], + ); return ( - - {children} - {visible && createPortal(, document.body)} - + + + {children} + {visible && createPortal(, document.body)} + + ); }); From b450140ed658a83f513294a149432ada90b3b39e Mon Sep 17 00:00:00 2001 From: chan9yu Date: Thu, 24 Jul 2025 21:04:42 +0900 Subject: [PATCH 22/23] =?UTF-8?q?feat:=20ModalProvider=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 --- .../src/components/modal/ModalProvider.tsx | 35 +++++++++++++------ 1 file changed, 25 insertions(+), 10 deletions(-) diff --git a/packages/app/src/components/modal/ModalProvider.tsx b/packages/app/src/components/modal/ModalProvider.tsx index f4147ec6..d1ece93e 100644 --- a/packages/app/src/components/modal/ModalProvider.tsx +++ b/packages/app/src/components/modal/ModalProvider.tsx @@ -1,27 +1,42 @@ /* eslint-disable react-refresh/only-export-components */ -import { createContext, memo, type PropsWithChildren, type ReactNode, useContext, useState } from "react"; + +import { createContext, memo, type PropsWithChildren, type ReactNode, useContext, useMemo, useState } from "react"; import { createPortal } from "react-dom"; + import { Modal } from "./Modal"; -export const ModalContext = createContext<{ - open: (content: ReactNode) => void; +type ModalContextValue = { close: () => void; -}>({ - open: () => null, + open: (content: ReactNode) => void; +}; + +const ModalContext = createContext({ close: () => null, + open: () => null, }); -export const useModalContext = () => useContext(ModalContext); +export const useModalContext = () => { + const context = useContext(ModalContext); + if (!context) { + throw new Error("ModalProvider 내에서 useModalContext을 사용해야 합니다!"); + } + + return context; +}; export const ModalProvider = memo(({ children }: PropsWithChildren) => { const [content, setContent] = useState(null); - const open = (newContent: ReactNode) => setContent(newContent); - - const close = () => setContent(null); + const modalContextValue = useMemo( + () => ({ + close: () => setContent(null), + open: setContent, + }), + [], + ); return ( - + {children} {content && createPortal({content}, document.body)} From 6d00097e83d1bc1d656b804fd43ea8c04f4f54b2 Mon Sep 17 00:00:00 2001 From: chan9yu Date: Fri, 25 Jul 2025 01:45:11 +0900 Subject: [PATCH 23/23] =?UTF-8?q?refactor:=20useRef=20=EB=82=B4=EB=B6=80?= =?UTF-8?q?=20useState=20=EC=B4=88=EA=B8=B0=ED=99=94=20=EB=B6=80=EB=B6=84?= =?UTF-8?q?=20lazy=20initialization=20=EB=B0=A9=EC=8B=9D=EC=9C=BC=EB=A1=9C?= =?UTF-8?q?=20=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/lib/src/hooks/useRef.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/lib/src/hooks/useRef.ts b/packages/lib/src/hooks/useRef.ts index 2bdaae7a..7c91f1b8 100644 --- a/packages/lib/src/hooks/useRef.ts +++ b/packages/lib/src/hooks/useRef.ts @@ -6,6 +6,6 @@ type RefObject = { export function useRef(initialValue: T) { // useState를 이용해서 리렌더링 간에 동일한 참조를 유지하는 ref 객체를 생성 - const [refObject] = useState>({ current: initialValue }); + const [refObject] = useState>(() => ({ current: initialValue })); return refObject; }