diff --git a/.github/ISSUE_TEMPLATE/task-template.md b/.github/ISSUE_TEMPLATE/task-template.md new file mode 100644 index 00000000..e1f74d7c --- /dev/null +++ b/.github/ISSUE_TEMPLATE/task-template.md @@ -0,0 +1,32 @@ +--- +name: task template +about: Describe this issue template's purpose here. +title: '' +labels: '' +assignees: 0miiii + +--- + +## ๐Ÿ“Œ ์ œ๋ชฉ +[Component] ๋กœ๊ทธ์ธ ํŽ˜์ด์ง€ ๊ตฌํ˜„ +[Page] ์ƒํ’ˆ ์ƒ์„ธ ํŽ˜์ด์ง€ UI/UX ๊ฐœ๋ฐœ +[Refactor] useProduct ํ›… ๋ชจ๋“ˆ ๋ถ„๋ฆฌ ๋“ฑ + +--- + +## ๐ŸŽฏ ๊ตฌํ˜„ ๋ชฉํ‘œ +- [ ] ๋ฒ„ํŠผ, ์ธํ’‹ ๋“ฑ ์ปดํฌ๋„ŒํŠธ UI ๊ตฌํ˜„ +- [ ] ์ƒํƒœ์— ๋”ฐ๋ผ submit ๊ฐ€๋Šฅ ์—ฌ๋ถ€ ์ œ์–ด +- [ ] ๋กœ๊ทธ์ธ ์š”์ฒญ API ์—ฐ๋™ (`POST /auth/login`) +- [ ] ์„ฑ๊ณต ์‹œ ํ† ํฐ ์ €์žฅ ๋ฐ ํ™ˆ ๋ฆฌ๋””๋ ‰์…˜ +- [ ] ์‹คํŒจ ์‹œ ์—๋Ÿฌ ๋ฉ”์‹œ์ง€ ํ‘œ์‹œ + +--- + +## ๐Ÿงฉ ์„ค๋ช… +- ํ•ด๋‹น ์ด์Šˆ์—์„œ ๊ตฌํ˜„ํ•˜๊ฑฐ๋‚˜ ๋ฆฌํŒฉํ† ๋งํ•  ํ”„๋ก ํŠธ์—”๋“œ ๊ธฐ๋Šฅ์˜ ๊ฐœ์š”๋ฅผ ์ž‘์„ฑํ•ด์ฃผ์„ธ์š”. +- ํ•ด๋‹น ๊ธฐ๋Šฅ์ด ์–ด๋–ค ์‚ฌ์šฉ์ž ์‹œ๋‚˜๋ฆฌ์˜ค(UX)์— ๋Œ€์‘ํ•˜๋Š”์ง€๋„ ๋ช…์‹œํ•ด์ฃผ์„ธ์š”. + +์˜ˆ: +- ์‚ฌ์šฉ์ž๊ฐ€ ์ด๋ฉ”์ผ/๋น„๋ฐ€๋ฒˆํ˜ธ ์ž…๋ ฅ ํ›„ ๋กœ๊ทธ์ธ ์š”์ฒญ์„ ๋ณด๋‚ผ ์ˆ˜ ์žˆ๋„๋ก ๋กœ๊ทธ์ธ ํŽ˜์ด์ง€ UI๋ฅผ ๊ตฌ์„ฑํ•ฉ๋‹ˆ๋‹ค. +- ์ƒํƒœ์— ๋”ฐ๋ผ ๋ฒ„ํŠผ ์ƒํƒœ ๋ณ€ํ™” ๋ฐ ์—๋Ÿฌ ๋ฉ”์‹œ์ง€๋ฅผ ์ฒ˜๋ฆฌํ•ฉ๋‹ˆ๋‹ค. diff --git a/packages/app/src/components/toast/ToastProvider.tsx b/packages/app/src/components/toast/ToastProvider.tsx index 0aed9451..2863e5de 100644 --- a/packages/app/src/components/toast/ToastProvider.tsx +++ b/packages/app/src/components/toast/ToastProvider.tsx @@ -1,5 +1,5 @@ /* eslint-disable react-refresh/only-export-components */ -import { createContext, memo, type PropsWithChildren, useContext, useReducer } from "react"; +import { createContext, memo, type PropsWithChildren, useCallback, useContext, useMemo, useReducer } from "react"; import { createPortal } from "react-dom"; import { Toast } from "./Toast"; import { createActions, initialState, toastReducer, type ToastType } from "./toastReducer"; @@ -8,45 +8,43 @@ import { debounce } from "../../utils"; 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<{ message: string; type: ToastType }>(initialState); +const ToastCommandContext = createContext<{ show: ShowToast; hide: Hide }>({ show: () => null, hide: () => null }); -const DEFAULT_DELAY = 3000; +export const useToastState = () => useContext(ToastStateContext); +export const useToastCommand = () => useContext(ToastCommandContext); -const useToastContext = () => useContext(ToastContext); -export const useToastCommand = () => { - const { show, hide } = useToastContext(); - return { show, hide }; -}; -export const useToastState = () => { - const { message, type } = useToastContext(); - return { message, type }; -}; +const DEFAULT_DELAY = 3000; 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) => { - show(...args); - hideAfter(); - }; + const showWithHide: ShowToast = useCallback( + (...args) => { + show(...args); + hideAfter(); + }, + [hideAfter, show], + ); + + const command = useMemo( + () => ({ + show: showWithHide, + hide, + }), + [hide, showWithHide], + ); 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..efa16976 100644 --- a/packages/lib/src/createObserver.ts +++ b/packages/lib/src/createObserver.ts @@ -6,10 +6,8 @@ export const createObserver = () => { // useSyncExternalStore ์—์„œ ํ™œ์šฉํ•  ์ˆ˜ ์žˆ๋„๋ก subscribe ํ•จ์ˆ˜๋ฅผ ์ˆ˜์ •ํ•ฉ๋‹ˆ๋‹ค. const subscribe = (fn: Listener) => { listeners.add(fn); - }; - const unsubscribe = (fn: Listener) => { - listeners.delete(fn); + return () => listeners.delete(fn); }; const notify = () => listeners.forEach((listener) => listener()); diff --git a/packages/lib/src/equals/deepEquals.ts b/packages/lib/src/equals/deepEquals.ts index f5f4ebd5..3601dc2a 100644 --- a/packages/lib/src/equals/deepEquals.ts +++ b/packages/lib/src/equals/deepEquals.ts @@ -1,3 +1,24 @@ -export const deepEquals = (a: unknown, b: unknown) => { +import { isObject } from "./utils"; + +export const deepEquals = (a: unknown, b: unknown): boolean => { + if (Array.isArray(a) && Array.isArray(b)) { + if (a.length !== b.length) { + return false; + } + + return a.every((item, index) => deepEquals(item, b[index])); + } + + if (isObject(a) && isObject(b)) { + const aKeys = Object.keys(a); + const bKeys = Object.keys(b); + + if (aKeys.length !== bKeys.length) { + return false; + } + + return aKeys.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..833fe27a 100644 --- a/packages/lib/src/equals/shallowEquals.ts +++ b/packages/lib/src/equals/shallowEquals.ts @@ -1,3 +1,24 @@ +import { isObject } from "./utils"; + export const shallowEquals = (a: unknown, b: unknown) => { + if (Array.isArray(a) && Array.isArray(b)) { + if (a.length !== b.length) { + return false; + } + + return a.every((el, index) => el === b[index]); + } + + if (isObject(a) && isObject(b)) { + const aKeys = Object.keys(a); + const bKeys = Object.keys(b); + + if (aKeys.length !== bKeys.length) { + return false; + } + + return aKeys.every((key) => 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..3c9c5a74 --- /dev/null +++ b/packages/lib/src/equals/utils.ts @@ -0,0 +1,3 @@ +export const isObject = (value: unknown): value is Record => { + return typeof value === "object" && value !== null && !Array.isArray(value); +}; diff --git a/packages/lib/src/hocs/deepMemo.ts b/packages/lib/src/hocs/deepMemo.ts index 13f7f18e..7d9e00f8 100644 --- a/packages/lib/src/hocs/deepMemo.ts +++ b/packages/lib/src/hocs/deepMemo.ts @@ -1,5 +1,7 @@ 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..07e99746 100644 --- a/packages/lib/src/hocs/memo.ts +++ b/packages/lib/src/hocs/memo.ts @@ -2,5 +2,17 @@ import { type FunctionComponent } from "react"; import { shallowEquals } from "../equals"; export function memo

