Skip to content

Conversation

@Jeong-wonho
Copy link

@Jeong-wonho Jeong-wonho commented Apr 10, 2025

과제 체크포인트

배포 링크

https://jeong-wonho.github.io/front_5th_chapter1-3/

기본과제

  • shallowEquals 구현 완료
  • deepEquals 구현 완료
  • memo 구현 완료
  • deepMemo 구현 완료
  • useRef 구현 완료
  • useMemo 구현 완료
  • useDeepMemo 구현 완료
  • useCallback 구현 완료

심화 과제

  • 기본과제에서 작성한 hook을 이용하여 렌더링 최적화를 진행하였다.
  • Context 코드를 개선하여 렌더링을 최소화하였다.

과제 셀프회고

기술적 성장

Object를 구분하는 세가지 방법

typeof의 한계
typeof는 객체를 비교하는데 있어서 한계를 가지고 있습니다. 바로 Arraynullobject로 반환한다는 것입니다.

const obj = {};
const arr = [];
const null_value = null;

console.log(typeof obj); //object
console.log(typeof arr);  //object
console.log(typeof null); //object

라는 결과가 나옵니다. 이것을 극복하기 위해서 해야할 것은 아래와 같이 작성하면 위와 같은 상황은 피할 수 있습니다.

//배열과 같은 경우
Array.isArray(arr);
//null과 같은 경우
null_value === null

추가적인 사항으로는 순수객체내장객체의 차이를 구분하는 방법도 있습니다. 내가 선언한 객체와 기본적으로 js 에 내장된 객체를 구분하는 방법도 있습니다.

const obj = {};
const date = new Date();

//비교하는 경우는 세가지 경우가 있다.
Object.prototype.toString.call()
Object.getPrototype()
instanceof

이 세 가지는 각각이 가지는 특징과 한계가 있음을 알 수 있었습니다.

const plainObj = {};
const arrayObj = [];
const dateObj = new Date();
const nullValue = null;

// 1. instanceof 연산자
console.log(plainObj instanceof Object);  // true
console.log(arrayObj instanceof Array);   // true
console.log(dateObj instanceof Date);     // true
console.log(nullValue instanceof Object); // false

// 2. Object.prototype.toString.call()
console.log(Object.prototype.toString.call(plainObj));  // "[object Object]"
console.log(Object.prototype.toString.call(arrayObj));  // "[object Array]"
console.log(Object.prototype.toString.call(dateObj));   // "[object Date]"
console.log(Object.prototype.toString.call(nullValue)); // "[object Null]"

// 3. Object.getPrototypeOf()
console.log(Object.getPrototypeOf(plainObj) === Object.prototype);  // true
console.log(Object.getPrototypeOf(arrayObj) === Array.prototype);   // true
console.log(Object.getPrototypeOf(dateObj) === Date.prototype);     // true
// console.log(Object.getPrototypeOf(nullValue)); // TypeError!

여기서 이 타입을 가장 분명하게 구별할 수 있는 것은 Object.proptotype.toString.call()로 보입니다. 해당 객체가 어떤 것인지 바로 알려주기 때문이죠.!

instanceof는 한계를 가지고 있는데요. iframe에서 생성된 객체를 알 수 없다는 점입니다. iframe 자신만의 실행 컨택스트를 가지고 있어서 iframe의 생성자로 만든 배열은 현재 컨택스트의 instanceof Arrayfalsy로 반환됩니다.

하지만 이번 과제에서는 위 내용들이 그렇게 핵심적으로 보이지는 않습니다. 깊은 비교를 어떻게 할 것인가?

hasOwnProperty를 사용해서 깊은 비교를 해보겠습니다. 사실 이건 깊은 비교라기보다는. 참조 값을 비교하는 것에 더 가깝다고 생각이 듭니다.

저의 코드는 그냥 hasOwnProperty가 같다면 그 해당 키들에 대해서 비교하고, 같다면 true를 리턴하고 다르다면 false를 리턴하면서 계속해서 찾아들어가게 작성해놓은 것입니다. 즉 원시값이 나올때까지 비교하는 것이라고 생각하면 좋을 거 같습니다.

return keysA.every((key) => {
    const itemA = (objA as Record<string, unknown>)[key];
    const itemB = (objB as Record<string, unknown>)[key];

    return itemA === itemB;
  });

이번 과제를 수행하면서 deepEqualsshallowEquals의 구현과 발제자료를 보면서 이번 과제의 핵심은 바로 참조에 있다는 생각이 들었습니다. 이 참조를 어떻게 풀어나갈것인가가 이번 발제의 중요한 점이라고 생각했습니다.

useRef 구현

발제자료를 읽어보닌 useStatesetState는 새로운 참조를 만들고 저장하고 랜더링 한다는 것을 알 수 있었습니다. 리액트는 참조 값을 비교하여 랜더링 여부를 결정하기 때문이죠 하지만 useRef는 랜더링은 일어나지 않지만 값이 변경되는 hook입니다. 그래서 참조를 동일하게 해주고 값만 변경해주면 되지 않을까 싶었습니다.

useMemo 구현

  1. useRef로 참조해야 하는 값을 저장 (메모이제이션 된 값, 의존성 저장)
  2. useRef내부의 dependency로 저장한 값이 변경되면 값을 얕은 비교로 비교
  3. 의존성에 값의 변경이 생기면 새롭게 값을 변경 factory함수를 통해 계산, ref.current.value에 저장.

HOC를 활용한 memo 구현

지속적인 테스트 코드 에러, props가 동일한데도 rendering이 두번 실행 되는것을 볼 수 있엇다. > 이 문제를 어떻게 해결할 수 있을까? 함수가 실행되는 순간 이미 렌더링이 실행된다..! React.createElement를 return 하기 때문에 렌더링이 두번 발생.! 이 부분을 새로운 Ref 담은 후 Ref를 return 하니 랜더링이 한 번만 실행되었다.

