Yangs1s 님의 상세페이지[5팀 양성진] Chapter 1-3. React, Beyond the Basics

과제 체크포인트

배포 링크

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

과제 셀프회고

기술적 성장

물론 리액트를 잘 알고 써본건 아니여도 직장에서 쓰긴 써봐서. 이번 과제는 기존지식의 재발견과 조금 딥하게 공부를 좀 해봤던거 같아요. 그리고 새로운 개념들이 참 흥미롭게 다가온거도 많았습니다.

shallowEquals & DeepEqual 을 구현시 개념을 공부하고자 봤는데. shallow compare을 통해서 참조타입 과 원시타입의 특징을 좀 더 명확하게 알게 되었고, 참조라는 특징을 좀 더 명확하게 알게된 계기가 되었습니다.

hooks 만들면서 알게된 fiber

리액트 16전에는 재조정(Reconciliation) 엔진이 Stack방식이지만 파이버 기반으로 16부터는 새롭게 바뀌었다. 이전엔 함수형컴퍼넌트안에서 hooks들을 사용할 수 없었지만 이 방식이 채택되고 사용가능 해졌다.

특징 (Feature)스택 재조정기 (React 15 이하)파이버 재조정기 ( React 16 이상)
작업 방식동기적 (Synchronous)비동기적 (Asynchronous)
작업 단위전체 컴포넌트 트리파이버(Fiber) 라는 작은 작업 단위 (Unit of Work)
중단 가능성불가능 (Non-interruptible)가능 (Interruptible)
핵심 아이디어재귀(Recursion)를 이용한 깊이 우선탐색 연결 리스트(Linked List)를 이용한 가상 스택 프레임
렌더링 제어일단 시작하면 끝날 때까지 멈추지 못함작업을 멈추고, 재시작하고, 우선순위를 정할 수 있음
주요 단점중간에 작업을 중단하기 어렵다. (렌더링 블로킹)개념적으로 더 복잡함
주요 장점개념적으로 단순함 부드러운 사용자 경험,동시성(Concurrency), Suspense 등 구현 가능

작업의 단위나 작업 방식, 장단점을 제외하고 나머진 AI에게 정리를 부탁해서 만들었습니다.

fiber 재조정자는 랜더링 단위를 더 잘게 나누어서 작업 우선순위가 높은거 부터 처리한다. 이게 핵심!

자랑하고 싶은 코드

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

수정전

export const shallowEquals = (a: unknown, b: unknown) => {


  if (Object.is(a, b)) return true;


  if (typeof a !== "object" || typeof b !== "object" || a === null || b === null) return false;

  const KeysA = Object.keys(a);
  const KeysB = Object.keys(b);


  if (KeysA.length !== KeysB.length) return false;


  if (Array.isArray(a) && Array.isArray(b)) {
    if (a.length !== b.length) return false;
    for (let i = 0; i < a.length; i++) {
      if (!Object.is(a[i], b[i])) return false;
    }
    return true;
  }


  for (const key of KeysA) {
    if (
      !Object.prototype.hasOwnProperty.call(b, key) ||
      !Object.is((a as Record<string, unknown>)[key], (b as Record<string, unknown>)[key])
    )
      return false;
  }

  return true;
};

너무 한 기능에 여러개를 때려박은거 같은거 같아서 찬규님 pr을 한번 읽어보면서 코드를 한번 기능별로 분할시켜봐야겠다 라는 생각이 들어서 한번 바꿔보려고 합니다. (-- 진행 중 ..---)

수정후,

function hasOwnProperty(obj: unknown, key: string) {
  return Object.prototype.hasOwnProperty.call(obj, key);
}

function isNotNull(obj: unknown) {
  return obj !== null;
}

function isObject(obj: unknown) {
  return typeof obj === "object";
}

