Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
b4ff75d
chore: 배포를 위한 설정 추가
chan9yu Jul 20, 2025
99c5bd4
Merge pull request #1 from chan9yu/chore/deploy
chan9yu Jul 20, 2025
6f42747
feat: shallowEquals 함수 구현
chan9yu Jul 20, 2025
3e82103
refactor: shallowEquals 함수를 선언적 방식으로 개선
chan9yu Jul 20, 2025
2fa9f27
Merge pull request #4 from chan9yu/feat/shallowEquals
chan9yu Jul 20, 2025
2d9b762
feat: deepEquals 함수 구현
chan9yu Jul 20, 2025
968fb25
refactor: 공통 헬퍼로직 유틸 디렉토리 분리, 이관작업
chan9yu Jul 20, 2025
8f2a631
Merge pull request #5 from chan9yu/feat/deepEquals
chan9yu Jul 20, 2025
5eb891b
feat: useRef 구현
chan9yu Jul 20, 2025
4a6d4e6
feat: useMemo 구현
chan9yu Jul 20, 2025
d04db1a
feat: useCallback 구현
chan9yu Jul 20, 2025
279080c
refactor: useRef 주석 정리
chan9yu Jul 20, 2025
5d96e18
Merge pull request #6 from chan9yu/feat/hooks
chan9yu Jul 20, 2025
9418a64
refactor: dispatchWithCondition 함수를 활용한 deepEquals, shallowEquals 리팩토링
chan9yu Jul 21, 2025
07d6d15
Merge pull request #7 from chan9yu/feat/equals
chan9yu Jul 21, 2025
53233b8
refactor: useCallback 타입 정리 및 린트 경고 우회 처리
chan9yu Jul 21, 2025
de04cb4
feat: useDeepMemo 구현
chan9yu Jul 21, 2025
e34ee93
feat: useShallowState 구현
chan9yu Jul 21, 2025
3217a54
feat: useAutoCallback 구현
chan9yu Jul 21, 2025
d132c71
feat: memo, deepMemo 구현
chan9yu Jul 21, 2025
82f5093
Merge pull request #8 from chan9yu/feat/hoc
chan9yu Jul 21, 2025
0b3db6c
feat: useSyncExternalStore 호환을 위한 createObserver 리팩토링
chan9yu Jul 22, 2025
54999d0
feat: useShallowSelector 구현
chan9yu Jul 22, 2025
3a81f18
feat: useStore 구현
chan9yu Jul 22, 2025
1320242
feat: useStorage 구현
chan9yu Jul 22, 2025
8fa19a3
feat: useRouter 구현
chan9yu Jul 22, 2025
de338e6
Merge pull request #9 from chan9yu/feat/hooks
chan9yu Jul 22, 2025
83dac2b
feat: ToastProvider 구현
chan9yu Jul 24, 2025
b450140
feat: ModalProvider 구현
chan9yu Jul 24, 2025
65d35e9
Merge pull request #10 from chan9yu/feat/context
chan9yu Jul 24, 2025
6d00097
refactor: useRef 내부 useState 초기화 부분 lazy initialization 방식으로 개선
chan9yu Jul 24, 2025
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
40 changes: 40 additions & 0 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
@@ -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
18 changes: 18 additions & 0 deletions packages/app/404.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<script>
var segmentCount = 1;
var l = window.location;
l.replace(
l.protocol +
"//" +
l.hostname +
(l.port ? ":" + l.port : "") +
l.pathname
.split("/")
.slice(0, 1 + segmentCount)
.join("/") +
"/?p=/" +
l.pathname.slice(1).split("/").slice(segmentCount).join("/").replace(/&/g, "~and~") +
(l.search ? "&q=" + l.search.slice(1).replace(/&/g, "~and~") : "") +
l.hash,
);
</script>
60 changes: 39 additions & 21 deletions packages/app/index.html
Original file line number Diff line number Diff line change
@@ -1,26 +1,44 @@
<!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"
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>상품 쇼핑몰</title>
<link rel="stylesheet" href="/src/styles.css">
<script src="https://cdn.tailwindcss.com"></script>
<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>
};
</script>
<script>
(function (l) {
if (l.search) {
var q = {};
l.search
.slice(1)
.split("&")
.forEach(function (v) {
var a = v.split("=");
q[a[0]] = a.slice(1).join("=").replace(/~and~/g, "&");
});
if (q.p !== undefined) {
window.history.replaceState(null, null, q.p + (q.q ? "?" + q.q : "") + l.hash);
}
}
})(window.location);
</script>
</head>

