-
Notifications
You must be signed in to change notification settings - Fork 56
[8팀 김상수] Chapter 1-3. React, Beyond the Basics #34
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
b380b12
2a68809
5d908bf
70e1673
2db140d
baf381a
ad2857b
ef082c4
50c4cad
7515773
f3ce84b
7b54127
d2f8a69
89bd349
4a631dc
c9b688c
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 |
|---|---|---|
|
|
@@ -18,6 +18,7 @@ | |
| } | ||
| }; | ||
| </script> | ||
|
|
||
| </head> | ||
| <body class="bg-gray-50"> | ||
| <div id="root"></div> | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,52 +1,77 @@ | ||
| /* 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, useToastActions } from "./toastReducer"; | ||
| import { debounce } from "../../utils"; | ||
| import { useAutoCallback } from "@hanghae-plus/lib"; | ||
| import { useMemo } from "@hanghae-plus/lib/src/hooks"; | ||
|
|
||
| type ShowToast = (message: string, type: ToastType) => void; | ||
| type Hide = () => void; | ||
|
|
||
| const ToastContext = createContext<{ | ||
| message: string; | ||
| type: ToastType; | ||
| // toast command를 관리하는 context | ||
| const ToastCommandContext = createContext<{ | ||
| show: ShowToast; | ||
| hide: Hide; | ||
| }>({ | ||
| ...initialState, | ||
| show: () => null, | ||
| hide: () => null, | ||
| }); | ||
|
|
||
| // toast 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(); | ||
| return { show, hide }; | ||
| }; | ||
| export const useToastState = () => { | ||
| const { message, type } = useToastContext(); | ||
| return { message, type }; | ||
| }; | ||
| // context를 사용하는 함수 | ||
| 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); | ||
| // action을 생성 | ||
| const { show, hide } = useToastActions(dispatch); | ||
|
|
||
| const showWithHide: ShowToast = (...args) => { | ||
| // hide 함수를 3초 후에 호출 | ||
| const hideAfter = useMemo(() => debounce(hide, DEFAULT_DELAY), [hide]); | ||
|
|
||
| // show 함수에 hideAfter 함수를 추가 | ||
| const showWithHide: ShowToast = useAutoCallback((...args) => { | ||
| show(...args); | ||
| hideAfter(); | ||
| }; | ||
| }); | ||
|
|
||
| // Command context value를 메모이제이션 | ||
| const commandValue = useMemo( | ||
| () => ({ | ||
| show: showWithHide, | ||
| hide, | ||
| }), | ||
| [showWithHide, hide], | ||
| ); | ||
|
|
||
| // State context value를 메모이제이션 | ||
| 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,30 @@ | ||
| import { typeUtils } from "../utils/typeUtil.ts"; | ||
| import { deepEqualsArrays } from "./deepEqualsArrays.ts"; | ||
| import deepEqualsObjects from "./deepEqualsObjects.ts"; | ||
|
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. 해당 함수 util로 빼주셧네요 깔끔해요 |
||
|
|
||
| export const deepEquals = (a: unknown, b: unknown) => { | ||
| return a === b; | ||
| // 원시 타입인 경우 | ||
| if (typeUtils.isPrimitive(a) || typeUtils.isPrimitive(b)) { | ||
| return a === b; | ||
| } | ||
|
|
||
| const aIsArray = Array.isArray(a); | ||
| const bIsArray = Array.isArray(b); | ||
|
|
||
| // 비교하려는 값의 타입이 다르면 false | ||
| if (aIsArray !== bIsArray) { | ||
| return false; | ||
| } | ||
|
|
||
| // 배열인 경우 | ||
| if (aIsArray && bIsArray) { | ||
| return deepEqualsArrays(a, b); | ||
| } | ||
|
|
||
| // 객체인 경우 | ||
| if (typeUtils.isObject(a) && typeUtils.isObject(b)) { | ||
| return deepEqualsObjects(a, b); | ||
| } | ||
|
|
||
| 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. ??마지막에 모든 조건 다 지나가고 무조건 true 값을 던져주네요?? |
||
| }; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,32 @@ | ||
| import { deepEquals } from "./deepEquals.ts"; | ||
|
|
||
| type ArrayValue = unknown[]; | ||
|
|
||
| export const deepEqualsArrays = (arrayA: ArrayValue, arrayB: ArrayValue) => { | ||
| // 참조가 같으면 true | ||
| if (arrayA === arrayB) { | ||
| return true; | ||
| } | ||
|
|
||
| // 둘 중 하나라도 null 또는 undefined면 false | ||
| if (!arrayA || !arrayB) { | ||
|
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 false; | ||
| } | ||
|
|
||
| const arrayALength = arrayA.length; | ||
| const arrayBLength = arrayB.length; | ||
|
|
||
| // 길이가 다르면 false | ||
| if (arrayALength !== arrayBLength) { | ||
| return false; | ||
| } | ||
|
|
||
| for (let i = 0; i < arrayALength; i++) { | ||
| // 각 요소를 재귀적으로 비교 | ||
| if (!deepEquals(arrayA[i], arrayB[i])) { | ||
| return false; | ||
| } | ||
| } | ||
|
|
||
| return true; | ||
| }; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,41 @@ | ||
| import { deepEquals } from "./deepEquals.ts"; | ||
|
|
||
| type ObjectValue = Record<string, unknown>; | ||
|
|
||
| export default function deepEqualsObjects(objectA: ObjectValue, objectB: ObjectValue): boolean { | ||
| // 참조가 같으면 true | ||
| if (objectA === objectB) { | ||
| return true; | ||
| } | ||
|
|
||
| // 둘 중 하나라도 null 또는 undefined면 false | ||
| if (!objectA || !objectB) { | ||
| return false; | ||
| } | ||
|
|
||
| const objectAKeys = Object.keys(objectA); | ||
| const objectBKeys = Object.keys(objectB); | ||
| const objectALength = objectAKeys.length; | ||
| const objectBLength = objectBKeys.length; | ||
|
|
||
| // 키가 개수가 다르면 false | ||
| if (objectALength !== objectBLength) { | ||
| return false; | ||
| } | ||
|
|
||
| for (let i = 0; i < objectALength; i++) { | ||
|
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. 반복문이 deepEquals를 호출을 안하네요? if (!objectB.hasOwnProperty(key)) {
return false;
}
if (!deepEquals(objectA[key], objectB[key])) {
return false;
}이게 빠진것같은데...맞을까요?? |
||
| const key = objectAKeys[i]; | ||
|
|
||
| // B에 해당 key가 없으면 다름 | ||
| if (!Object.hasOwn(objectB, key)) { | ||
| return false; | ||
| } | ||
|
|
||
| // 각 요소를 재귀적으로 비교 | ||
| if (!deepEquals(objectA[key], objectB[key])) { | ||
| return false; | ||
| } | ||
| } | ||
|
|
||
| return true; | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,3 +1,28 @@ | ||
| import shallowEqualsArrays from "./shallowEqualsArrays.ts"; | ||
| import shallowEqualsObjects from "./shallowEqualsObjects.ts"; | ||
| import { typeUtils } from "../utils/typeUtil.ts"; | ||
|
|
||
| export const shallowEquals = (a: unknown, b: unknown) => { | ||
| return a === b; | ||
| // 원시 타입인 경우 | ||
| if (typeUtils.isPrimitive(a) || typeUtils.isPrimitive(b)) { | ||
| return a === b; | ||
| } | ||
|
|
||
| const aIsArray = Array.isArray(a); | ||
| const bIsArray = Array.isArray(b); | ||
|
|
||
| // 비교하려는 값의 타입이 다르면 false | ||
| if (aIsArray !== bIsArray) { | ||
| return false; | ||
| } | ||
|
|
||
| // 배열인 경우 | ||
| if (aIsArray && bIsArray) { | ||
| return shallowEqualsArrays(a, b); | ||
| } | ||
|
|
||
| // 객체인 경우 | ||
| if (typeUtils.isObject(a) && typeUtils.isObject(b)) { | ||
| return shallowEqualsObjects(a, b); | ||
| } | ||
| }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,30 @@ | ||
| type ArrayValue = unknown[]; | ||
|
|
||
| export default function shallowEqualsArrays(arrayA: ArrayValue, arrayB: ArrayValue) { | ||
| // 참조가 같으면 true | ||
| if (arrayA === arrayB) { | ||
| return true; | ||
| } | ||
|
|
||
| // 둘 중 하나라도 null 또는 undefined면 false | ||
| if (!arrayA || !arrayB) { | ||
|
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. 인자값 받을떄 배열로 받고 null값으로 받을수가 있나요?? |
||
| return false; | ||
| } | ||
|
|
||
| const arrayALength = arrayA.length; | ||
| const arrayBLength = arrayB.length; | ||
|
|
||
| // 길이가 다르면 false | ||
| if (arrayALength !== arrayBLength) { | ||
| return false; | ||
| } | ||
|
|
||
| for (let i = 0; i < arrayALength; i++) { | ||
| // 값이 다르면 false | ||
| if (arrayA[i] !== arrayB[i]) { | ||
| return false; | ||
| } | ||
| } | ||
|
|
||
| return true; | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,34 @@ | ||
| type ObjectValue = Record<string, unknown>; | ||
|
|
||
| export default function shallowEqualsObjects(objectA: ObjectValue, objectB: ObjectValue): boolean { | ||
| // 참조가 같으면 true | ||
| if (objectA === objectB) { | ||
| return true; | ||
| } | ||
|
|
||
| // 둘 중 하나라도 null 또는 undefined면 false | ||
| if (!objectA || !objectB) { | ||
| return false; | ||
| } | ||
|
|
||
| const objectAKeys = Object.keys(objectA); | ||
| const objectBKeys = Object.keys(objectB); | ||
| const objectALength = objectAKeys.length; | ||
| const objectBLength = objectBKeys.length; | ||
|
|
||
| // 키가 개수가 다르면 false | ||
| if (objectALength !== objectBLength) { | ||
| return false; | ||
| } | ||
|
|
||
| for (let i = 0; i < objectALength; i++) { | ||
| const key = objectAKeys[i]; | ||
|
|
||
| // 값이 다르거나, B에 해당 key가 없으면 다름 | ||
| if (objectA[key] !== objectB[key] || !Object.hasOwn(objectB, key)) { | ||
|
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. 이부분 생각보다 순서가 중요해서 key가 없는데 접근해서 undefined !== undefined로 통과될 수 있어요 if (!Object.hasOwn(objectB, key) || objectA[key] !== objectB[key]) {
return false;
}이렇게 구성하기를 권장드려요 |
||
| return false; | ||
| } | ||
| } | ||
|
|
||
| return true; | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,5 +1,7 @@ | ||
| import type { FunctionComponent } from "react"; | ||
| import { memo } from "./memo.ts"; | ||
| import { deepEquals } from "../equals"; | ||
|
|
||
| export function deepMemo<P extends object>(Component: FunctionComponent<P>) { | ||
| return Component; | ||
| return memo(Component, deepEquals); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,2 +1,2 @@ | ||
| export * from "./deepMemo"; | ||
| export * from "./memo"; | ||
| export * from "./memo.ts"; |
|
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. LGTM 👍 주석이 잘 달려 잇어서 확인이 편하네요 |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,6 +1,27 @@ | ||
| import { type FunctionComponent } from "react"; | ||
| import type { FunctionComponent, ReactNode } from "react"; | ||
| import { shallowEquals } from "../equals"; | ||
| import { useRef } from "../hooks"; | ||
|
|
||
| export function memo<P extends object>(Component: FunctionComponent<P>, equals = shallowEquals) { | ||
| return Component; | ||
| return function MemoizedComponent(props: P) { | ||
| const prevPropsRef = useRef<P | null>(null); | ||
| const prevComponentRef = useRef<ReactNode | Promise<ReactNode> | 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. Promise 로 들어가야되는 이유가 혹시 잇을까요??? |
||
|
|
||
| // 첫 렌더링이면, 이전 컴포넌트를 생성하고 반환한다. | ||
| if (!prevPropsRef.current) { | ||
| prevPropsRef.current = props; | ||
| prevComponentRef.current = Component({ ...props }); | ||
| return prevComponentRef.current; | ||
| } | ||
|
|
||
| // props가 변경되지 않았다면, 이전 컴포넌트를 반환한다. | ||
| if (equals(prevPropsRef.current, props)) { | ||
| return prevComponentRef.current; | ||
| } | ||
|
|
||
| // props가 변경되었으므로, 이전 컴포넌트를 삭제하고 새로운 컴포넌트를 생성한다. | ||
| prevPropsRef.current = props; | ||
| prevComponentRef.current = Component({ ...props }); | ||
| return prevComponentRef.current; | ||
| }; | ||
| } | ||
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.
상수님도
useAutoCallback으로 관리를 해주셨군요유열님이 구성해주신 React 내장 객체 useCallBack도 한번 확인하면 좋을 것 같아요!
https://github.com/hanghae-plus/front_6th_chapter1-3/pull/15/files