Skip to content

[1팀 주산들] Chapter 1-3. React, Beyond the Basics#33

Open
DEV4N4 wants to merge 17 commits intohanghae-plus:mainfrom
DEV4N4:main
Open

[1팀 주산들] Chapter 1-3. React, Beyond the Basics#33
DEV4N4 wants to merge 17 commits intohanghae-plus:mainfrom
DEV4N4:main

Conversation

@DEV4N4
Copy link

@DEV4N4 DEV4N4 commented Jul 21, 2025

과제 체크포인트

배포 링크

https://dev4n4.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 개선

과제 셀프회고

React의 Hook들에 대해 deepdive 해보는 계기가 되어서 좋았다.
Hook들은 당연히 JS로 구현이 되어 있었겠지만… Hook을 직접! JS로 작성해 볼 수도 있다는 생각은 안해봤는데 이렇게 과제로 제시받아 직접 해보니까 이해도가 올라가고 Hook의 내부 구조에 대해 고민해보고 알 수 있게 되어서 정말 좋았다..

나는 기초가 부족한 편이었다고 스스로 생각하고 있었기도 해서 이번 기회에 보완하는 데 많은 도움을 받았던 것 같다.

기술적 성장

👍 새로 학습한 개념

Object.is

https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Global_Objects/Object/is

이번 과제에서 많이 사용했던 메서드이고 == 연산자, === 연산자와 어떤 점이 다른지에 대해 잘 아는 계기가 되었던 것 같다.

  • == 연산자와의 차이점
    • == 연산자는 같음을 테스트하기 전에 양 쪽(이 같은 형이 아니라면)에 다양한 강제(coercion)를 적용하지만("" == false가 true가 되는 것과 같은 행동을 초래), Object.is는 어느 값도 강제하지 않습니다.
  • === 연산자와의 차이점
    • Object.is()는 === 연산자와도 같지 않습니다. Object.is()와 ===의 유일한 차이는 부호 있는 0과 NaN 값들의 처리입니다. === 연산자(및 == 연산자)는 숫자값 -0과 +0을 같게 처리하지만, NaN은 서로 같지 않게 처리합니다.

Object.prototype.hasOwnProperty() 대신 Object.hasOwn() 사용하기

https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Global_Objects/Object/hasOwn

지금까지 Object.prototype.hasOwnProperty()를 사용해 왔었는데, hasOwn을 사용하는게 권장된다는 걸 이번에 첨 알았다!

Object.hasOwn() 가 권장됩니다. hasOwnProperty() 는 이를 지원하는 브라우저에서만 사용됩니다.

💪 기존 지식의 재발견/심화

useRef

useState를 활용해 useRef를 만들 수 있다는 것을 이번에 알았다!

두 Hook에 대해 다 알고는 있었는데… 이렇게 구현될 수 있다는 것이 신기했고
생각해보면 그렇지 싶은데 왜 지금까지는 별 생각이 없었을까… 사실 잘 몰랐던 게 아닐까 싶었다.

const [value] = useState(...)로 Setter 함수를 무시하면, 해당 객체를 변경해도 리렌더링이 발생하지 않는다.

  • 일반적으로 useState를 사용하면 상태를 업데이트할 때 setState를 호출해 리렌더링을 유발하지만, 여기서는 setter를 전혀 사용하지 않기 때문에 .current만 변경해도 화면 갱신이 일어나지 않는다.
    즉, value.current = 새로운값처럼 직접 할당해도 React는 이 변경을 인지하지 않으므로 리렌더링을 하지 않는다.

위 방식은 공식 useRef 훅의 동작 원리와 같다.

  • useRef한 번만 생성된 객체를 기억해 두었다가, 렌더링마다 동일한 객체를 반환한다. 그 객체의 .current 프로퍼티를 바꿔도 React가 다시 렌더링하지 않는다.
  • useState의 “초기 상태 유지” 특성을 활용해 저장소(ref)를 만든 것이다.

useMemo

useMemo가 이렇게 의존성들을 직접 비교 하면서 메모이제이션을 해주는 것이 신기했다. (생각해보면 비교가 당연히 들어갈텐데 그동안은 왜 이렇게 마법같이 막연하게 느껴졌었는지..?)

export function useMemo<T>(factory: () => T, _deps: DependencyList, _equals = shallowEquals): T {
  // 직접 작성한 useRef를 통해서 만들어보세요.

  // 1. 이전 의존성과 결과를 저장할 ref 생성
  const ref = useRef<{
    deps?: DependencyList;
    result?: T;
  }>({});

  // 2. 현재 의존성과 이전 의존성 비교
  // 3. 의존성이 변경된 경우 factory 함수 실행 및 결과 저장
  if (!ref.current.deps || !_equals(ref.current.deps, _deps)) {
    ref.current.deps = _deps;
    ref.current.result = factory();
  }

  // 4. 메모이제이션된 값 반환
  return ref.current.result as T;
}

