Skip to content

[6팀 이민재] Chapter 1-3. React, Beyond the Basics #25

Open
minjaeleee wants to merge 24 commits intohanghae-plus:mainfrom
minjaeleee:main
Open

[6팀 이민재] Chapter 1-3. React, Beyond the Basics #25
minjaeleee wants to merge 24 commits intohanghae-plus:mainfrom
minjaeleee:main

Conversation

@minjaeleee
Copy link

@minjaeleee minjaeleee commented Jul 20, 2025

과제 체크포인트

https://minjaeleee.github.io/front_6th_chapter1-3

배포 링크

기본과제

equalities

  • shallowEquals 구현 완료
  • deepEquals 구현 완료

hooks

  • useRef 구현 완료
  • useMemo 구현 완료
  • useCallback 구현 완료
  • useDeepMemo 구현 완료
  • useShallowState 구현 완료
  • useAutoCallback 구현 완료

High Order Components

  • memo 구현 완료
  • deepMemo 구현 완료

심화 과제

hooks

  • createObserver를 useSyncExternalStore에 사용하기 적합한 코드로 개선
  • useShallowSelector 구현
  • useStore 구현
  • useRouter 구현
  • useStorage 구현

context

  • ToastContext, ModalContext 개선

과제 셀프회고

기술적 성장

평소 참조 동일성에 대해서 깊이 이해하지 않았습니다.(= 그동안 대충 아는 ‘척’만 하고 중요한 사실은 스윽 넘겼다…) 메모이제이션이나 hooks에서 사용하는 의존성 배열에 해당하는 값들은 항상 참조 동일성을 유지하게 되고 렌더링 부분에서 즉시 비용 감소로 여겼기 때문입니다.
사실 이 부분을 유심히 고민해보면 저는 리액트의 얕은 비교를 통한 렌더링 원리에 대해서 부족했던 것 같습니다. 따라서, 다시 이 부분을 따라가서 정리해보았습니다.

참조 동일성이란?

function MyComponent() {
  const config = { darkMode: true };

  useEffect(() => {
    console.log("config changed!");
  }, [config]);

  return null;
}
  • 이 컴포넌트가 리렌더링될 때마다 config 객체는 새로 만들어집니다.
  • 값은 같아 보여도 참조가 다르기 때문에 useEffect는 매번 실행이 됩니다.
    ⇒ 이것이 바로 “모든 객체는 리렌더링 때 재생성되고, 참조가 다르다.” 라는 의미입니다.

참조 동일성을 유지하지 못하는 것이 어떤 문제를 유발하나?

과제를 진행하면서 memo, useMemo, useAutoCallback 훅을 작성한 코드를 보면 이해할 수 있습니다.

// memo.ts

import { type FunctionComponent } from "react";
import { shallowEquals } from "../equals";
import { useRef } from "../hooks";

// memo HOC는 컴포넌트의 props를 얕은 비교하여 불필요한 렌더링을 방지합니다.
export function memo<P extends object>(Component: FunctionComponent<P>, equals = shallowEquals): FunctionComponent<P> {
  // 메모이제이션된 컴포넌트 생성
  const MemoizedComponent: FunctionComponent<P> = (props) => {
    // 1. 이전 props를 저장할 ref 생성
    const memoizedRef = useRef<{
      prevProps: P | null;
      rendered: ReturnType<FunctionComponent<P>> | null;
    }>({
      // 이전 props 저장
      prevProps: null,
      // 이전 JSX 저장
      rendered: null,
    });

    // 3. eqauls 함수를 사용하여 props 비교 - 새롭게 컴포넌트 렌더링X
    if (memoizedRef.current.prevProps !== null && equals(memoizedRef.current.prevProps, props)) {
      return memoizedRef.current.rendered!;
    }

    // 4. props가 변경된 경우에만 새로운 렌더링 수행
    memoizedRef.current.prevProps = props;
    memoizedRef.current.rendered = Component(props);

    return memoizedRef.current.rendered;
  };

  return MemoizedComponent;
}

memo는 props를 shallowEqual(얕은 비교)를 수행하고 다르면 리렌더링을 하고 같으면 리렌더링을 막습니다.
그런데 config 객체는 매번 새로 생성되므로 얕은 비교에서 false를 반환하고 리렌더링을 하게 됩니다.