심화과제

context 분리

의존성 주입을 하였으나, 렌더링이 2번 발생해야 하나 4번 발생하는 문제를 확인했다. useMemo의 문제라기 보다는 관심사를 분리하고 처리하니 정상 작동하였습니다. context가 제대로 분리 되지 않았던 것으로 보이고 context의 value값이 변경 되다. 보니 많은 렌더링이 발생한 것으로 보입니다.

코드 품질

컴포넌트 분리

  • App.tsx에 하나로 있던 컴포넌트를 세가지 기준으로 나누었습니다. 어떤 관심사로 분리할지 고민하다가.
    아래와 같이 구분했습니다. 저의 관심사는 기능에 중점했던 거 같습니다. 이렇게 기능을 분리하니 memo 관리 context 관리가 편하다는 장점이 있었습니다.
src/
├── @lib                // 공통 유틸리티 기능
├── context             // 상태 관리 기능
├── components          // UI 컴포넌트
└── models              // 데이터 모델 정의
  • 렌더링이 왜 일어나는지, 기존 2주차에서 배운 것을 생각해보면 props가 변경될 때, 렌더링이 일어나는 것을 알 수 있었습니다.

학습 효과 분석

  • 객체에 대한 깊은 이해
  • 리액트 렌더링에 대한 고찰
  • contextAPI 학습

과제 피드백

  • 발제 자료가 풍성해서 좋았습니다.

리뷰 받고 싶은 내용

  • 분석을 해보니 가장 많은 렌더링이 일어나는 곳은 상품목록이었습니다. 해당 부분을 어떻게 성능을 최적화 시킬 수 있을까요?
  • Notification 클릭 시 왜 렌더링이 전체적으로 발생할까요?

Copy link

@ywkim95 ywkim95 left a comment

Choose a reason for hiding this comment

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

원호님 고생 많으셨습니다!
코드를 작성하실 때 생각을 계속 정리하시면서 접근하는 것이 정말 본받아야겠다는 생각이 드네요!
이후에는 코드의 일관성이나 단일 책임 원칙 등에 대해서 고려해보시는 것도 추천드립니다!
이번 주도 고생 많으셨고, 다음 주도 화이팅입니다!

Copy link

Choose a reason for hiding this comment

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

배포 과정을 직접 구현 하신건가요?
벌써 선행학습을 하시다니 대단하시네요 ㅎㅎ

// 1. 기본 타입이거나 null인 경우 처리
if (objA === objB) return true;
// 둘 중 하나라도 null이거나 객체가 아닌 경우
if (!objA || !objB || typeof objA !== "object" || typeof objB !== "object") {
Copy link

Choose a reason for hiding this comment

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

해당 부분에서 비어있는 문자열 값이 있는 경우도 고려하시면 좋을 듯 해요!

Comment on lines +7 to +9
const MemoizedComponent: React.FC<P> = (props: P) => {
return React.createElement(Component, props);
};
Copy link

Choose a reason for hiding this comment

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

여기서 React.xxxx로 시작하는 부분과 일반적인 함수 호출을 같이 사용하신 이유가 있을까요?

Comment on lines +10 to +25
function Wrapper(props: P) {
const prevPropsRef = useRef<P | null>(null);
const renderedRef = useRef<React.ReactElement | null>(null);

const hasChanged =
!prevPropsRef.current || !_equals(prevPropsRef.current, props);

if (hasChanged) {
prevPropsRef.current = props;
renderedRef.current = React.createElement(Component, props);
} else {
console.log("🧠 memo로 스킵됨", props);
}

return renderedRef.current;
}
Copy link

Choose a reason for hiding this comment

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

구현하신 Wrapper 이외의 함수가 없다면 즉시 반환을 해도 문제가 없을 것이라고 생각됩니다!

Copy link

Choose a reason for hiding this comment

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

구현하려고 하시는 로직에 대해서 플로우를 적어놓으신건 굉장히 좋은 것 같아요!
저도 항상 헷갈려서 문서를 다시 들여다보는데 좋은 습관을 들이신 것 같습니다!

export function useRef<T>(initialValue: T): { current: T } {
// React의 useState를 이용해서 만들어보세요.
return { current: initialValue };
const [ref] = useState({ current: initialValue });
Copy link

Choose a reason for hiding this comment

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

이 부분에서 일반적으로 객체나 값을 직접적으로 할당하는데 큰 데이터나 복잡한 연산에서는 조금 비효율적인 것으로 알고 있어요!

아래와 같이 선언하시면 최적화와 성능을 같이 잡을 수 있을 거에요!

const [refValue] = useState(() => ({ current: initialValue });

import { renderLog } from "../utils";

// NotificationSystem 컴포넌트
export const NotificationSystem: React.FC = () => {
Copy link

Choose a reason for hiding this comment

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

해당 명칭을 NotificationSystem이라고 작명하신 이유가 있을까요?
system이라는 명칭이 단순한 컴포넌트 구현한 것과 거리가 있어보인다는 생각이 들어서 질문 해봅니다

Copy link

Choose a reason for hiding this comment

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

Wrapper를 통해서 theme를 관리하는 방식은 굉장히 좋은 듯 합니다!
다만 해당 Wrapper를 사용하실때 여러 개의 Layout으로 구성하실지 단일 페이지로 구성하실지는 한 번 생각해보시면 좋을듯 합니다!

@ljwoon1211
Copy link

context를 관심사별로 두고 provider랑 createContext 나눈게 보기좋네요.

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.

3 participants