자랑하고 싶은 코드

대체로 정답이 있는 코드들 같아서 자랑하고 싶은 코드가 크게 생각나지는 않는다.

그래도 굳이 자랑할 코드를 생각해보자면 equal 함수들을 AI를 사용하지 않고 직접 생각하면서 구현했다는거..? 리팩토링도 한번 했다는거!?

// shallowEquals 함수는 두 값의 얕은 비교를 수행합니다.
export const shallowEquals = (objA: unknown, objB: unknown) => {
  // 1. 두 값이 정확히 같은지 확인 (참조가 같은 경우)
  if (Object.is(objA, objB)) return true;

  // 2. 둘 중 하나라도 객체가 아닌 경우 처리
  if (objA === null || objB === null) return false;

  if (Array.isArray(objA) && Array.isArray(objB)) {
    if (objA.length !== objB.length) return false;

    for (let i = 0; i < objA.length; i++) {
      if (!Object.is(objA[i], objB[i])) return false;
    }

    return true;
  }

  if (typeof objA === "object" && typeof objB === "object") {
    // 3. 객체의 키 개수가 다른 경우 처리
    const objAKeys = Object.keys(objA);
    const objBKeys = Object.keys(objB);

    if (objAKeys.length !== objBKeys.length) return false;

    // 4. 모든 키에 대해 얕은 비교 수행
    for (const key of objAKeys) {
      if (!Object.is((objA as Record<string, unknown>)[key], (objB as Record<string, unknown>)[key])) return false;
    }

    return true;
  }

  // 이 부분을 적절히 수정하세요.
  return false;
};

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

으음… 이것도 대체로 정답이 있는 코드를 정답의 원인을 찾아가면서 적어나갔던 거라 크게 생각이 나지 않는다.

그래도 전반적으로 자잘하게 (변수명이라던가) 리팩토링을 해서 가독성을 좋게 만들면 더 좋지 않을까 하는 생각이 든다.

학습 효과 분석

React의 Hook들에 대해 deepdive 해 볼 수 있었던 시간이었던 것 같아서 좋았다.

공식 문서를 정독하고, Hook들을 해부해 보면서 막연하게만 생각했던 구조가 현실적으로 다가와 이해도가 깊어졌던 것 같다.

이전에는 뭐랄까 JS와 React를 따로 생각했었는데 지금은 React가 JS 코드로 어떻게 만들어졌는지를 보니까
기초가 탄탄해서 라이브러리에 구애받지 않고 개발을 잘 하는 것에 대해서도 생각을 해보게 되는 것 같고…

기초가 중요하다, JS에 대해서 깊게 이해하는 것이 중요하다 라고 말로만 듣고 실제로 체감해본 적은 없는데
이번 과제들을 수행하며 왜 기초를 탄탄히 해야하는지 직접 체감하고(다 JS로 이루어져있구나 하고 실감이 가서), 앞으로 어떻게 공부를 해야 할 지에 대해서도 감을 잡을 수 있었던 것 같다.

나중에 React나 기타 라이브러리들의 repo를 뜯어보고 싶다는 생각도 들었다!

과제 피드백

기초 과제가 너무 좋았어요! Hook에 대해서 깊은 이해를 할 수 있어서 정말 유익하고 진행하면서 배운 것도 많았어요. 이전 주차들과 연계되는 느낌이 들어서 이해도 빨랐어요!!

심화 과제는 조금 어려웠어요.. AI의 도움을 받으면서 진행을 했고 잘 해결되긴 했으나 아직도 약간 헷갈리는 것 같아요.

Toast에서 좀 헤맸어요! “Context를 분리해야겠다!” 까지는 생각이 금방 도달했는데 useCallback을 쓸 지, useAutoCallback을 쓸 지, useMemo를 쓸 지 고민하고 이것저것 삽질을 했던 것 같아요.

근데 삽질하면서 이전에 구현했던 부분들을 다시 읽어보면서 복습도 되고 좋았어요!

학습 갈무리

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

리액트의 랜더링 과정

  • 리액트는 상태(state)나 props가 변경되면 렌더링이 발생합니다.
  • 변경이 감지되면 Virtual DOM에서 새로운 트리(가상 노드)를 생성하고, 이전 Virtual DOM과 비교하는 Reconciliation 과정을 거칩니다.
  • 이때 실제 DOM에는 변경된 부분만 최소한으로 반영하여 브라우저의 성능을 최적화합니다.

