Skip to content

[5팀 양성진] Chapter 1-3. React, Beyond the Basics#46

Open
Yangs1s wants to merge 10 commits intohanghae-plus:mainfrom
Yangs1s:main
Open

[5팀 양성진] Chapter 1-3. React, Beyond the Basics#46
Yangs1s wants to merge 10 commits intohanghae-plus:mainfrom
Yangs1s:main

Conversation

@Yangs1s
Copy link

@Yangs1s Yangs1s commented Jul 23, 2025

과제 체크포인트

배포 링크

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를 구현하면서 메모이제이션은 항상 성능을 보장하지 않는다는 점이였는데 이런 최적화 기법들을 어떤 기준에서 적용하시는지 궁금합니다.

@Yangs1s Yangs1s changed the title [6팀 양성진] Chapter 1-3. React, Beyond the Basics [5팀 양성진] Chapter 1-3. React, Beyond the Basics Jul 23, 2025
- array일시 이 부분을 제거함
- 하단에 for-of문의 코드가 중복적으로 같은 일을 하는걸 알게되었고, 제거 및 너무 길어지는건 따로 함수로 분리시킴
@heojungseok
Copy link

나이스

@JiHoon-0330
Copy link

fiber 에 대해서 잘 정리된 것 같아요

@Yangs1s
Copy link
Author

Yangs1s commented Jul 25, 2025

나이스

굳굳 감사요 정석님도 나이스

@Yangs1s
Copy link
Author

Yangs1s commented Jul 25, 2025

fiber 에 대해서 잘 정리된 것 같아요

감사합니다!

show(...args);
hideAfter();
};
const actions = useMemo(() => {

Choose a reason for hiding this comment

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

useMemo 를 사용할 때 의존성 배열에 사용중인 값을 넣어주면 좋을 것 같아요
예상하지 못한 오류가 발생할 수 있음

}

for (const key of keysA) {
if (!deepEquals((a as Record<string, unknown>)[key], (b as Record<string, unknown>)[key])) return false;

Choose a reason for hiding this comment

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

as 를 사용히지 않는 방법도 고려해보면 좋을 것 같아요

return Object.prototype.hasOwnProperty.call(obj, key);
}

function isNotNull(obj: unknown) {

Choose a reason for hiding this comment

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

function isNotNull(obj: T) : obj is NonNullable
이런 식으로 타입 추론이 가능하게 작성하는 방법도 있어요!


export const useAutoCallback = <T extends AnyFunction>(fn: T): T => {
return fn;
const lastFnRef = useRef<T | null>(null);

Choose a reason for hiding this comment

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

입력받은 fn 을 초기값으로 할당하면 null 인 경우를 고려하지 않아도 되서 단순해질 것 같아요

// step 2. 종속성이 변경되지 않았다면, 이전 값만 변환 변경되면 함수 다시 실행
// 다시 계산하고 메모 그다음 이값을 반환.

if (ref.current === null) {

Choose a reason for hiding this comment

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

중첩된 조건문을 단순하게 만드는 것을 고려해보면 좋을 것 같아요

const [state, setState] = useState<unknown>(initialValue);

// setState는 언제나 같은 함수를 반환
const updateState = useCallback((newState: T) => {

Choose a reason for hiding this comment

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

useState set 함수는 변경할 상태 값을 넣을 수도 있지만, 함수를 넣을수도 있어서 함수를 받는 경우도 만족하도록 추가해보시면 좋을 것 같습니다

Copy link

@YeongseoYoon-hanghae YeongseoYoon-hanghae left a comment

Choose a reason for hiding this comment

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

성진님 너무 고생하셨습니다 4주차도 킵고잉~

Comment on lines +15 to +27
if (ref.current === null) {
const result = factory();
ref.current = { _deps, result };
return ref.current.result;
} else {
if (_equals(ref.current._deps, _deps)) {
return ref.current.result;
} else {
const result = factory();
ref.current = { _deps, result };
return ref.current.result;
}
}
Copy link
Member

@chan9yu chan9yu Jul 26, 2025

Choose a reason for hiding this comment

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

저는 개인적으로 코드의 가독성을 올리기 위해 불필요한 중첩 분기처리를 지양하는편입니다

ref.current === null 여기 분기에서 return을 한다면 else를 안써도 되지않을까요?
_equals(ref.current._deps, _deps) 여기도 마찬가지 일 거 같습니다

if (ref.current === null) {
  const result = factory();
  ref.current = { _deps, result };
  return ref.current.result;
} 

if (_equals(ref.current._deps, _deps)) {
  return ref.current.result;
} 

const result = factory();
ref.current = { _deps, result };
return ref.current.result;

Comment on lines +49 to +50
<ToastStateContext value={{ ...state }}>
<ToastActionContext value={actions}>

Choose a reason for hiding this comment

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

아이디어) 어차피 action 호출할 때 message 를 함수 인자로 넘기므로 action에선 stateContext 가 필요 없는 것 같습니다, 오히려 순서를 바꿔서 Action → State로 감싸는 것이 불필요한 렌더링을 줄여서 더 효율적일 수 있지 않을까 싶어요...?

// useSyncExternalStore 에서 활용할 수 있도록 subscribe 함수를 수정합니다.
const subscribe = (fn: Listener) => {
listeners.add(fn);
return () => unsubscribe(fn);

Choose a reason for hiding this comment

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

createObserver함수에서 unsubscribe를 객체 메서드에 담아 반환하지 않고 subscribe에서 cleanup 함수를 반환한 부분이 좋은 것 같아요

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.

7 participants