Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 38 additions & 0 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
name: Deploy to GitHub Pages

on:
push:
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: "22"

- name: Setup pnpm
uses: pnpm/action-setup@v4
with:
version: 10

- name: Install dependencies
run: pnpm install

- name: Build
run: pnpm build
env:
NODE_ENV: production

- name: Deploy to GitHub Pages
uses: peaceiris/actions-gh-pages@v4
if: github.ref == 'refs/heads/main'
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
publish_dir: ./packages/app/dist
26 changes: 26 additions & 0 deletions packages/app/404.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<!doctype html>
<html lang="ko">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>상품 쇼핑몰</title>
<script src="https://cdn.tailwindcss.com"></script>
<link rel="stylesheet" href="/src/styles.css" />
<script>
tailwind.config = {
theme: {
extend: {
colors: {
primary: "#3b82f6",
secondary: "#6b7280",
},
},
},
};
</script>
</head>
<body class="bg-gray-50">
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
50 changes: 32 additions & 18 deletions packages/app/src/components/toast/ToastProvider.tsx
Original file line number Diff line number Diff line change
@@ -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, useReducer, useMemo } from "react";
import { createPortal } from "react-dom";
import { Toast } from "./Toast";
import { createActions, initialState, toastReducer, type ToastType } from "./toastReducer";
Expand All @@ -8,45 +8,59 @@ import { debounce } from "../../utils";
type ShowToast = (message: string, type: ToastType) => void;
type Hide = () => void;