리액트의 렌더링 최적화 방법

  • React.memo, useMemo, useCallback을 활용해 불필요한 리렌더링을 방지할 수 있습니다.
  • 상태를 분리하거나, 컴포넌트를 나눠서 렌더링 범위를 줄이는 방식도 많이 사용됩니다.
  • key 값을 적절히 부여하거나 shouldComponentUpdate, PureComponent를 사용하는 것도 최적화 방법 중 하나입니다.

리액트의 렌더링과 관련된 개념

  • Virtual DOM: 실제 DOM보다 가벼운 JavaScript 객체로, 변경 사항을 빠르게 계산할 수 있도록 도와줍니다.
  • Reconciliation: 이전 Virtual DOM과 새 Virtual DOM을 비교(diffing)하여 최소한의 변경만 실제 DOM에 반영하는 과정입니다.
  • Batching: 여러 상태 업데이트를 하나로 묶어 한 번에 처리함으로써 렌더링 횟수를 줄이는 기법입니다.

렌더링과 관련된 라이프사이클 & Hook

  • 클래스 컴포넌트: componentDidMount, shouldComponentUpdate, componentDidUpdate
  • 함수형 컴포넌트: useEffect, useMemo, useCallback, useLayoutEffect, useSyncExternalStore

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

메모이제이션

  • 메모이제이션은 계산 비용이 높은 연산 결과를 재사용하기 위해 사용합니다.
  • useMemo, useCallback, React.memo 등으로 값이나 함수의 재생성을 방지할 수 있습니다.

메모이제이션이 언제 필요할까?

  • 렌더링 시 무거운 계산이 자주 발생하는 경우
  • 동일한 props로 자식 컴포넌트가 자주 리렌더링 되는 경우
  • 콜백 함수가 자식 컴포넌트에 props로 전달될 때, 참조가 매번 바뀌는 걸 방지하고 싶을 때

메모이제이션을 사용하지 않으면?

  • 렌더링이 자주 발생하고, 불필요한 연산으로 인해 성능 저하가 생길 수 있습니다.

장점

  • 불필요한 연산/렌더링을 줄여 성능을 높일 수 있음
  • 렌더링 결과의 일관성을 유지할 수 있음

단점

  • 코드 복잡도가 증가함
  • 의존성 배열 관리가 까다로울 수 있음
  • 너무 남용하면 오히려 성능이 저하될 수 있음

메모이제이션을 사용하지 않고도 해결할 수 있는 방법

  • 렌더링 자체를 줄일 수 있는 구조로 컴포넌트를 쪼개거나 상태를 최적화하는 방향으로 설계하기

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

컨텍스트와 상태관리가 필요한 이유

  • 전역적으로 공유되는 값(예: 사용자 정보, 테마, 언어 등)을 여러 컴포넌트에서 편리하게 사용할 수 있게 해줍니다.
  • 깊은 컴포넌트 트리를 props drilling 없이 상태를 전달할 수 있게 해줍니다.

컨텍스트와 상태관리를 사용하지 않으면?

  • props를 계속 전달하게 되면서 코드가 지저분해지고 유지보수가 어려워짐
  • 중첩된 컴포넌트에서 상태 접근이 비효율적임

장점

  • 전역 상태 공유가 쉬워짐
  • 특정 도메인 상태를 분리하여 관리할 수 있음 (예: 로그인, UI, 토스트 등)

단점

  • 남용하면 모든 컴포넌트가 렌더링되며 성능 저하 발생
  • 컨텍스트가 바뀔 때마다 해당 컨텍스트를 구독 중인 모든 컴포넌트가 리렌더링됨

컨텍스트와 상태관리를 사용하지 않고도 해결할 수 있는 방법

  • 컴포넌트 단위에서 상태를 관리하고, 필요한 경우에만 컨텍스트 사용하기
  • 전역 상태 관리 도구(Zustand, Redux 등)를 도입해 더 정교한 상태 관리 가능

사용할 때 주의할 점

  • 실제로 전역으로 공유할 필요가 있는 데이터만 컨텍스트로 관리하기
  • 값 변경이 자주 발생하는 상태는 컨텍스트보다는 로컬 상태나 store 기반 접근이 더 유리함

