-
Notifications
You must be signed in to change notification settings - Fork 56
[6팀 정용준] Chapter 1-3. React, Beyond the Basics #40
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
f8045aa
6b6a524
d313784
a683aa3
40dc996
58e135f
bb47b7c
c2d526d
ec04970
1e41ca7
e0021cf
7f2441f
66fe47b
ce447b9
d650650
e80b884
9a288da
395c38a
062575f
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 |
|---|---|---|
| @@ -1,3 +1,22 @@ | ||
| export const deepEquals = (a: unknown, b: unknown) => { | ||
| import { getObjectKeys, isArray, isObject, isPrimitive } from "./utils"; | ||
|
|
||
| const comparePrimitive = (a: unknown, b: unknown) => Object.is(a, b); | ||
|
|
||
| export const deepEquals = (a: unknown, b: unknown): boolean => { | ||
| if (isPrimitive(a) && isPrimitive(b)) { | ||
| return comparePrimitive(a, b); | ||
| } | ||
|
|
||
| if (isArray(a) && isArray(b)) { | ||
| return a.length === b.length && a.every((item, index) => deepEquals(item, b[index])); | ||
| } | ||
|
|
||
| if (isObject(a) && isObject(b)) { | ||
| const keysA = getObjectKeys(a); | ||
| const keysB = getObjectKeys(b); | ||
|
|
||
| return keysA.length === keysB.length && keysA.every((key) => deepEquals(a[key], b[key])); | ||
| } | ||
|
|
||
| return a === b; | ||
| }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,3 +1,22 @@ | ||
| import { getObjectKeys, isArray, isObject, isPrimitive } from "./utils"; | ||
|
|
||
| const comparePrimitive = (a: unknown, b: unknown) => Object.is(a, b); | ||
|
|
||
| export const shallowEquals = (a: unknown, b: unknown) => { | ||
| if (isPrimitive(a) && isPrimitive(b)) { | ||
| return comparePrimitive(a, b); | ||
| } | ||
|
|
||
| if (isArray(a) && isArray(b)) { | ||
| return a.length === b.length && a.every((item, index) => comparePrimitive(item, b[index])); | ||
| } | ||
|
|
||
| if (isObject(a) && isObject(b)) { | ||
| const keysA = getObjectKeys(a); | ||
| const keysB = getObjectKeys(b); | ||
|
|
||
| return keysA.length === keysB.length && keysA.every((key) => comparePrimitive(a[key], b[key])); | ||
| } | ||
|
|
||
| return a === b; | ||
| }; |
|
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로 타입 유틸 함수들을 잘 분리해두셔서 전체적인 가독성과 재사용성이 좋아진 것 같아요! |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,14 @@ | ||
| export type Primitive = string | number | boolean | null | undefined | bigint | symbol; | ||
|
|
||
| export const isArray = (value: unknown): value is unknown[] => Array.isArray(value); | ||
|
|
||
| export const isObject = (value: unknown): value is object => typeof value === "object" && value !== 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. 객체를 판단하는 로직에서 엣지 케이스가 있을 것 같아요!
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. 민재님 말씀대로 Plain object로 의도하고 작성해야겠네요. 혹시 제가 얘기한게 민재님 리뷰의 의도와 맞을까요? |
||
|
|
||
| export const isPrimitive = (value: unknown): value is Primitive => typeof value !== "object" || value === null; | ||
|
|
||
| export const getObjectKeys = <T extends object>(o: T): (keyof T)[] => Object.keys(o) as (keyof T)[]; | ||
|
|
||
| // eslint-disable-next-line @typescript-eslint/no-explicit-any | ||
| export function isFunction(value: any): value is (...args: any[]) => any { | ||
| return typeof value === "function"; | ||
| } | ||
|
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 파일로 함수화시켜서 적용하신 것 정말 좋네요! 저도 이렇게 별도 파일을 생성해서 가독성을 높여 리팩토링 해봐야겠습니다. 👍 |
||
|
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. 용준님이 구현하신 memo를 통해서 deepmemo를 간단하게 구현하는 방법으로 수정하면 좋을 것 같아요. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,5 +1,21 @@ | ||
| import type { FunctionComponent } from "react"; | ||
| import { useRef, type FunctionComponent } from "react"; | ||
| import { deepEquals } from "../equals"; | ||
|
|
||
| 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<typeof Component> | null>(null); | ||
|
|
||
| if (prevPropsRef.current && deepEquals(prevPropsRef.current, props)) { | ||
| return prevResultRef.current!; | ||
| } | ||
|
|
||
| const result = Component(props); | ||
| prevPropsRef.current = props; | ||
| prevResultRef.current = result; | ||
|
|
||
| return result; | ||
| }; | ||
|
|
||
| return MemoizedComponent; | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,6 +1,21 @@ | ||
| import { type FunctionComponent } from "react"; | ||
| import { useRef, type FunctionComponent } from "react"; | ||
| import { shallowEquals } from "../equals"; | ||
|
|
||
| export function memo<P extends object>(Component: FunctionComponent<P>, equals = shallowEquals) { | ||
| return Component; | ||
| const MemoizedComponent = (props: P) => { | ||
| const prevPropsRef = useRef<P | null>(null); | ||
| const prevResultRef = useRef<ReturnType<typeof Component> | null>(null); | ||
|
|
||
| if (prevPropsRef.current && equals(prevPropsRef.current, props)) { | ||
| return prevResultRef.current!; | ||
| } | ||
|
|
||
| const result = Component(props); | ||
|
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. 이 부분에서 prevReulstRef.current를 return 시키지 않고 result를 return 시키시는 이유가 궁금합니다! |
||
| prevPropsRef.current = props; | ||
| prevResultRef.current = result; | ||
|
|
||
| return result; | ||
| }; | ||
|
|
||
| return MemoizedComponent; | ||
| } | ||
| 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 react-hooks/exhaustive-deps */ | ||
| import type { DependencyList } from "react"; | ||
| import { useMemo } from "./useMemo"; | ||
| import type { AnyFunction } from "../types"; | ||
|
|
||
| export function useCallback<T extends Function>(factory: T, _deps: DependencyList) { | ||
| // 직접 작성한 useMemo를 통해서 만들어보세요. | ||
| return factory as T; | ||
| export function useCallback<T extends AnyFunction>(factory: T, deps: DependencyList): T { | ||
| const memoized = useMemo(() => factory, [...deps]); | ||
|
|
||
| return memoized; | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,8 +1,17 @@ | ||
| /* eslint-disable @typescript-eslint/no-unused-vars */ | ||
| import type { DependencyList } from "react"; | ||
| import { useRef } from "./useRef"; | ||
| import { shallowEquals } from "../equals"; | ||
|
|
||
| export function useMemo<T>(factory: () => T, _deps: DependencyList, _equals = shallowEquals): T { | ||
| // 직접 작성한 useRef를 통해서 만들어보세요. | ||
| return factory(); | ||
| export function useMemo<T>(factory: () => T, deps: DependencyList, _equals = shallowEquals): T { | ||
| const memoized = useRef<T | undefined>(undefined); | ||
| const prevDeps = useRef(deps); | ||
| const isInitial = useRef(true); | ||
|
|
||
| if (isInitial.current || !_equals(prevDeps.current, deps)) { | ||
| isInitial.current = false; | ||
| memoized.current = factory(); | ||
| prevDeps.current = deps; | ||
| } | ||
|
Comment on lines
+6
to
+14
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. memoized가 undefined 값이면 최초 렌더링 값이지 않을까요?
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. useMemo에서 undefined를 반환하면 실제 리액트에서는 아마 에러가 날텐데 일단 factory에도 undefined를 반환하는 경우를 생각해서 isInitial.current로 체킹하고 있어요. 반환값이 undefined면 아무래도 순수한 로직이 아니라는 뜻일테니 isInitial을 지우고 factory 반환값이 undefined면 에러를 뱉도록 하는게 더 맞는 것 같네요. 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 memoized.current!; | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,4 +1,9 @@ | ||
| import { useState } from "react"; | ||
|
|
||
| export function useRef<T>(initialValue: T): { current: T } { | ||
| // useState를 이용해서 만들어보세요. | ||
| return { current: initialValue }; | ||
| const [ref] = useState(() => ({ | ||
| current: initialValue, | ||
| })); | ||
|
|
||
| return ref; | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,7 +1,19 @@ | ||
| import { useState } from "react"; | ||
| /* eslint-disable react-hooks/exhaustive-deps */ | ||
| import { useCallback, useState } from "react"; | ||
| import { shallowEquals } from "../equals"; | ||
| import { isFunction } from "../equals/utils"; | ||
|
|
||
| export const useShallowState = <T>(initialValue: Parameters<typeof useState<T>>[0]) => { | ||
| // useState를 사용하여 상태를 관리하고, shallowEquals를 사용하여 상태 변경을 감지하는 훅을 구현합니다. | ||
| return useState(initialValue); | ||
| export const useShallowState = <T>(initialValue: T | (() => T)) => { | ||
| const [state, setState] = useState<T>(initialValue); | ||
|
|
||
| const setShallowState = useCallback((value: T | ((prev: T) => T)) => { | ||
| const nextValue = isFunction(value) ? value(state) : value; | ||
| if (shallowEquals(state, nextValue)) { | ||
| return; | ||
| } | ||
|
|
||
| setState(nextValue); | ||
| }, []); | ||
|
|
||
| return [state, setShallowState] 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.
useToastCommand 와 useToastState 안에서도 useMemo 처리를 해주신 이유가 궁금합니다.
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.
처음에 저기 넣었다가 미처 못지웠네요. 체크 감사합니다.