-
Notifications
You must be signed in to change notification settings - Fork 56
[6팀 김수현] Chapter 1-3. React, Beyond the Basics #35
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
689eff4
0b277fe
436948a
c059409
1477848
a3911ba
3ee3e71
55356c8
c26112c
bb17322
dc4abb2
cb3b9bf
72d4c4f
33a483d
a25582e
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,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 |
| 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> |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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
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. 오? 이전 unsubscirbe 함수를 주석처리하고 구현체만 옮기신 이유가 따로 있을까요? unsubscribe 함수를 호출하게 될 경우, 함수읽는 입장에서는 구독이 종료될때 구독을 해제한다라는 것을 명확하게 알수 있지 않을까요?!
Author
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. 저는 그냥 바로 return할 생각이었는데 나중에 unsubscribe를 사용하려면 다시 구현을 해놔야겠네요 ㅎㅎ ! |
||
|
|
||
| const notify = () => listeners.forEach((listener) => listener()); | ||
|
|
||
|
|
||
| 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; | ||
| } | ||
|
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. 1depth까지 순회하며 비교하는 로직은 shallowEquals 함수와 동일하고, 2depth이상부터 비교만 재귀적으로 deepEqauls함수를 사용하시니, shallowEquals 함수 사용으로 불필요한 로직을 제거하고 가독성을 높이면서 리팩토링 해보시는 건 어떨까요?? |
||
| 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; | ||
| } |
| 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); | ||
| } |
| 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; | ||
| } |
| 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; | ||
| }; |
| 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); | ||
| } |
| 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; | ||
| } |
| 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; | ||
| } |
| 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; | ||
| }; |
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, useAutoCallback을 이용해 리팩토링 해보시는 건 어떨까요?
훅 직접 만드셨는데 직접 사용하고 평가해보시면 좋을 것 같습니다 !