리뷰 받고 싶은 내용

  1. useMemo, useCallback, useAutoCallback을 구현할 때 마지막으로 값을 리턴할 때 타입 단언을 사용하여 “as T” 형식으로 리턴하였는데 이보다 나은 방법이 있을 지 궁금합니다. 약간 “any” 처럼 TS의 검사를 소홀히 하는 방식으로 빠져나간게 아닌가 싶어서요. 근데 보기에는 깔끔해 보이니 괜찮은가 싶기도 한데 TS의 의도대로 사용하지 못한 것 같기도 해서 자주 쓰면 안좋은 문법인가 하는 생각이 들기도 합니다. 코치님께서는 타입 단언을 사용하여 구현하는 것에 대해 어떻게 생각하시나요? 해당 케이스들에서는 어떻게 구현하는 것을 추천하시나요?

  2. ToastProvider 구현에 관한 질문입니다. 지금은 Context를 나누고, useMemo를 사용해서 최적화를 진행하였습니다. 그런데 이게 최선일지… 의도하신 대로 제가 문제를 잘 푼건지 궁금합니다. 뭔가 비슷한 형태의 코드가 반복되는 것도 같은데 반복되는 부분이 Context 선언하는 부분이니까 어쩔수 없나 싶기도 하면서도 여기서 조금 더 코드를 개선할 수도 있지 않을까 생각이 들기도 하네요. 여기서 조금 더 깔끔하고 예쁘게 코드를 고치려면 어떻게 나아가야 할까요?

Comment on lines +9 to +17
if (Array.isArray(objA) && Array.isArray(objB)) {
if (objA.length !== objB.length) return false;

for (let i = 0; i < objA.length; i++) {
if (!Object.is(objA[i], objB[i])) return false;
}

return true;
}
Copy link

@jinsoul75 jinsoul75 Jul 25, 2025

Choose a reason for hiding this comment

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

사실 배열도 타입이 'object'여서 이부분이 없어도 통과될거에요 :)
배열을 Object.keys() 메서드로 접근시 인덱스가 key가 됩니다!

Comment on lines 1 to 24
/* 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();

// 1. 이전 의존성과 결과를 저장할 ref 생성
const ref = useRef<{
deps?: DependencyList;
result?: T;
}>({});

// 2. 현재 의존성과 이전 의존성 비교
// 3. 의존성이 변경된 경우 factory 함수 실행 및 결과 저장
if (!ref.current.deps || !_equals(ref.current.deps, _deps)) {
ref.current.deps = _deps;
ref.current.result = factory();
}

// 4. 메모이제이션된 값 반환
return ref.current.result as T;
}
Copy link

@jinsoul75 jinsoul75 Jul 25, 2025

Choose a reason for hiding this comment

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

ref.current.result가 옵셔널이기 때문에 undefined가 될 수 있어 as T 단언을 해주어야 하는것으로 보여요!
ref 값에 빈 객체 {} 대신 초기값

const ref = useRef<{
    deps?: DependencyList;
    result: T;
  }>({
    deps: _deps,
    result: factory(),
  });

을 설정해주면 타입스크립트가 T로 추론할 것 같습니당

Copy link

@jinsoul75 jinsoul75 left a comment

Choose a reason for hiding this comment

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

산들님 PR은 옆에서 말로 설명해주는것 같이 술술 읽히네요 ㅎㅎㅎ 3주차 과제도 고생많으셨습니당 :) ~

Comment on lines +30 to +34
const objKey = objAKeys[i];

if (!Object.hasOwn(objB, objKey)) {
return false;
}
Copy link

@areumH areumH Jul 26, 2025

Choose a reason for hiding this comment

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

이런 식으로 b 객체에 a 객체의 키가 존재하는지 확인하여 먼저 리턴하는 방법도 있군요!!
objKey 대신 objAkey 처럼 a 객체의 키값이라는 걸 명시해주는건 어떨까요!? 🤩

return Component;
return function MemoizedComponent(props: P) {
const prevPropsRef = useRef<P | null>(null);
const renderedResultRef = useRef<ReactNode | Promise<ReactNode> | null>(null);

Choose a reason for hiding this comment

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

useRef 안에 ReactNode 와 Promise 사용한 이유가 궁금해요 ! (저는 ReactNode 를 안써봐서요!!)

Choose a reason for hiding this comment

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

저는 <ReturnType<typeof Component> | null>으로 renderedResultRef 타입 해줬어요!

}
}, []);

return [state, setStateWithShallowEquals] as const;

Choose a reason for hiding this comment

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

오 useState 형식으로 return한게 인상이 깊네요!! 별칭(const) 없이도 쓸수 있는지가 궁금해요. 그리고 왜 const로 지정했는지도 궁금해요~

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.

5 participants