(Component: FunctionComponent

, equals = shallowEquals) { - return Component; + let prevProps: P | null = null; + let memoizedElement: ReturnType | null = null; + + const MemoizedComponent = (props: P) => { + if (!prevProps || !equals(prevProps, props)) { + prevProps = props; + memoizedElement = Component(props); + } + + return memoizedElement; + }; + + return MemoizedComponent; } diff --git a/packages/lib/src/hooks/useAutoCallback.ts b/packages/lib/src/hooks/useAutoCallback.ts index 23100162..9805ce90 100644 --- a/packages/lib/src/hooks/useAutoCallback.ts +++ b/packages/lib/src/hooks/useAutoCallback.ts @@ -3,5 +3,13 @@ import { useCallback } from "./useCallback"; import { useRef } from "./useRef"; export const useAutoCallback = (fn: T): T => { - return fn; + const fnRef = useRef(fn); + + fnRef.current = fn; + + const stableFn = useCallback((...args: Parameters): ReturnType => { + return fnRef.current(...args); + }, []); + + return stableFn as T; }; diff --git a/packages/lib/src/hooks/useCallback.ts b/packages/lib/src/hooks/useCallback.ts index 712bc898..44344669 100644 --- a/packages/lib/src/hooks/useCallback.ts +++ b/packages/lib/src/hooks/useCallback.ts @@ -1,7 +1,7 @@ -/* 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"; export function useCallback(factory: T, _deps: DependencyList) { - // ์ง์ ‘ ์ž‘์„ฑํ•œ useMemo๋ฅผ ํ†ตํ•ด์„œ ๋งŒ๋“ค์–ด๋ณด์„ธ์š”. - return factory as T; + return useMemo(() => factory, _deps); } diff --git a/packages/lib/src/hooks/useMemo.ts b/packages/lib/src/hooks/useMemo.ts index e80692e2..686db97e 100644 --- a/packages/lib/src/hooks/useMemo.ts +++ b/packages/lib/src/hooks/useMemo.ts @@ -1,8 +1,23 @@ -/* 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(); + const ref = useRef<{ + deps: DependencyList; + value: T | undefined; + initialized: boolean; + }>({ + deps: [], + value: undefined, + initialized: false, + }); + + if (!ref.current.initialized || !_equals(ref.current.deps, _deps)) { + ref.current.value = factory(); + ref.current.deps = _deps; + ref.current.initialized = true; + } + + return ref.current.value as T; } diff --git a/packages/lib/src/hooks/useRef.ts b/packages/lib/src/hooks/useRef.ts index 285d4ae7..919854f9 100644 --- a/packages/lib/src/hooks/useRef.ts +++ b/packages/lib/src/hooks/useRef.ts @@ -1,4 +1,9 @@ -export function useRef(initialValue: T): { current: T } { - // useState๋ฅผ ์ด์šฉํ•ด์„œ ๋งŒ๋“ค์–ด๋ณด์„ธ์š”. - return { current: initialValue }; +import { useState } from "react"; + +type Ref = { current: T }; + +export function useRef(initialValue: T): Ref { + 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..fb592863 100644 --- a/packages/lib/src/hooks/useRouter.ts +++ b/packages/lib/src/hooks/useRouter.ts @@ -6,7 +6,7 @@ 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); + + return useSyncExternalStore(router.subscribe, () => shallowSelector(router)); }; diff --git a/packages/lib/src/hooks/useShallowSelector.ts b/packages/lib/src/hooks/useShallowSelector.ts index b75ed791..e48342ff 100644 --- a/packages/lib/src/hooks/useShallowSelector.ts +++ b/packages/lib/src/hooks/useShallowSelector.ts @@ -4,6 +4,17 @@ import { shallowEquals } from "../equals"; type Selector = (state: T) => S; export const useShallowSelector = (selector: Selector) => { - // ์ด์ „ ์ƒํƒœ๋ฅผ ์ €์žฅํ•˜๊ณ , shallowEquals๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ์ƒํƒœ๊ฐ€ ๋ณ€๊ฒฝ๋˜์—ˆ๋Š”์ง€ ํ™•์ธํ•˜๋Š” ํ›…์„ ๊ตฌํ˜„ํ•ฉ๋‹ˆ๋‹ค. - return (state: T): S => selector(state); + const prevSelectedRef = useRef(null); + const hasInitialized = useRef(false); + + return (state: T): S => { + const selected = selector(state); + + if (!hasInitialized.current || !shallowEquals(prevSelectedRef.current, selected)) { + hasInitialized.current = true; + prevSelectedRef.current = selected; + } + + return prevSelectedRef.current as S; + }; }; diff --git a/packages/lib/src/hooks/useShallowState.ts b/packages/lib/src/hooks/useShallowState.ts index 7b589748..c8d471a9 100644 --- a/packages/lib/src/hooks/useShallowState.ts +++ b/packages/lib/src/hooks/useShallowState.ts @@ -1,7 +1,19 @@ import { useState } from "react"; import { shallowEquals } from "../equals"; +import { useCallback } from "./useCallback"; -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((next: T) => { + setState((prev) => { + if (shallowEquals(prev, next)) { + return prev; + } + + return next; + }); + }, []); + + return [state, setShallowState] as const; }; diff --git a/packages/lib/src/hooks/useStorage.ts b/packages/lib/src/hooks/useStorage.ts index fdc97a6f..1c51cd7a 100644 --- a/packages/lib/src/hooks/useStorage.ts +++ b/packages/lib/src/hooks/useStorage.ts @@ -4,6 +4,5 @@ import type { createStorage } from "../createStorage"; type Storage = ReturnType>; 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..ecacf423 100644 --- a/packages/lib/src/hooks/useStore.ts +++ b/packages/lib/src/hooks/useStore.ts @@ -7,7 +7,7 @@ type Store = ReturnType>; 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()); + + return useSyncExternalStore(store.subscribe, () => shallowSelector(store.getState())); };