//객체의 최상위 속성들만 비교하며, 중첩된 객체는 참조값(메모리 주소)만 비교함
export const shallowEquals = (a: unknown, b: unknown) => {


  const objA = a as Record<string, unknown>;
  const objB = b as Record<string, unknown>;

  if (Object.is(objA, objB)) return true;


  if (!isObject(objA) || !isObject(objB) || !isNotNull(objA) || !isNotNull(objB)) return false;

  const KeysA = Object.keys(objA);
  const KeysB = Object.keys(objB);


  if (KeysA.length !== KeysB.length) return false;



  for (const key of KeysA) {
    if (!hasOwnProperty(b, key) || !Object.is(objA[key], objB[key])) return false;
  }

  return true;
};

조금 고쳐봤습니다. 테스트코드에서 배열과 객체일시 케이스가 나눠져있길래 배열일때를 따로 if문으로 작업했는데 Object.keys와 for-of로 이미 같은방식으로 처리하는 데 굳이 저게 필요한가 싶어서 중복기능이라 생각해 삭제했습니다.

그리고 내부가 좀 너무 길어지는 내용들이 많아서 따로 위에 함수를 만들어서 사용했습니다. 이러니까 저번보다 짧아지고 가독성도 쬐끔 올라간거같습니다.

학습 효과 분석

  1. 리액트 hook들의 동작원리 (특히 usestate) 가장 많이 배웠다고 느낍니다.
  2. 상태관리는 추가적으로 좀더 공부해봐야겟습니다.

과제 피드백

학습 갈무리

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

리엑트에서의 렌더링

  • 브라우저에서 필요한 Dom Tree를 만드는 과정이다.
  1. 렌더링을 유발하는 단계
  • createRoot의 실행 혹은 state업데이트시 발생.
  1. 렌더링으로 컴퍼넌트를 호출하는단계
  • createRoot 실행시 Root컴퍼넌트 호출
  • state update시 state가 속한 컴퍼넌트 호출
  1. 커밋 단계
  • 첫커밋시: 모든 노드 appendChild로 생성
  • 아니면: 최소한의 작업을 통해, 변경사항만 실제 DOM에 적용. 변경사항은 렌더링중 계산된다.

1단계,2단계는 렌더 phase, 3은 커밋 페이즈 이다.

재조정(Reconciliation)이란?

  • 렌더 페이즈에서 일어나는 핵심적인 과정으로, Current Tree와 Work-in-Progress Tree를 비교하는 과정입니다. Current Tree는 현재 화면에 보여지고 있는 상태를 나타내는 트리 Work-in-Progress Tree는 새롭게 변화된 상태를 구성하는 트리

재조정의 목적

  • 효율적인 업데이트: 실제 변경된 부분만 찾아내어 DOM 조작을 최소화
  • 성능 최적화: 불필요한 DOM 조작을 피하고 최대한 효율적으로 작업 수행
  • 정확한 변경 감지: 어떤 컴포넌트가 실제로 업데이트되어야 하는지 정확히 판단

재조정에서 Key의 중요성

배열 렌더링에서 Key가 필수인 이유

  • 불필요한 재생성: 멀쩡한 컴포넌트나 내용을 부수고 다시 작성
  • 성능 저하: 모든 항목이 다시 렌더링됨
  • 불필요한 리렌더링: 실제로는 변경되지 않은 항목들까지 재렌더링
  • 상태 손실: 컴포넌트 내부 상태가 초기화될 수 있음
  • 부작용: 애니메이션이나 포커스 상태 등이 예상과 다르게 동작

Key가 있을 때의 동작

  • 정확한 식별: 각 항목에 고유한 키를 달아주면 React가 항목이 추가되거나 제거될 때 정확히 무엇이 변화했는지 알 수 있음
  • 효율적인 업데이트: 기존 컴포넌트를 재사용하고 필요한 부분만 업데이트

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

메모이제이션

메모이제이션이란?

**메모이제이션(Memoization)**은 프로그래밍 최적화 기법 중 하나로, 흔히 **"기억하기 기술"**이라고도 부릅니다. 특정 값이나 함수를 캐싱하고 동일한 값의 불필요한 재계산을 방지하는 최적화 기법입니다