<body class="bg-gray-50">
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
35 changes: 25 additions & 10 deletions packages/app/src/components/modal/ModalProvider.tsx
Original file line number Diff line number Diff line change
@@ -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<ModalContextValue>({
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<ReactNode>(null);

const open = (newContent: ReactNode) => setContent(newContent);

const close = () => setContent(null);
const modalContextValue = useMemo<ModalContextValue>(
() => ({
close: () => setContent(null),
open: setContent,
}),
[],
);

return (
<ModalContext value={{ open, close }}>
<ModalContext value={modalContextValue}>
{children}
{content && createPortal(<Modal>{content}</Modal>, document.body)}
</ModalContext>
Expand Down
83 changes: 59 additions & 24 deletions packages/app/src/components/toast/ToastProvider.tsx
Original file line number Diff line number Diff line change
@@ -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<ToastCommandContextValue>({
show: () => null,
hide: () => null,
});

const ToastStateContext = createContext<ToastStateContextValue>({
...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]);

Choose a reason for hiding this comment

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

useMemo 활용해서 리팩토링 잘 한 것 같아요!


const visible = state.message !== "";

const showWithHide: ShowToast = (...args) => {
show(...args);
const showWithHide = useAutoCallback((message: string, type: ToastType) => {
show(message, type);

Choose a reason for hiding this comment

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

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 (
<ToastContext value={{ show: showWithHide, hide, ...state }}>
{children}
{visible && createPortal(<Toast />, document.body)}
</ToastContext>
<ToastCommandContext.Provider value={toastCommandContextValue}>
<ToastStateContext.Provider value={toastStateContextValue}>
{children}
{visible && createPortal(<Toast />, document.body)}
</ToastStateContext.Provider>
</ToastCommandContext.Provider>
);
});
14 changes: 11 additions & 3 deletions packages/lib/src/createObserver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,24 @@ type Listener = () => void;
export const createObserver = () => {
const listeners = new Set<Listener>();

// 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,
};
};
23 changes: 22 additions & 1 deletion packages/lib/src/equals/deepEquals.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,24 @@
import { compareArrays, compareObjects, dispatchWithCondition, isArray, isObject } from "../utils";

/**
* 두 값의 깊은 비교를 수행
*
* - 기본 타입 값들을 정확히 비교해야 한다
* - 배열을 정확히 비교해야 한다
* - 객체를 정확히 비교해야 한다
* - 중첩된 구조를 정확히 비교해야 한다
*/
export const deepEquals = (a: unknown, b: unknown) => {
return a === b;
return dispatchWithCondition<[typeof a, typeof b], boolean>(

Choose a reason for hiding this comment

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

지난 시간에 배운 방법을 적용하려 하는 모습이 좋은 것 같습니다

// 두 값이 정확히 같은지 확인 (참조가 같은 경우)
[([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]);
};
22 changes: 21 additions & 1 deletion packages/lib/src/equals/shallowEquals.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,23 @@
import { compareArrays, compareObjects, dispatchWithCondition, isArray, isObject } from "../utils";

/**
* 두 값의 얕은 비교를 수행
*
* - 기본 타입 값들을 정확히 비교해야 한다
* - 배열을 얕게 비교해야 한다
* - 중첩된 구조를 깊게 비교하지 않아야 한다
*/
export const shallowEquals = (a: unknown, b: unknown) => {
return a === b;
return dispatchWithCondition<[typeof a, typeof b], boolean>(
Copy link

Choose a reason for hiding this comment

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

따로 빼서 가져와서 쓰이니까 가독성이 확실히 좋아지네요.!

// 두 값이 정확히 같은지 확인 (참조가 같은 경우)
[([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]);
};
5 changes: 4 additions & 1 deletion packages/lib/src/hocs/deepMemo.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import type { FunctionComponent } from "react";

import { deepEquals } from "../equals";
import { memo } from "./memo";

export function deepMemo<P extends object>(Component: FunctionComponent<P>) {
return Component;
return memo(Component, deepEquals);
}
Loading