-
Notifications
You must be signed in to change notification settings - Fork 56
[1팀 이의찬] Chapter 1-3. React, Beyond the Basics #36
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
d96801f
a0c1262
ea8d423
0ed217c
8135065
ea12e82
c3a7b2c
99c6b90
b03e2c8
6fad527
c6da682
08eed21
aa56069
5a3e7a5
8ff8ed0
3c7e2f8
f636215
3f8813c
133926f
35fbac0
e88d6f9
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,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.js"></script> | ||
| </body> | ||
| </html> |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,52 +1,61 @@ | ||
| /* eslint-disable react-refresh/only-export-components */ | ||
| import { createContext, memo, type PropsWithChildren, useContext, useReducer } from "react"; | ||
| import { createPortal } from "react-dom"; | ||
| import { Toast } from "./Toast"; | ||
| import { createActions, initialState, toastReducer, type ToastType } from "./toastReducer"; | ||
| import { initialState, toastReducer, type ToastType } from "./toastReducer"; | ||
| import { debounce } from "../../utils"; | ||
| import { useCallback, useMemo } from "@hanghae-plus/lib/src/hooks"; | ||
|
|
||
| type ShowToast = (message: string, type: ToastType) => void; | ||
| type Hide = () => void; | ||
|
|
||
| const ToastContext = createContext<{ | ||
| message: string; | ||
| type: ToastType; | ||
| const ToastCommandContext = createContext<{ | ||
| show: ShowToast; | ||
| hide: Hide; | ||
| }>({ | ||
| ...initialState, | ||
| show: () => null, | ||
| hide: () => null, | ||
| }); | ||
|
|
||
| const ToastStateContext = createContext<{ | ||
| message: string; | ||
| type: ToastType; | ||
| }>({ | ||
| ...initialState, | ||
| }); | ||
|
|
||
| const DEFAULT_DELAY = 3000; | ||
|
|
||
| const useToastContext = () => useContext(ToastContext); | ||
| export const useToastCommand = () => { | ||
| const { show, hide } = useToastContext(); | ||
| return { show, hide }; | ||
| }; | ||
| export const useToastState = () => { | ||
| const { message, type } = useToastContext(); | ||
| return { message, type }; | ||
| }; | ||
| export const useToastCommand = () => useContext(ToastCommandContext); | ||
|
|
||
| export const useToastState = () => useContext(ToastStateContext); | ||
|
|
||
| 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 hide = useCallback(() => { | ||
| dispatch({ type: "hide" }); | ||
| }, []); | ||
|
|
||
| const hideAfter = useMemo(() => debounce(hide, DEFAULT_DELAY), [hide]); | ||
|
|
||
| const show = useCallback( | ||
| (message: string, type: ToastType) => { | ||
| dispatch({ type: "show", payload: { message, type } }); | ||
| hideAfter(); | ||
| }, | ||
| [hideAfter], | ||
| ); | ||
|
|
||
| const showWithHide: ShowToast = (...args) => { | ||
| show(...args); | ||
| hideAfter(); | ||
| }; | ||
| const commandValue = useMemo(() => ({ show, hide }), [show, hide]); | ||
| const stateValue = 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={commandValue}> | ||
| <ToastStateContext.Provider value={stateValue}> | ||
| {children} | ||
| {visible && createPortal(<Toast />, document.body)} | ||
| </ToastStateContext.Provider> | ||
| </ToastCommandContext.Provider> | ||
| ); | ||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,3 +1,20 @@ | ||
| export const deepEquals = (a: unknown, b: unknown) => { | ||
| return a === b; | ||
| import { compareObject, isObject } from "./util"; | ||
|
|
||
| /** | ||
| * 두 값의 깊은 비교 결과를 반환 | ||
| * @param {unknown} a 비교할 첫 번째 값 | ||
| * @param {unknown} b 비교할 두 번째 값 | ||
| * @returns {boolean} 두 값의 깊은 비교 결과 | ||
| */ | ||
| export const deepEquals = (a: unknown, b: unknown): boolean => { | ||
| // 1. 기본 타입이거나 null인 경우 처리 | ||
| if (a === b) return true; | ||
|
|
||
| // 2. 둘 다 객체인 경우: | ||
| // - 배열인지 확인 | ||
| // - 객체의 키 개수가 다른 경우 처리 | ||
| // - 재귀적으로 각 속성에 대해 deepEquals 호출 | ||
| if (!isObject(a) || !isObject(b)) return false; | ||
|
|
||
| return compareObject(a, b, deepEquals); | ||
| }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,3 +1,17 @@ | ||
| import { compareObject, isObject } from "./util"; | ||
|
|
||
| /** | ||
| * 두 값의 얕은 비교 결과를 반환 | ||
| * @param {unknown} a 비교할 첫 번째 값 | ||
| * @param {unknown} b 비교할 두 번째 값 | ||
| * @returns {boolean} 두 값이 얕은 비교 결과 | ||
| */ | ||
| export const shallowEquals = (a: unknown, b: unknown) => { | ||
| return a === b; | ||
| // 1. 두 값이 정확히 같은지 확인 (참조가 같은 경우) | ||
| if (Object.is(a, b)) return true; | ||
|
|
||
| // 2. 둘 중 하나라도 객체가 아닌 경우 처리 | ||
| if (!isObject(a) || !isObject(b)) return false; | ||
|
|
||
| return compareObject(a, b); | ||
| }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,29 @@ | ||
| /** | ||
| * 주어진 값이 객체인지 확인하는 함수 | ||
| * @reference https://github.com/toss/es-toolkit/blob/main/src/compat/predicate/isObject.ts | ||
| * @param {unknown} value - 객체인지 확인할 값 | ||
| * @returns {value is Record<string, unknown>} 객체인지 확인 결과 | ||
| */ | ||
| export function isObject(value?: unknown): value is Record<string, unknown> { | ||
| return value !== null && typeof value === "object"; | ||
| } | ||
|
|
||
| /** | ||
| * 두 객체를 비교하는 함수 | ||
| * @param a 비교할 첫 번째 객체 | ||
| * @param b 비교할 두 번째 객체 | ||
| * @returns 두 객체가 같은지 확인 결과 | ||
| */ | ||
| export function compareObject( | ||
| a: Record<string, unknown>, | ||
| b: Record<string, unknown>, | ||
| compareFn: (a: unknown, b: unknown) => boolean = (a, b) => a === b, | ||
| ): boolean { | ||
| // 객체의 키 개수가 다른 경우 처리 | ||
| const keysA = Object.keys(a); | ||
| const keysB = Object.keys(b); | ||
| if (keysA.length !== keysB.length) return false; | ||
|
|
||
| // 모든 키에 대해 비교 수행 | ||
| return keysA.every((key) => compareFn(a[key], b[key])); | ||
| } | ||
|
Comment on lines
+1
to
+29
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. 오 utils 함수로 분리 좋습니다! 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. compareObject 함수명만 봤을 때 상세 동작을 상상하기가 어려운 것 같아요! 참조값을 비교하는지, 키값을 비교하는지 더 명확하게 알 수 있으면 좋을 것 같아요!! compareObject로 분리하는 것 좋네요 반복되는 로직이었는데! 👍🏻 |
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,5 +1,14 @@ | ||
| import type { FunctionComponent } from "react"; | ||
| import { memo } from "./memo"; | ||
| import { deepEquals } from "../equals"; | ||
|
|
||
| /** | ||
| * deepMemo HOC는 컴포넌트의 props를 깊은 비교하여 불필요한 리렌더링을 방지합니다. | ||
| * @param Component 메모이제이션할 컴포넌트 | ||
| * @returns 메모이제이션된 컴포넌트 | ||
| */ | ||
| export function deepMemo<P extends object>(Component: FunctionComponent<P>) { | ||
| return Component; | ||
| // deepEquals 함수를 사용하여 props 비교 | ||
| // 앞에서 만든 memo를 사용 | ||
| return memo(Component, deepEquals); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,6 +1,28 @@ | ||
| import { type FunctionComponent } from "react"; | ||
| import { type FunctionComponent, type JSX } from "react"; | ||
| import { shallowEquals } from "../equals"; | ||
| import { useRef } from "../hooks"; | ||
|
|
||
| /** | ||
| * memo HOC는 컴포넌트의 props를 얕은 비교하여 불필요한 리렌더링을 방지합니다. | ||
| * @param Component 메모이제이션할 컴포넌트 | ||
| * @param equals 비교 함수 | ||
| * @returns 메모이제이션된 컴포넌트 | ||
| */ | ||
| export function memo<P extends object>(Component: FunctionComponent<P>, equals = shallowEquals) { | ||
| return Component; | ||
| const MemoizedComponent = (props: P) => { | ||
| // Hook을 컴포넌트 내부에서 호출 | ||
| const prevPropsRef = useRef<P | null>(null); | ||
| const prevResultRef = useRef<JSX.Element | null>(null); | ||
|
|
||
| const shouldUpdate = prevPropsRef.current === null || !equals(prevPropsRef.current, props); | ||
Legitgoons marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| if (shouldUpdate) { | ||
| prevPropsRef.current = props; | ||
| prevResultRef.current = Component(props) as JSX.Element; | ||
| } | ||
|
|
||
| return prevResultRef.current!; | ||
| }; | ||
|
|
||
| return MemoizedComponent; | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,7 +1,13 @@ | ||
| /* 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"; | ||
|
|
||
| /** | ||
| * 콜백 함수를 메모이제이션하는 훅 | ||
| * @param factory 메모이제이션할 콜백 함수 | ||
| * @param _deps 의존성 배열 | ||
| * @returns 메모이제이션된 콜백 함수 | ||
| */ | ||
| 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,26 @@ | ||
| /* eslint-disable @typescript-eslint/no-unused-vars */ | ||
| import type { DependencyList } from "react"; | ||
| import { shallowEquals } from "../equals"; | ||
| import { useRef } from "./useRef"; | ||
|
|
||
| /** | ||
| * useMemo 훅은 의존성이 변경되었을 때만 함수를 실행하여 메모이제이션된 값을 반환합니다. | ||
| * @param factory 메모이제이션할 함수 | ||
| * @param _deps 의존성 배열 | ||
| * @param _equals 비교 함수 | ||
| * @returns 메모이제이션된 값 | ||
| */ | ||
| export function useMemo<T>(factory: () => T, _deps: DependencyList, _equals = shallowEquals): T { | ||
| // 직접 작성한 useRef를 통해서 만들어보세요. | ||
| return factory(); | ||
| // 1. 이전 의존성과 결과를 저장할 ref 생성 | ||
| const ref = useRef<{ deps: DependencyList; result: T } | null>(null); | ||
|
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. 주석이 없어도 이 ref값이 잘 이해되도록 구체적인 변수명으로 해주면 더 좋을 것 같아요! |
||
| if (!ref.current) ref.current = { deps: _deps, result: factory() }; | ||
|
|
||
| // 2. 현재 의존성과 이전 의존성 비교 | ||
| if (!_equals(ref.current.deps, _deps)) { | ||
| // 3. 의존성이 변경된 경우 factory 함수 실행 및 결과 저장 | ||
| ref.current.result = factory(); | ||
| ref.current.deps = _deps; | ||
| } | ||
|
|
||
| // 4. 메모이제이션된 값 반환 | ||
| return ref.current.result; | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,4 +1,16 @@ | ||
| import { useState } from "react"; | ||
|
|
||
| /** | ||
| * 렌더링 사이에 값을 유지하는 가변 ref 객체를 생성 | ||
| * - 반환된 ref 객체는 컴포넌트의 전체 생명주기 동안 유지됩니다. | ||
| * - ref 객체의 .current 속성을 변경해도 리렌더링이 트리거되지 않습니다. | ||
| * - DOM 요소에 접근하거나 이전 상태를 저장하는 등 다양한 용도로 사용될 수 있습니다. | ||
| * @param initialValue 초기값 | ||
| * @returns ref 객체 | ||
| */ | ||
|
|
||
| export function useRef<T>(initialValue: T): { current: T } { | ||
| // useState를 이용해서 만들어보세요. | ||
| return { current: initialValue }; | ||
| //useState를 사용하여 초기값을 저장하고, 이후 값을 변경할 때 리렌더링을 트리거하지 않도록 합니다. | ||
| const [refObject] = useState(() => ({ current: initialValue })); | ||
| return refObject; | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -5,8 +5,17 @@ import { useShallowSelector } from "./useShallowSelector"; | |
|
|
||
| const defaultSelector = <T, S = T>(state: T) => state as unknown as S; | ||
|
|
||
| /** | ||
| * useSyncExternalStore를 사용해서 router의 상태를 구독하고 가져오는 훅 | ||
| * @param router 라우터 | ||
| * @param selector 선택자 함수 | ||
| * @returns 라우터의 상태 | ||
| */ | ||
| 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), | ||
| () => shallowSelector(router), | ||
|
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. getServerSnapshot 파라미터는 useRouter 파라미터에 옵션으로 넣어줘도 될 것 같아요 export const useRouter = <T extends RouterInstance<AnyFunction>, S>(
router: T,
selector = defaultSelector<T, S>,
getServerSnapshot?: () => S, // 옵셔널 파라미터
) => {
// useSyncExternalStore를 사용하여 router의 상태를 구독하고 가져오는 훅을 구현합니다.
const shallowSelector = useShallowSelector(selector);
return shallowSelector(router);
return useSyncExternalStore(
router.subscribe,
() => shallowSelector(router),
getServerSnapshot // 변경
// 생략 |
||
| ); | ||
| }; | ||
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.
subscribe에서 반환해주는 함수 로직이 unsubscribe랑 동일하니까 unsubscribe를 그대로 반환해주면 될 것 같아요!