-
Notifications
You must be signed in to change notification settings - Fork 56
[4팀 김지혜] Chapter 1-3. React, Beyond the Basics #48
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
7e7f70e
e6ea1b7
1197c99
96ebdff
7d6580e
19c4c84
b1da35e
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.tsx"></script> | ||
| </body> | ||
| </html> |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,52 +1,68 @@ | ||
| /* eslint-disable react-refresh/only-export-components */ | ||
| import { createContext, memo, type PropsWithChildren, useContext, useReducer } from "react"; | ||
| import { createContext, memo, type PropsWithChildren, useContext, useReducer, useMemo } from "react"; | ||
| import { createPortal } from "react-dom"; | ||
| import { Toast } from "./Toast"; | ||
| import { createActions, initialState, toastReducer, type ToastType } from "./toastReducer"; | ||
| import { debounce } from "../../utils"; | ||
|
|
||
| import { useAutoCallback } from "@hanghae-plus/lib"; | ||
| type ShowToast = (message: string, type: ToastType) => void; | ||
| type Hide = () => void; | ||
|
|
||
| const ToastContext = createContext<{ | ||
| message: string; | ||
| type: ToastType; | ||
| //action context 설정 | ||
| const ToastCommandContext = 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(ToastCommandContext); | ||
| return { show, hide }; | ||
| }; | ||
| export const useToastState = () => { | ||
| const { message, type } = useToastContext(); | ||
| const { message, type } = useContext(ToastStateContext); | ||
| return { message, type }; | ||
| }; | ||
|
|
||
| // ContextContext를 분리해도, value로 넘기는 함수의 참조가 바뀌면 하위 컴포넌트가 리렌더링됨. | ||
| // console를 통해서 리렌더링이 어디서 발생하는지 확인해 수정해야 함 | ||
| // show, hide, showWithHide, hideAfter 모두 useCallback/useMemo로 감싸서 참조 고정 | ||
| // Context value에 바로 객체 리터럴 넘기지 말고, useMemo로 만든 값 넘기기 | ||
| export const ToastProvider = memo(({ children }: PropsWithChildren) => { | ||
| const [state, dispatch] = useReducer(toastReducer, initialState); | ||
| const { show, hide } = createActions(dispatch); | ||
| // 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 = (...args) => { | ||
| // useAutoCallback으로 함수 고정 | ||
| const showWithHide: ShowToast = useAutoCallback((...args) => { | ||
| show(...args); | ||
| hideAfter(); | ||
| }; | ||
| }); | ||
|
|
||
| const commandValue = useMemo(() => ({ show: showWithHide, hide }), [showWithHide, 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 value={commandValue}> | ||
| <ToastStateContext value={stateValue}> | ||
| {children} | ||
| {visible && createPortal(<Toast />, document.body)} | ||
| </ToastStateContext> | ||
| </ToastCommandContext> | ||
| ); | ||
| }); | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,3 +1,38 @@ | ||
| export const deepEquals = (a: unknown, b: unknown) => { | ||
| return a === b; | ||
| }; | ||
| // deepEquals 함수는 두 값의 깊은 비교를 수행합니다. | ||
| export function deepEquals(objA: unknown, objB: unknown): boolean { | ||
| // 1. 기본 타입이거나 null인 경우 처리 | ||
| if (Object.is(objA, objB)) return true; | ||
| if (typeof objA !== "object" || objA === null || typeof objB !== "object" || objB === null) return false; | ||
|
|
||
| // 2. 둘 다 객체인 경우: | ||
| // 배열인지 확인 | ||
| // 재귀적으로 각 속성에 대해 deepEquals | ||
| const isArrayA = Array.isArray(objA); | ||
| const isArrayB = Array.isArray(objB); | ||
|
|
||
| if (isArrayA !== isArrayB) return false; | ||
| if (isArrayA && isArrayB) { | ||
| if (objA.length !== objB.length) return false; | ||
| for (let i = 0; i < objA.length; i++) { | ||
| if (!deepEquals(objA[i], objB[i])) return false; | ||
| } | ||
|
|
||
| return true; | ||
| } | ||
|
|
||
| // 객체의 키 개수가 다른 경우 처리 | ||
| const keysA = Object.keys(objA as Record<string, unknown>); | ||
| const keysB = Object.keys(objB as Record<string, unknown>); | ||
|
|
||
| if (keysA.length !== keysB.length) return false; | ||
|
|
||
| for (let i = 0; i < keysA.length; i++) { | ||
| const currentKey = keysA[i]; | ||
| if (!Object.prototype.hasOwnProperty.call(objB, currentKey)) return false; | ||
|
|
||
| if (!deepEquals((objA as Record<string, unknown>)[currentKey], (objB as Record<string, unknown>)[currentKey])) | ||
| return false; | ||
| } | ||
|
|
||
| return true; | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,3 +1,30 @@ | ||
| export const shallowEquals = (a: unknown, b: unknown) => { | ||
| return a === b; | ||
| }; | ||
| // shallowEquals 함수는 두 값의 얕은 비교를 수행합니다. | ||
| // 다양한 타입(기본값, 배열, 객체 등)을 받아야 하므로 unknown 타입으로 선언 | ||
| export function shallowEquals(objA: unknown, objB: unknown): boolean { | ||
| // 1. 두 값이 정확히 같은지 확인 (참조가 같은 경우) | ||
| if (Object.is(objA, objB)) return true; | ||
|
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. <자랑하고 싶어요> 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. 저는 아 그리구 첨언해보자면, |
||
|
|
||
| // 2. 둘 중 하나라도 객체가 아닌 경우 처리 - 진짜 객체만 분류 | ||
| // null과 객체를 구분하려면 null은 객체가 아니어야 하고, 진짜 객체만 객체로 취급해야 함 | ||
| if (typeof objA !== "object" || objA === null || typeof objB !== "object" || objB === null) return false; | ||
|
|
||
| // 3. 객체의 키 개수가 다른 경우 처리 | ||
| // 타입상 object임을 TypeScript에 명확히 알려주기 위해 타입 단언이 필요 | ||
| const keysA = Object.keys(objA as object); | ||
| const keysB = Object.keys(objB as object); | ||
|
|
||
| if (keysA.length !== keysB.length) return false; | ||
|
|
||
| // 4. 모든 키에 대해 얕은 비교 수행 | ||
| for (let i = 0; i < keysA.length; i++) { | ||
| const currentKey = keysA[i]; | ||
| // Object.prototype.hasOwnProperty.call()로 객체의 고유한 속성인지 확인 | ||
| if ( | ||
| !Object.prototype.hasOwnProperty.call(objB, currentKey) || | ||
| !Object.is((objA as Record<string, unknown>)[currentKey], (objB as Record<string, unknown>)[currentKey]) | ||
| ) { | ||
| 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 { ComponentType } from "react"; | ||
| import { memo } from "./memo.ts"; | ||
|
|
||
| // deepMemo HOC는 컴포넌트의 props를 깊은 비교하여 불필요한 리렌더링을 방지합니다. | ||
| // FunctionComponent: 함수형 컴포넌트만 type FunctionComponent<P = {}> = (props: P) => ReactElement | null; | ||
| // ComponentType: 함수형 + 클래스형 컴포넌트 모두 type ComponentType<P = {}> = FunctionComponent<P> | ComponentClass<P>; | ||
| // ? 요구사항 복붙하니까 되었다 왜..? | ||
| export function deepMemo<P extends object>(Component: FunctionComponent<P>) { | ||
| return Component; | ||
| // Component : 첫번째 인자 그대로 전달 / deepEquals : 두번째 인자 shallowEquals 대신 사용 | ||
|
|
||
| // 테스트 조건 | ||
| // props로 전달하는 값이 모두 변경되어야 리렌더링 된다. | ||
| // 깊은 객체 비교를 수행해야 한다. | ||
| // 깊은 배열 비교를 수행해야 한다. | ||
| // deepEquals 함수를 사용하여 props 비교 | ||
| // 앞에서 만든 memo를 사용 | ||
| return memo(Component, deepEquals); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,6 +1,33 @@ | ||
| import { type FunctionComponent } from "react"; | ||
| import { shallowEquals } from "../equals"; | ||
|
|
||
| // HoC (High Order Component) : 고차 컴포넌트 | ||
| // 함수가 컴포넌트를 받아서 새로운 컴포넌트를 반환하는 패턴 / 컴포넌트를 "감싸는" 함수라고 생각하면 됩니다 | ||
| // memo HOC는 컴포넌트의 props를 얕은 비교하여 불필요한 리렌더링을 방지합니다. | ||
| // 3-2. React 메모이제이션 발제자료 참고 중 | ||
| // P = Props의 줄임말, 제네릭 타입 매개변수 | ||
| // 컴포넌트가 받는 props의 타입을 나타냄 | ||
| // <P extends object>는 "P는 object를 확장하는 타입"이라는 제약 | ||
| export function memo<P extends object>(Component: FunctionComponent<P>, equals = shallowEquals) { | ||
| return Component; | ||
| // 1. 이전 props를 저장할 ref 생성 | ||
| let prevProps: P | null = null; // 클로저로 상태 저장 | ||
|
|
||
| // 2. 메모이제이션된 컴포넌트 생성 | ||
| // React.ReactElement : React 컴포넌트가 렌더링한 결과의 타입 / JSX 요소의 타입을 나타냄 / <div>Hello</div>, <MyComponent /> 등의 타입 | ||
| let memoizedResult: React.ReactElement | null = null; | ||
|
|
||
| return function (props: P) { | ||
| // 3. equals 함수를 사용하여 props 비교 | ||
| // equals을 사용하여 이전 props와 현재 props를 비교 | ||
| if (prevProps === null || !equals(prevProps, props)) { | ||
| console.log("Props changed, re-rendering"); | ||
| // 4. props가 변경된 경우에만 새로운 렌더링 수행 | ||
| memoizedResult = Component(props) as React.ReactElement; // 타입 단언 추가 | ||
| } else { | ||
| console.log("Props unchanged, using memoized result"); | ||
| } | ||
|
|
||
| prevProps = props; | ||
| return memoizedResult; | ||
| } as FunctionComponent<P>; | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,7 +1,8 @@ | ||
| /* 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"; | ||
|
|
||
| export function useCallback<T extends Function>(factory: T, _deps: DependencyList) { | ||
| // 직접 작성한 useMemo를 통해서 만들어보세요. | ||
| return factory as T; | ||
| // useCallback은 메모이제이션된 콜백 함수를 반환하는 Hook입니다. | ||
| export function useCallback<T extends Function>(factory: T, deps: DependencyList): T { | ||
| return useMemo(() => factory, deps); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,8 +1,20 @@ | ||
| /* eslint-disable @typescript-eslint/no-unused-vars */ | ||
| import type { DependencyList } from "react"; | ||
| import { useRef, type DependencyList } from "react"; | ||
| // DependencyList는 React의 useEffect, useMemo, useCallback 등에서 사용하는 "의존성 배열"의 타입 의미 | ||
| import { shallowEquals } from "../equals"; | ||
|
|
||
| // useMemo 훅은 계산 비용이 높은 값을 메모이제이션 합니다. | ||
| export function useMemo<T>(factory: () => T, _deps: DependencyList, _equals = shallowEquals): T { | ||
| // 직접 작성한 useRef를 통해서 만들어보세요. | ||
| return factory(); | ||
| // 1. 이전 의존성과 결과를 저장할 ref 생성 | ||
| const prevDeps = useRef<DependencyList>([]); | ||
| const prevResult = useRef<T | null>(null); | ||
|
|
||
| // 2. 현재 의존성과 이전 의존성 비교 | ||
| if (prevResult.current === null || !_equals(prevDeps.current, _deps)) { | ||
| // 3. 의존성이 변경된 경우 factory 함수 실행 및 결과 저장 | ||
| prevDeps.current = _deps; | ||
| prevResult.current = factory(); | ||
| } | ||
|
|
||
| // 4. 메모이제이션된 값 반환 | ||
| return prevResult.current; | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,4 +1,9 @@ | ||
| // useRef 훅은 렌더링 사이에 값을 유지하는 가변 ref 객체를 생성합니다. | ||
| import { useState } from "react"; | ||
|
|
||
| export function useRef<T>(initialValue: T): { current: T } { | ||
| // useState를 이용해서 만들어보세요. | ||
| return { current: initialValue }; | ||
| // 리렌더링이 되어도 useRef의 참조값이 유지된다. | ||
| // 렌더링 간에 ref 값을 유지하고, 값 변경 시 리렌더링을 트리거하지 않아야 한다. | ||
| const [ref] = useState(() => ({ current: initialValue })); | ||
| return ref; | ||
| } |
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.
<궁금해요>
Context를 사용할 때ToastCommandContext와ToastCommandContext.Provider중 어떤 방식을 사용해야 하는지 궁금합니다. 저는 공식문서 보고ToastCommandContext만 사용했는데,.provider를 붙여서 하신 분들이 많으셔서.. 어떻게 하셨고 왜 사용하셨는지 궁금합니다.