결국엔 컴포넌트는 매번 리렌더링되고 memo의 효과는 없어집니다.
이러한 방식이 반복되고 구조가 비대해지다보면 불필요한 렌더링과 성능저하로 이어지겠죠.

참조 동일성을 안정화하는 방법

참조 동일성을 안정화하는 방법의 두 가지를 배웠습니다.

첫 번째, useRef를 사용하는 것입니다.
값이 변하지 않거나, 값이 바뀌어도 리렌더링을 유발하지 않아야 할 때 사용할 수 있습니다.
다만 객체, 함수, DOM 노드 등 “값은 유지하지만 UI와 무관”한 상태에만 사용해야 합니다. 이는 리액트 공식문서 useRef 사용 주의사항에도 볼 수 있습니다.

두 번째, 모든 의존성 값들을 useMemo나 useCallback으로 감싸 안정화하는 것입니다.
이번 과제의 ToastProvider를 이렇게 값을 안정화하여 사용했습니다.
createActions 함수를 매번 재실행하지 않고 useMemo를 사용하여 참조의 안정성을 확보했고, hideAfter 함수 역시 hide에 의존한 메모이제이션을 해주어 일관된 참조를 보장하도록 구성했습니다.

// ToastProvider.tsx

export const ToastProvider = memo(({ children }: PropsWithChildren) => {
  const [state, dispatch] = useReducer(toastReducer, initialState);
  const { show, hide } = useMemo(() => createActions(dispatch), [dispatch]);
  const visible = state.message !== "";

  const hideAfter = useMemo(() => debounce(hide, DEFAULT_DELAY), [hide]);

  const showWithHide = useAutoCallback((message: string, type: ToastType) => {
    show(message, type);
    hideAfter();
  });

  const commandValue = useMemo(() => ({ show: showWithHide, hide }), [showWithHide, hide]);
  const stateValue = useMemo(() => ({ message: state.message, type: state.type }), [state.message, state.type]);

  return (
    <ToastCommandContext.Provider value={commandValue}>
      <ToastStateContext.Provider value={stateValue}>
        {children}
        {visible && createPortal(<Toast />, document.body)}
      </ToastStateContext.Provider>
    </ToastCommandContext.Provider>
  );
});

이런 과정들을 통해서 얕은 비교에서 참조 동일성이 왜 중요한지를 이해하고, 리액트의 렌더링 원리에 대해서 더 깊게 이해할 수 있었습니다.

자랑하고 싶은 코드

얕은 비교 - 객체 순수 비교

처음에는 객체 비교를 하기 위해서 아래와 같이 type으로 구분을 했습니다. 물론, 테스트는 통과했지만 사실 자바스크립트에서 typeof value === “object” 라는 문자열로 반환되는 경우는 생각보다 많습니다. 배열, function, null, new Date(), new RegExp(), document.body …

image
 if (a && b && typeof a === "object" && typeof b === "object" && !Array.isArray(a) && !Array.isArray(b)) {
    const keysA = Object.keys(a);
    const keysB = Object.keys(b);

    if (keysA.length !== keysB.length) return false;

    for (const key of keysA) {
      if (!keysB.includes(key) || (a as Record<string, unknown>)[key] !== (b as Record<string, unknown>)[key])
        return false;
    }

    return true;
  }

따라서 isObject 함수로 순수 객체일 경우를 한 번 판단하고, Object.keys()로 직접 가진 key만 추출을 하고, Object.prototype,hasOwnProperty.call()로 해당 객체가 직접 소유하고 있는지 다시 판단하는 로직으로 변경하여 객체 여부에 대한 엣지 케이스를 보완하고 정밀도를 높였습니다.

const isObject = (val: unknown): val is Record<string, unknown> => {
    return typeof val === "object" && val !== null && !Array.isArray(val);
  };

  if (isObject(a) && isObject(b)) {
    const keysA = Object.keys(a);
    const keysB = Object.keys(b);

    if (keysA.length !== keysB.length) return false;

    for (const key of keysA) {
      if (!Object.prototype.hasOwnProperty.call(b, key)) return false;
      if (a[key] !== b[key]) return false; // 얕은 비교
    }

    return true;
  }

