-
Notifications
You must be signed in to change notification settings - Fork 56
[9팀 임두현] Chapter 1-3. React, Beyond the Basics #45
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
d8192aa
fa9f515
1c3bc22
72a30b9
9f2fd6d
d6ed426
a481884
f0cb539
8e0e270
cf2b239
d40bf27
f2f0811
99409a4
70a54d0
737de72
532ddf9
29be8f0
4f5a9fe
40ce625
69b5463
501a59f
faf11ee
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 |
|---|---|---|
|
|
@@ -25,3 +25,5 @@ dist-ssr | |
| /test-results/ | ||
| /playwright-report/ | ||
| /coverage/ | ||
|
|
||
| /docs | ||
|
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. 저는 depth라는 인자를 추가해서 비교의 깊이를 조절할 수 있게 만들었어요. 이 depth 값을 재귀적으로 1씩 줄여나가다가 0이 되면 === 비교로 마무리하는 방식입니다. 이렇게 하면 deepEquals와 shallowEquals를 하나의 함수로 통합할 수 있는 장점이 있습니다. 예를 들어, shallowEquals(a, b)는 내부적으로 deepEquals(a, b, 1)을 호출하는 거죠. 이런 방식을 고려한다면 중복되는 구조를 깔끔하게 해결할 수 있을 것 같아요!
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. 저도 코드 중복과 명시적인 구조 사이에서 고민이 되었어요. 물론 코드 중복을 시도해 보았지만 지수님의 피드백과 같은 방법을 생각해내지 못하여 강제적으로 후자를 택하게 되었습니다,,,ㅎ |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,3 +1,18 @@ | ||
| export const deepEquals = (a: unknown, b: unknown) => { | ||
| return a === b; | ||
| }; | ||
| export function deepEquals(objA: unknown, objB: unknown): boolean { | ||
| if (objA === objB) return true; | ||
|
|
||
| if (objA == null || objB == null || typeof objA !== "object" || typeof objB !== "object") return false; | ||
|
|
||
| const keysOfA = Object.keys(objA); | ||
| const keysOfB = Object.keys(objB); | ||
|
|
||
| if (keysOfA.length !== keysOfB.length) return false; | ||
|
|
||
| for (const key of keysOfA) { | ||
| const valueA = (objA as Record<string, unknown>)[key]; | ||
| const valueB = (objB as Record<string, unknown>)[key]; | ||
| if (!deepEquals(valueA, valueB)) return false; | ||
| } | ||
|
Comment on lines
+11
to
+15
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. 말씀 들어보고 생각해보니 나눠서 처리하는게 더 정교하고 부작용도 없겠네요. |
||
|
|
||
| return true; | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,3 +1,20 @@ | ||
| export const shallowEquals = (a: unknown, b: unknown) => { | ||
| return a === b; | ||
| }; | ||
| export function shallowEquals(objA: unknown, objB: unknown): boolean { | ||
| if (objA === objB) return true; | ||
|
|
||
| if (objA == null || objB == null || typeof objA !== "object" || typeof objB !== "object") return false; | ||
|
|
||
| const keysOfA = Object.keys(objA); | ||
| const keysOfB = Object.keys(objB); | ||
|
|
||
| if (keysOfA.length !== keysOfB.length) return false; | ||
|
Comment on lines
+2
to
+9
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. 요런 중복은 별도의 유틸함수로 빼도 괜찮은 것 같아요! export const compareObjectProperties = (a: ObjectType, b: ObjectType, equals: Equals) => {
const aKeys = Object.keys(a);
const bKeys = Object.keys(b);
if (aKeys.length !== bKeys.length) return false;
return aKeys.every((key) => equals(a[key], b[key]));
}; |
||
|
|
||
| for (const key of keysOfA) { | ||
| const valueA = (objA as Record<string, unknown>)[key]; | ||
| const valueB = (objB as Record<string, unknown>)[key]; | ||
| if (valueA !== valueB) { | ||
| return false; | ||
| } | ||
| } | ||
|
|
||
| return true; | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,5 +1,6 @@ | ||
| import type { FunctionComponent } from "react"; | ||
| import { memo, type FunctionComponent } from "react"; | ||
| import { deepEquals } from "../equals"; | ||
|
|
||
| export function deepMemo<P extends object>(Component: FunctionComponent<P>) { | ||
| return Component; | ||
| return memo<P>(Component, deepEquals); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,6 +1,20 @@ | ||
| import { type FunctionComponent } from "react"; | ||
| import { createElement, type FunctionComponent, type ReactElement } 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 MemorizedComponent(props: P) { | ||
| const propsRef = useRef<P | null>(null); | ||
| const componentRef = useRef<ReactElement | null>(null); | ||
|
|
||
| const isInitialRender = propsRef.current === null; | ||
| const propsChanged = !equals(propsRef.current, props); | ||
|
|
||
| if (isInitialRender || propsChanged) { | ||
| propsRef.current = props; | ||
| componentRef.current = createElement(Component, props); | ||
| } | ||
|
|
||
| return componentRef.current; | ||
| }; | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -3,5 +3,12 @@ import { useCallback } from "./useCallback"; | |
| import { useRef } from "./useRef"; | ||
|
|
||
| export const useAutoCallback = <T extends AnyFunction>(fn: T): T => { | ||
| return fn; | ||
| const fnRef = useRef<T>(fn); | ||
| fnRef.current = fn; | ||
|
Comment on lines
+6
to
+7
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. 저는 if 문을 추가해서 이전 callback과 현재 callback을 비교하는 과정을 추가했었는데 지금와서 생각해보면 과한 로직인 것 같아요. |
||
|
|
||
| const memoizedCallback = useCallback((...args: Parameters<T>) => { | ||
| return fnRef.current(...args); | ||
| }, []); | ||
|
|
||
| return memoizedCallback as T; | ||
| }; | ||
| 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 */ | ||
| import type { DependencyList } from "react"; | ||
| /* eslint-disable react-hooks/exhaustive-deps */ | ||
| /* 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; | ||
| 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,21 @@ | ||
| /* eslint-disable @typescript-eslint/no-unused-vars */ | ||
| 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(); | ||
| interface MemoCache<T> { | ||
| deps: DependencyList | undefined; | ||
| value: T; | ||
| } | ||
|
|
||
| export function useMemo<T>(factory: () => T, deps: DependencyList, equals = shallowEquals): T { | ||
| const ref = useRef<MemoCache<T> | null>(null); | ||
|
|
||
| if (ref.current?.deps && equals(ref.current.deps, deps)) return ref.current.value; | ||
|
|
||
| ref.current = { | ||
| deps: deps, | ||
| value: factory(), | ||
| }; | ||
|
|
||
| return ref.current.value; | ||
| } |
|
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. 값을 저장하고 자유롭게 수정하는 MutableRefObject를 완벽하게 재현하고 있는 것 같아요. 그리고 저도 리뷰를 달다가 생각난건데 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. 오호이 지수님 좋은 의견이네요 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. 두현님 멋집니다,, PR에서 useRef의 동작 원리에 대해 깊게 고민하신거 인상 깊게 읽었어요..! useState로 useRef의 참조 안정성을 구현하신게 재밌었습니다. useState가 컴포넌트 라이프사이클 동안에 참조를 유지해준다는 걸 잘 활용하신 것 같아요. (저는 이런 생각 몬했을듯) 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. 찾아보니 실제 useRef는 내부적으로 컴포넌트 인스턴스에 값을 저장하는 방식으로 동작한다고 합니다. 리렌더링이 발생해도 해당 컴포넌트 인스턴스는 유지돼서 저장된 ref 객체 또한 동일한 참조를 유지하게 된다고 하는데 현재 useState를 사용하신 구현 방식 외에 다른 방식으로도 useRef의 핵심 기능을 구현해볼 수 있지 않을까 싶어요! |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,4 +1,11 @@ | ||
| export function useRef<T>(initialValue: T): { current: T } { | ||
| // useState를 이용해서 만들어보세요. | ||
| return { current: initialValue }; | ||
| import { useState } from "react"; | ||
|
|
||
| interface MutableRefObject<T> { | ||
| current: T; | ||
| } | ||
|
|
||
| export function useRef<T = undefined>(): MutableRefObject<T | undefined>; | ||
| export function useRef<T>(initialValue: T): MutableRefObject<T>; | ||
| export function useRef<T>(initialValue?: T): MutableRefObject<T | undefined> | MutableRefObject<T> { | ||
|
Comment on lines
+7
to
+9
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 useState(() => ({ current: initialValue }))[0]; | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,7 +1,15 @@ | ||
| import { useState } from "react"; | ||
| import { useCallback, useState } 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 | (() => T)): [T, (newValue: T) => void] => { | ||
| const [state, setState] = useState<T>(initialValue); | ||
|
|
||
| const setShallowState = useCallback((newValue: T) => { | ||
| setState((currentValue) => { | ||
| if (shallowEquals(currentValue, newValue)) return currentValue; | ||
| else return newValue; | ||
| }); | ||
|
Comment on lines
+8
to
+11
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. 저는 setState 바깥에서 새로운 값을 계산했는데, setState 콜백 안에서 prevValue를 사용해야 여러 번의 상태 업데이트가 연달아 일어날 때도 stale state 문제 없이 항상 최신 상태를 기반으로 값을 계산할 수 있겠네요. 제가 놓쳤던 부분인데 다른 분들은 다 꼼꼼히 챙기셔서 약간 슬퍼졌어요... |
||
| }, []); | ||
|
|
||
| return [state, setShallowState]; | ||
| }; | ||
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.
과제의 요구사항에 맞게 정확히 State와 Command 컨텍스트를 분리하신 것 같아요! 추가로 제가 평소에 즐겨 사용하는
createSafeContext라는 패턴을 소개해 드리고 싶어요.createContext를 사용할 때마다 초기값을 반드시 설정해야 하거나Provider외부에서 컨텍스트를 사용하는 실수를 방지하기 위해null체크를 하고 값을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.
우라까이하겠습니다