const ToastContext = createContext<{
message: string;
type: ToastType;
//action context 설정
const ToastActionsContext = createContext<{
show: ShowToast;
hide: Hide;
}>({
...initialState,
show: () => null,
hide: () => null,
});

//state context 설정
const ToastStateContext = createContext<{
message: string;
type: ToastType;
}>({
...initialState,
});

const DEFAULT_DELAY = 3000;

const useToastContext = () => useContext(ToastContext);
//커스텀 훅 : 자주 사용하는 로직을 커스텀 훅으로 묶음(외부에서도 사용 가능)
export const useToastCommand = () => {
const { show, hide } = useToastContext();
const { show, hide } = useContext(ToastActionsContext);
return { show, hide };
};

export const useToastState = () => {
const { message, type } = useToastContext();
const { message, type } = useContext(ToastStateContext);
return { message, type };
};

export const ToastProvider = memo(({ children }: PropsWithChildren) => {
const [state, dispatch] = useReducer(toastReducer, initialState);
const { show, hide } = createActions(dispatch);
const { show, hide } = useMemo(() => createActions(dispatch), [dispatch]);
const visible = state.message !== "";

const hideAfter = debounce(hide, DEFAULT_DELAY);
const hideAfter = useMemo(() => debounce(hide, DEFAULT_DELAY), [hide]);

const showWithHide: ShowToast = useCallback(
(...args) => {
show(...args);
hideAfter();
},
[show, hideAfter],
); //의존성 배열에 show와 hideAfter를 추가하여 변경 시에만 실행되도록 함

const showWithHide: ShowToast = (...args) => {
show(...args);
hideAfter();
};
const context = useMemo(() => ({ show: showWithHide, hide }), [showWithHide, hide]); //showWithHide와 hide가 변하지 않으므로 변하지 않음

return (
<ToastContext value={{ show: showWithHide, hide, ...state }}>
{children}
{visible && createPortal(<Toast />, document.body)}
</ToastContext>
<ToastActionsContext value={context}>
<ToastStateContext value={state}>
{children}
{visible && createPortal(<Toast />, document.body)}
</ToastStateContext>
</ToastActionsContext>
);
});

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

수현님~~ 내부에서 사용된 메모이제이션 훅들을 직접 만든 useMemo, useAutoCallback을 이용해 리팩토링 해보시는 건 어떨까요?
훅 직접 만드셨는데 직접 사용하고 평가해보시면 좋을 것 같습니다 !

9 changes: 6 additions & 3 deletions packages/lib/src/createObserver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,14 @@ export const createObserver = () => {
// useSyncExternalStore 에서 활용할 수 있도록 subscribe 함수를 수정합니다.
const subscribe = (fn: Listener) => {
listeners.add(fn);
return () => {
listeners.delete(fn);
};
};

const unsubscribe = (fn: Listener) => {
listeners.delete(fn);
};
// const unsubscribe = (fn: Listener) => {
// listeners.delete(fn);
// };
Comment on lines +9 to +16

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

오? 이전 unsubscirbe 함수를 주석처리하고 구현체만 옮기신 이유가 따로 있을까요?

unsubscribe 함수를 호출하게 될 경우, 함수읽는 입장에서는 구독이 종료될때 구독을 해제한다라는 것을 명확하게 알수 있지 않을까요?!
그리고 나중에 unsubscirbe도 사용할수도 있구요.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

저는 그냥 바로 return할 생각이었는데 나중에 unsubscribe를 사용하려면 다시 구현을 해놔야겠네요 ㅎㅎ !


const notify = () => listeners.forEach((listener) => listener());

Expand Down
36 changes: 33 additions & 3 deletions packages/lib/src/equals/deepEquals.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,33 @@
export const deepEquals = (a: unknown, b: unknown) => {
return a === b;
};
// deepEquals 함수는 두 값의 깊은 비교를 수행합니다.
export function deepEquals(objA: any, objB: any): boolean {
if (objA === objB) {
return true; // 동일한 참조를 가진 경우
}
// 1. 기본 타입이거나 null인 경우 처리
if (typeof objA !== "object" || typeof objB !== "object" || objA === null || objB === null) {
return false;
}
// 2. 둘 다 객체인 경우:
// - 배열인지 확인
// - 객체의 키 개수가 다른 경우 처리
// - 재귀적으로 각 속성에 대해 deepEquals 호출

// 이 부분을 적절히 수정하세요.
if (typeof objA === "object" && typeof objB === "object") {
if (Array.isArray(objA) !== Array.isArray(objB)) {
return false;
}
const keysA = Object.keys(objA);
const keysB = Object.keys(objB);
if (keysA.length !== keysB.length) {
return false;
}
for (const key of keysA) {
if (!keysB.includes(key) || !deepEquals(objA[key], objB[key])) {
return false;
}
}
return true;
}
return true;
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1depth까지 순회하며 비교하는 로직은 shallowEquals 함수와 동일하고, 2depth이상부터 비교만 재귀적으로 deepEqauls함수를 사용하시니, shallowEquals 함수 사용으로 불필요한 로직을 제거하고 가독성을 높이면서 리팩토링 해보시는 건 어떨까요??

29 changes: 26 additions & 3 deletions packages/lib/src/equals/shallowEquals.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,26 @@
export const shallowEquals = (a: unknown, b: unknown) => {
return a === b;
};
// shallowEquals 함수는 두 값의 얕은 비교를 수행합니다.
export function shallowEquals(objA: any, objB: any): boolean {
// 1. 두 값이 정확히 같은지 확인 (참조가 같은 경우)
if (objA === objB) {
return true;
}
// 2. 둘 중 하나라도 객체가 아닌 경우 처리
if (typeof objA !== "object" || objA === null || typeof objB !== "object" || objB === null) {
return false;
}
const keysA = Object.keys(objA);
const keysB = Object.keys(objB);
// 3. 객체의 키 개수가 다른 경우 처리
if (keysA.length !== keysB.length) {
return false;
}
// 4. 모든 키에 대해 얕은 비교 수행
for (const key of keysA) {
if (!keysB.includes(key) || objA[key] !== objB[key]) {
return false;
}
}

// 이 부분을 적절히 수정하세요.
return true;
}
17 changes: 16 additions & 1 deletion packages/lib/src/hocs/deepMemo.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,20 @@
import type { FunctionComponent } from "react";
import { deepEquals } from "../equals";
import { useRef } from "../hooks/useRef";
import { memo } from "./memo";

export function deepMemo<P extends object>(Component: FunctionComponent<P>) {
return Component;
// const MemoizedComponent = (props: P) => {
// const prevPropsRef = useRef<P | null>(null);
// const prevResultRef = useRef<ReturnType<FunctionComponent<P>> | null>(null);
// if (!deepEquals(prevPropsRef.current, props)) {
// prevPropsRef.current = props;
// prevResultRef.current = Component(props);
// }

// return prevResultRef.current;
// };

// return MemoizedComponent;
return memo(Component, deepEquals);
}
27 changes: 25 additions & 2 deletions packages/lib/src/hocs/memo.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,29 @@
import { type FunctionComponent } from "react";
import { shallowEquals } from "../equals";

import { useRef } from "../hooks/useRef";
/*
props를 얕은 비교를 통해 검사 후
props가 변경 되었을 경우 새로 렌더링
변경되지 않았을 경우 이전 렌더 결과 재사용
*/
export function memo<P extends object>(Component: FunctionComponent<P>, equals = shallowEquals) {
return Component;
// 1. 이전 props를 저장할 ref 생성

// 2. 메모이제이션된 컴포넌트 생성

// 3. equals 함수를 사용하여 props 비교

// 4. props가 변경된 경우에만 새로운 렌더링 수행
const MemoizedComponent = (props: P) => {
const prevPropsRef = useRef<P | null>(null);
const prevResultRef = useRef<ReturnType<FunctionComponent<P>> | null>(null);
if (!equals(prevPropsRef.current, props)) {
prevPropsRef.current = props;
prevResultRef.current = Component(props);
}

return prevResultRef.current;
};

return MemoizedComponent;
}
7 changes: 5 additions & 2 deletions packages/lib/src/hooks/useAutoCallback.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import type { AnyFunction } from "../types";
import { useCallback } from "./useCallback";
import { useRef } from "./useRef";

//함수 타입만 받아서 함수 그대로 반환하는 구조
export const useAutoCallback = <T extends AnyFunction>(fn: T): T => {
return fn;
const ref = useRef(fn);
ref.current = fn;
//(...args: Parameters<T>) ...args는 fn의 매개변수 타입을 그대로 가져옴
return useCallback((...args: Parameters<T>) => ref.current(...args), []) as T;
};
11 changes: 7 additions & 4 deletions packages/lib/src/hooks/useCallback.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
/* 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";
/*
함수 타입만 받을 수 있도록 제한
useCallback은 useMemo와 비슷하게 동작하지만, 함수 타입을 반환
*/
export function useCallback<T extends Function>(factory: T, _deps: DependencyList) {
// 직접 작성한 useMemo를 통해서 만들어보세요.
return factory as T;
return useMemo(() => factory, _deps);
}
16 changes: 13 additions & 3 deletions packages/lib/src/hooks/useMemo.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,18 @@
/* 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";

export function useMemo<T>(factory: () => T, _deps: DependencyList, _equals = shallowEquals): T {
// 직접 작성한 useRef를 통해서 만들어보세요.
return factory();

//값과 의존성 배열 저장
const ref = useRef<{ deps: DependencyList; value: T } | null>(null);
if (!ref.current || !_equals(ref.current.deps, _deps)) {
ref.current = {
deps: _deps,
value: factory(),
};
}

return ref.current.value;
}
11 changes: 9 additions & 2 deletions packages/lib/src/hooks/useRef.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,11 @@
import { useState } from "react";

export function useRef<T>(initialValue: T): { current: T } {
// useState를 이용해서 만들어보세요.
return { current: initialValue };
//반환 타입이 {current : T}
/**
* useState를 사용하여 초기값을 가진 ref object을 생성
* ref.current 값을 직접 바꿔도 리렌더링 되지 않음
**/
const [ref] = useState<{ current: T }>({ current: initialValue });
return ref;
}
2 changes: 1 addition & 1 deletion packages/lib/src/hooks/useRouter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,5 @@ const defaultSelector = <T, S = T>(state: T) => state as unknown as S;
export const useRouter = <T extends RouterInstance<AnyFunction>, S>(router: T, selector = defaultSelector<T, S>) => {
// useSyncExternalStore를 사용하여 router의 상태를 구독하고 가져오는 훅을 구현합니다.
const shallowSelector = useShallowSelector(selector);
return shallowSelector(router);
return useSyncExternalStore(router.subscribe, () => shallowSelector(router));
};
9 changes: 8 additions & 1 deletion packages/lib/src/hooks/useShallowSelector.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,12 @@ type Selector<T, S = T> = (state: T) => S;

export const useShallowSelector = <T, S = T>(selector: Selector<T, S>) => {
// 이전 상태를 저장하고, shallowEquals를 사용하여 상태가 변경되었는지 확인하는 훅을 구현합니다.
return (state: T): S => selector(state);
const prevStateRef = useRef<S | undefined>(undefined);
return (state: T): S => {
const currentState = selector(state);
if (prevStateRef.current === undefined || !shallowEquals(prevStateRef.current, currentState)) {
prevStateRef.current = currentState;
}
return prevStateRef.current as S;
};
};
18 changes: 13 additions & 5 deletions packages/lib/src/hooks/useShallowState.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,15 @@
import { useState } from "react";
import { useState, useCallback } from "react";
import { shallowEquals } from "../equals";

export const useShallowState = <T>(initialValue: Parameters<typeof useState<T>>[0]) => {
// useState를 사용하여 상태를 관리하고, shallowEquals를 사용하여 상태 변경을 감지하는 훅을 구현합니다.
return useState(initialValue);
//얕은 비교를 통해 상태를 관리
export const useShallowState = <T>(initialValue: T) => {
const [first, setFirst] = useState<T>(initialValue);
const customSetState = useCallback((next: T) => {
setFirst((prev) => {
if (!shallowEquals(prev, next)) {
return next;
}
return prev;
});
}, []);
return [first, customSetState] as const;
};
Loading