깊은비교 리팩토링

deepEquals 함수를 작성할 때에는, shallowEquals에서 depth가 추가된 배열이나 객체일 경우에만 재귀적으로 모든 depth에 대한 탐색 및 비교가 필요했습니다. 따라서, shallowEquals 함수를 가져와 이 부분을 추가해 주었습니다.

// 리팩토링 전
export const deepEquals = (a: unknown, b: unknown): boolean => {
  // 1. 기본 값 비교
  if (a === b || (Number.isNaN(a) && Number.isNaN(b))) return true;

  // 2. 배열 비교
  if (Array.isArray(a) && Array.isArray(b)) {
    // 배열의 length로 비교
    if (a.length !== b.length) return false;

    // 재귀적으로 모든 depth의 요소를 비교
    for (let i = 0; i < a.length; i++) {
      if (!deepEquals(a[i], b[i])) return false;
    }

    return true;
  }

  // 배열이 아닌데 한쪽만 배열이면 false
  if (Array.isArray(a) !== Array.isArray(b)) return false;

  // 3. 순수 객체 비교
  const isObject = (val: unknown): val is Record<string, unknown> => {
    return typeof val === "object" && val !== null && !Array.isArray(val);
  };

  if (isObject(a) && isObject(b)) {
    const keysA = Object.keys(a);
    const keysB = Object.keys(b);

    if (keysA.length !== keysB.length) return false;

    for (const key of keysA) {
      if (!Object.prototype.hasOwnProperty.call(b, key)) return false;
      if (!deepEquals(a[key], b[key])) return false;
    }

    return true;
  }

  // 기본 타입인데 === 실패했으면 false
  return false;
};

작성하고 보니, 기존 비교는 shallowEquals 함수를 실행시켜 비교시키고 재귀적으로 탐색 및 비교가 필요한 요소들에 대해서만 deepEquals 함수로 깊은 비교를 실행시키도록 리팩토링하여 불필요한 코드를 shallowEquals 함수로 재활용하면서 가독성을 높였습니다.

import { shallowEquals } from "./shallowEquals";

export const deepEquals = (a: unknown, b: unknown): boolean => {
  if (shallowEquals(a, b)) return true;

  // 배열인 경우 재귀 비교
  if (Array.isArray(a) && Array.isArray(b)) {
    if (a.length !== b.length) return false;
    for (let i = 0; i < a.length; i++) {
      if (!deepEquals(a[i], b[i])) return false;
    }
    return true;
  }

  const isObject = (val: unknown): val is Record<string, unknown> =>
    typeof val === "object" && val !== null && !Array.isArray(val);

  // 순수 객체 재귀 비교
  if (isObject(a) && isObject(b)) {
    const keysA = Object.keys(a);
    const keysB = Object.keys(b);
    if (keysA.length !== keysB.length) return false;

    for (const key of keysA) {
      if (!Object.prototype.hasOwnProperty.call(b, key)) return false;
      if (!deepEquals(a[key], b[key])) return false;
    }

    return true;
  }

  return false;
};

개선이 필요하다고 생각하는 코드

export function memo<P extends object>(Component: FunctionComponent<P>, equals = shallowEquals): FunctionComponent<P> {
  // 메모이제이션된 컴포넌트 생성
  const MemoizedComponent: FunctionComponent<P> = (props) => {
    // 1. 이전 props를 저장할 ref 생성
    const memoizedRef = useRef<{
      prevProps: P | null;
      rendered: ReturnType<FunctionComponent<P>> | null;
    }>({
      // 이전 props 저장
      prevProps: null,
      // 이전 JSX 저장
      rendered: null,
    });

    // 3. eqauls 함수를 사용하여 props 비교 - 새롭게 컴포넌트 렌더링X
    if (memoizedRef.current.prevProps !== null && equals(memoizedRef.current.prevProps, props)) {
      return memoizedRef.current.rendered!;
    }

    // 4. props가 변경된 경우에만 새로운 렌더링 수행
    memoizedRef.current.prevProps = props;
    memoizedRef.current.rendered = Component(props);

    return memoizedRef.current.rendered;
  };

  return MemoizedComponent;
}

