-
Notifications
You must be signed in to change notification settings - Fork 56
[5팀 여찬규] Chapter 1-3. React, Beyond the Basics #37
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
b4ff75d
99c5bd4
6f42747
3e82103
2fa9f27
2d9b762
968fb25
8f2a631
5eb891b
4a6d4e6
d04db1a
279080c
5d96e18
9418a64
07d6d15
53233b8
de04cb4
e34ee93
3217a54
d132c71
82f5093
0b3db6c
54999d0
3a81f18
1320242
8fa19a3
de338e6
83dac2b
b450140
65d35e9
6d00097
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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 |
| 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> |
| 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> |
| 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]); | ||
|
|
||
| const visible = state.message !== ""; | ||
|
|
||
| const showWithHide: ShowToast = (...args) => { | ||
| show(...args); | ||
| const showWithHide = useAutoCallback((message: string, type: ToastType) => { | ||
| show(message, type); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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> | ||
| ); | ||
| }); | ||
| 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>( | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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]); | ||
| }; | ||
| 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>( | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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]); | ||
| }; | ||
| 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); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
useMemo 활용해서 리팩토링 잘 한 것 같아요!