React의 메모이제이션 도구들

  1. useMemo - 값 캐싱
const expensiveValue = useMemo(() => {
  return heavyCalculation(props.data);
}, [props.data]); // props.data가 같으면 재계산 안 함
  1. useCallback - 함수 캐싱
const handleClick = useCallback(() => {
  onClick(id);
}, [id, onClick]); // 의존성이 같으면 함수 재생성 안 함
  1. React.memo - 컴포넌트 캐싱
const MemoizedComponent = React.memo(({ name, age }) => {
  return <div>{name} ({age})</div>;
}); // props가 같으면 컴포넌트 재렌더링 안 함

장단점

장점

  • 복잡한 구조나 큰프로젝트에서 성능을 크게 개선, 리소스를 효율적으로 사용합니다.
  • 복잡한 연산을 다시 안하고 저장했다가 필요시 사용합니다.

단점

  • 올바른 사용이 중요합니다.
  • 잘못사용하면 성능저하를 일으킬수 있습니다.
  • 성능 병목현상이 실제로 발생하는 경우만 사용해야한다

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

Context API

핵심 개념

  • Props 드릴링을 방지
  • 데이터 파이프라인 역할, 깊숙한 곳 까지 데이터를 전달해주는 통로역할을 함 단순하게 전달한다고 보면 됨.

언제 사용하는게 좋을까요

  • 간단한 상태 전달이 목적일때,
  • 상태 로직이 복잡하지 않을때,
  • useReducer 와 사용시 외부 라이브러리 없이 내장 기능으로만 상태관리도 하고싶을때 사용.
  • 컴포넌트 구조 설계를 고민하고 사용합니다.

상태관리 라이브러리

  • 선택적 구독: 필요한 것만 골라서 최적화시키는 기능이 내장
  • 체계적 관리: 앱 상태가 복잡하고 클 때 구조적으로 관리
  • 렌더링 최적화: 성능 최적화가 중요할 때 세밀한 제어 가능

리뷰 받고 싶은 내용

  • useMemo,useCallback,react.memo를 구현하면서 메모이제이션은 항상 성능을 보장하지 않는다는 점이였는데 이런 최적화 기법들을 어떤 기준에서 적용하시는지 궁금합니다.

과제 피드백

성진님 고생하셨습니다! 작성해주신 파이버 재조정에 대해서도 좀 더 구체적으로 살펴보면 큰 도움이 될 것 같은데요! 이 부분에 대해서 저번에 공유드렸던 것처럼 잘 설명되어있는

추가로 이 과제를 진행하는데 있어 핵심적인 부분 중 하나는 상태 관리 였던 것 같은데, 좀 더 공부해보셔야겠다고 남겨주셨으니 꼭! 살펴보시고 팀원분들과 함께 이야기 나눠보시면 좋을것 같네요.

추가로 질문 주셨던 것처럼 과거에는 모든 코드에 메모이제이션을 하는 파와 절대 하지않는다 파(?)로 나뉘어져서 막 논쟁이 있었던 것 같은데, 최근에는 그런 사람들이 많이 사라진 것 같아요. 일반적으로 저희가 따르는 최적화 논리대로 섣부른 최적화는 하지 않되, 성능적으로 이슈가 발생하는 지점이 생긴다면 그 지점에 대해서 파악한 뒤 그 부분만 국지적으로 처리하는게 적절한 사용 방법인 것 같아요. 내부적으로 이미 처리가 어느정도 되어있고 최근 들어서는 모든걸 해결해주는 관점에서 개발을 하는 것은 아니지만 컴파일러의 발전도 지속되고 있잖아요. 필요한 부분이 발견되면 그 때 적용하는 형태로 하면 좋을 것 같습니다.

고생하셨고 다음 주도 이번 주처럼 화이팅입니다!