memo 컴포넌트에서 equals 함수를 사용하여 props를 비교할 때 "새롭게 컴포넌트 렌더링을 시키지 않는다." 라는 의도로 Component 함수를 호출하지 않고 return해버렸는데, 이렇게 Component(props)를 조건부로 호출하는 방식이 문제가 생길 수도 있겠다는 생각이 들었습니다.
예를 들어서, 아래와 같이 memo로 감싸진 컴포넌트를 사용할 때 내부에서 hooks를 사용하게 되면 equals를 통한 변경이 일어나지 않았음에도 Component(props) 함수를 생략해버리니 React의 hook 규칙 위반 가능성이 될 수 있을것 같습니다.
이 부분에 대한 개선이 필요해보입니다!

const MyComponent = memo(function MyComponent(props) {
  const [count, setCount] = useState(0);
  useEffect(() => {
    console.log("effect 실행됨");
  }, []);

  return <div>{count}</div>;
});

학습 효과 분석

리액트의 렌더링 과정과 원리에 대해서 심층 깊게 이해한 시간이었습니다.
다만, 꽤 많은 내용을 학습했기 때문에 제 지식으로 온전히 습득하기 위한 과정을 다시 한번 스스로 거쳐야겠습니다.

리액트의 렌더링 과정과 원리에 대해서는 리액트의 공식문서와 도서, 문서 등으로 학습했고 면접과 같은 중요한 날 전에는 이를 달달 외우곤 했습니다.
하지만, 직접 학습하고 구현해보니 자연스레 외워지게 되었습니다.
예를 들어서, 2주차 때 학습한 과제에서 React element의 object 속성을 이해할 때 type, props, children 값들의 정의를 글로 외우다 보니 항상 몇 일 지나면 까먹었는데
이번에 직접 object로 트랜스파일링 하는 과정을 겪고, DOM에 반영하는 과정을 겪다보니 자연스레 type, props, children 값이 필요한 이유와 의미에 대해서 이해할 수 있었습니다.
또 리액트의 렌더링은 “얕은 비교를 수행하며 가상 DOM은 변경된 사항만 DOM에 직접 반영한다” 라는 의미를 워딩으로 외우다보니, 막상 얕은 비교는 어떻게 이루어지는지 참조 동일성은 어떻게 유지해야하는지 내부 원리에 대해서 정확히 이해하지 못하고 있었습니다.
자연스레 과제를 하면서 부족했던 부분을 확인하고 지식을 습득할 수 있었습니다.

결론은 직접 구현하면서 이해하는 과정이 있다보니 재미있게 학습할 수 있었고, 1~3주차의 과제를 통해서 리액트에 대한 깊은 이해를 더욱 할 수 있는 기반이 만들어진 것 같습니다.

과제 피드백

과제 학습을 수행하며 리액트와 자바스크립트에 대한 조금 더 많이 이해할 수 있게 되었고, 더 깊은 수준을 이해할 수 있는 중요한 디딤돌이 되었습니다.
또한, AI와 함께 과제를 수행하면서 빠르게 나의 코드를 구조화하고, 분석하고, 리팩토링을 할 수 있었습니다.

다만 요구하는 특정 기능을 “정답”에 가까운 코드로 만드는 것은 과제를 수행함에 있어서 사람마다 큰 습득이나 변별력이 없겠다는 생각이 들었습니다.
AI로 빠르고 일원화된 정답을 제출하는데, 코드를 작성하기 위해 고민한 시간과 노력들 그리고 의도와 목적들을 모두 표현하는게 더 중요하겠다는 생각이 들었습니다.
그래서 과제로서 이런 문제를 해결하는데는 어렵겠지만 경험하고 해결한 사례, 배경, 고민한 흔적들을 나눌 수 있을만한 과제나 시간이 있으면 더 좋을 것 같습니다.

아직.. 구체적인 예시까지는 생각은 못 해봤습니다.. ㅎ

학습 갈무리

리액트의 렌더링이 어떻게 이루어지는지 정리해주세요.

리액트의 렌더링 과정은 React Element 생성 → 가상 DOM 비교 (diffing) → 실제 DOM 반영 (commit) 세 가지 단계로 구성이됩니다.

1. JSX → React Element 생성
JSX는 React.createElement 호출로 변환되어 React Element라는 일반 Javascript 객체가 됩니다.

2. Reconciliation (조정 단계)
이전 렌더링 결과와 새로운 React Element를 비교하여 업데이트해야 할 변경점만 계산하게 됩니다. 이때, 이 과정을 통해 성능을 높이고 불필요한 DOM 업데이트를 방지합니다.

이때 2주차 때 구현한 updateElement와 비교하면 주요 규칙은 다음과 같은 것들이 있습니다.

- 타입(type)이 다르면 업데이트
    - 예) ```<div> → <span>```
    - 이전 노드 제거 + 새로운 노드 추가
- 타입이 같으면 속성만 비교
    - 예)) ```<div className=”a”> → <div className=”b”>```
    - DOM은 재사용되고, className만 업데이트
- Key 기반 리스트 비교
    - key를 기준으로 요소 재사용 여부를 판단

3. Commit Phase (실제 DOM 반영 단계)
앞서 수집된 변경사항을 기반으로 실제 DOM을 조작합니다.

메모이제이션에 대한 나의 생각을 적어주세요.

메모이제이션에 대한 생각을 먼저 표현하기 전에, 배경이 되는 리액트의 렌더링에 대한 생각을 먼저 정리하겠습니다.

리액트의 렌더링에 대한 나의 생각

리액트의 렌더링은 단순하고 예측 가능하다는 점에서 탁월하지만, 렌더링마다 함수 전체가 재실행되고 객체, 함수, 배열 등 모든 참조가 새로 만들어진다는 구조적인 특성은 개발자가 깊이 고민하고 다루어야할 부분이라고 생각합니다.
그래서 저는 이 구조를 단점으로 보지 않는 대신 변경을 감지하는 기준이 참조이기 때문에 개발자는 참조를 유지할 책임이 있다고 생각합니다. 즉, 의미있는 리렌더링을 의도할 책임이 있다고 생각합니다.
그렇기 때문에 리액트의 렌더링 원리를 정확하게 이해하는 것은 값의 참조를 안정화해서, 의미있는 UI의 변화가 반영될 수 있는 것이라고 생각합니다.

메모이제이션

먼저, 메모이제이션이란 어떤 연산의 결과를 캐싱해 두었다가, 동일한 입력이 들어오면 재계산하지 않고 캐시된 결과를 재사용하는 최적화 기법입니다.리액트의 메모이제이션은 제가 앞서 설명한 리액트의 렌더링의 구조적 단점을 보완할 수 있는 중요한 기술입니다.

단, 앞서 제가 얘기한 “리엑트의 렌더링 원리를 정확하게 이해한 상태에서 구현할 때” 라는 전제가 붙습니다.
React.memo, useMemo, useCallback 등은 참조의 동일성을 기반으로 동작하기 때문에 참조의 동일성을 지켜주지 않으면 오히려 불필요한 리렌더링, 불필요한 실행, 성능 저하로 이어지기 때문입니다.

컨텍스트와 상태관리에 대한 나의 생각을 적어주세요.

상태 관리란?

상태(state)는 UI가 반응해야 하는 데이터의 현재 모습을 의미합니다. 이 상태를 언제, 어디서, 어떻게 관리할지를 결정하는 것이프론트엔드 앱 설계에서는 가장 중요한 결정 중 하나입니다.
현재 상태관리 라이브러리로는 Redux, Zustand, Recoil 등이 주류를 이루고 있으며 React의 상태관리는 컴포넌트 내부에서 useState를 다루는 것이 일상이 되었습니다.
하지만, 상태의 복잡도보다 더 중요한 것은 “이 상태를 누가 알아야 하는가?” 즉, 컨텍스트의 경계 설정입니다.

컨텍스트의 역할

React의 Context는 컴포넌트 트리 어디에서든 데이터를 공급하고 구독할 수 있게 해줍니다.
그러나, Context 내부 상태가 자주 바뀌면 그 상태를 구독 중인 모든 컴포넌트가 불필요하게 렌더링이 발생됩니다. 따라서, 컨텍스트는 공유 상태의 목적으로 쓰는 것이 가장 적절하다고 생각합니다.

공유 상태(Shared State)

공유 상태란 둘 이상의 컴포넌트가 함께 사용하는 상태를 의미합니다.
앱 전역에서 일관된 정보를 관리하는 전역 상태의 목적과는 다르게 특정 UI흐름이나 기능 내에서 여러 컴포넌트 간 데이터를 공유하고 싶을 때, 제한된 범위 내에서 데이터를 공유하고 싶을 때 사용하는데 목적이 있습니다.

결론

따라서 상태 관리의 목적을 이해하고 설계하는 것이 중요하다고 생각이 됩니다. 공유 상태가 필요할 때는 컨텍스트, 전역 상태가 필요할 때는 전역 상태를 고려하는 것이 장,단점과 목적에 따른 적절한 사용 방법이라고 생각합니다.

리뷰 받고 싶은 내용

  1. 메모이제이션이 필요한 부분을 어떻게 평가하며 사용하고 계신지가 궁금합니다!
  • 과제를 진행하며 메모이제이션을 위한 참조 동일성을 보장하는 구조를 만들다 보니 depth가 깊어지거나 공유 상태 구조가 복잡해질수록 가독성도 떨어지고, 캐시 관련 유지비용이 더 생길 것 같습니다. 리액트에서도 최적화에 의한 확실한 이점이 있는 경우에만 사용할 것으로 명시가 되어 있는데.. 현업에서는 적용하기 애매한 포인트들이 여럿 있었는데, 주로 코치님께서는 어떤 상황에서 메모이제이션이 필요한 상황이라고 판단하고 의사결정했는지 간단하게라도 경험을 들려주실 수 있을까요?

minjaeleee and others added 24 commits July 20, 2025 20:10
1. 얕은 비교를 수행하는 shallowEquals 함수 구현
2. 순수 객체 비교 - 엣지 케이스를 고려
3. commit시 발생하는 lint 에러 주석 처리
1. shallowEqauls로 1dpeth 비교를 수행
2. deepEquals로 배열, 객체 모든 depth 탐색 및 비교를 수행
1.  shallowEquals 재활용해서 실행시키고, deepEquals 모든 요소 탐색 및 비교가 필요한 재귀 함수 로직만 실행시키며 가독성을 향상시킴
1. 이전 의존성과 결과를 저장할 ref 생성
2. 현재 의존성과 이전 의존성 비교
3. 의존성이 변경된 경우 factory 함수 실행 및 결과 저장
4. 메모이제이션된 값 반환
1. 직접 작성한 useMemo를 통한 useCallback 기능 구현
1. 직접 작성한 useMemo를 통한 useDeepMemo 기능 구현
1. shallowEquals로 비교
2. useCallback으로 setState를 고정하여 항상 같은 참조를 유지하게 함
3. resolveNextState 함수형 업데이트 처리
1. 콜백함수가 참조하는 값을 렌더링 시점에 최신화 시키는 기능
2. useCallback으로 항상 동일한 참조를 유지하게 시키는 기능
1. 이전 props를 저장할 ref 생성
2. equals 함수 비교를 통해 동일하면 이전 값 반환
3. equals 함수 비교를 통해 동일하지 않을 때만 렌더링 수행
4. 결국 메모이제이션된 컴포넌트 생성 및 반환 - hoc 구현
1. deepEquals 함수를 사용하여 props 비교
1. 순수 객체 비교시, a 객체의 key만 비교하는 로직이므로 깊은 비교를 수행할 때, 정상적인 기능을 하지 못하는 이슈 수정
1. 이전 상태를 저장하고, shallowEquals를 사용해 상태가 변경되었는지 확인하는 로직
1. 외부 상태 구독 방식 구현
2. shallow memoization을 결합한 방식으로 기능 구현
1. 외부 상태 구독 방식 구현
2. shallow memoization을 결합한 방식으로 기능 구현
1. command와 state context가 결합되어 있어서 불필요한 렌더링 유발됨. 이로 인한 context 분기 처리
1. 직접 만든 useAutoCallback과 useMemo 기능을 통해서 필요한 값과 함수들에 대해서 메모이제이션
@JHeeJinDev
Copy link

안녕하세요 민재님!

  • 개선이 필요하다고 생각하는 코드
    요기에서 민재님 말씀대로 equal가 true면 Component(props)를 호출하지 않고 return 하는방식이면 리액트의 hook 규칙을 위반할 수 있을것 같습니다! 그래서 현재처럼 Component(props) 호출을 조건부로 처리하기보다는, 항상 렌더 함수는 실행하되 결과만 캐싱하는 방향으로 구조를 바꾸면 hook 규칙을 지키는 데 더 안전하지 않을까 싶은데 이 방식은 어떠실까요?!

import { shallowEquals } from "./shallowEquals";

export const deepEquals = (a: unknown, b: unknown): boolean => {
if (shallowEquals(a, b)) return true;
Copy link

@taeyeong0814 taeyeong0814 Jul 26, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이 함수에서 얕은 비교해서 true 로 처리하면 깊은 비교를 안 하게 되는 거 아닐까요?
객체 안에 객체나 배열이 있을 때 처음 객체만 판단해서 같으면 그냥 true 하면 중첩 된 부분들의 값을 비교하지 않을 것 같은데 이렇게 얕은 비교로만 먼저 true로 반환해도 괜찮을까 궁금합니다

Copy link
Author

@minjaeleee minjaeleee Jul 26, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

태영님 리뷰 감사합니다!
만약에 배열 내부에서 2depth 이상의 중첩된 배열이라면 얕은 비교로 1depth로 순회할 때, (예를 들어 [1, 2]와 [1, 2])는 참조 값이 달라(!==) false를 반환하게 되고 깊은 비교를 하게됩니다!

제가 이렇게 구조화한 이유는 1depth만 있는 구조에서는 성능을 위해 재귀적으로 깊은 비교 함수를 실행하지 않고 비용을 줄여 얕은 비교만으로 실행시키고 싶어서 성능을 고려하여 이렇게 구조화하였습니다!

제가 고려하지 못한 엣지 케이스가 있다면 피드백 주시면 너무 감사하겠습니다 👍

return a === b;
export const shallowEquals = (a: unknown, b: unknown): boolean => {
// 1. 기본 타입 값들을 정확히 비교해야 한다.
if (a === b || (Number.isNaN(a) && Number.isNaN(b))) return true;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Object.is를 활용해보시면 좋을 것 같아요. NaN 비교도 해주는 친구입니다.
https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Global_Objects/Object/is

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

용준님 리뷰 감사합니다!
맞습니다! Object.is 메소드로 더욱 안정적인 비교를 하면 좋을 것 같습니다
추후 리팩토링에 적용해보겠습니다 ㅎㅎ

if (keysA.length !== keysB.length) return false;

for (const key of keysA) {
if (!Object.prototype.hasOwnProperty.call(b, key)) return false;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

in 대신 hasOwnProperty를 사용하신 의도가 궁금해요.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

사실 Object를 구별하는 모든 엣지 케이스를 고려하고 싶었습니다. 아시다시피, 자바스크립트 에러로 type이 "object"로 반환되는 것들이 많다보니, 그런 측면에서 의도를 봐주시면 될 것 같습니다!

Comment on lines +17 to +22
rendered: null,
});

// 3. eqauls 함수를 사용하여 props 비교 - 새롭게 컴포넌트 렌더링X
if (memoizedRef.current.prevProps !== null && equals(memoizedRef.current.prevProps, props)) {
return memoizedRef.current.rendered!;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

rendered의 초기값으로 Component를 넣어주게되면 if 문 내의 return값에 타입단언을 하지않아도 될 것 같은데 어떻게 생각하세요?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

지현님 리뷰 감사합니다 ㅎㅎ

오 그렇겠네요!
추후 리팩토링 하게 되면 고려해보겠습니다!

@pitangland
Copy link

LGTM!!!!!!!!!!!!

@minjaeleee
Copy link
Author

희진님 리뷰 남겨주셔서 감사합니다!
저도 희진님과 마찬가지로 과제 체크할 때 발견했던 부분이며 PR - "개선이 필요한 코드"에도 작성해 놓았었답니다ㅎㅎ
추후 리팩토링 때 참고하겠습니다!!

if (Array.isArray(a) !== Array.isArray(b)) return false;

// 3. 순수 객체 비교
const isObject = (val: unknown): val is Record<string, unknown> => {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

여러 케이스에 대해 생각하시고 구현하신 느낌이 확 드네요 ! 나중에 저도 적용해봐야겠어요.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

8 participants