과제 체크포인트
배포 링크
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로 이미 같은방식으로 처리하는 데 굳이 저게 필요한가 싶어서 중복기능이라 생각해 삭제했습니다.
그리고 내부가 좀 너무 길어지는 내용들이 많아서 따로 위에 함수를 만들어서 사용했습니다. 이러니까 저번보다 짧아지고 가독성도 쬐끔 올라간거같습니다.
학습 효과 분석
- 리액트 hook들의 동작원리 (특히 usestate) 가장 많이 배웠다고 느낍니다.
- 상태관리는 추가적으로 좀더 공부해봐야겟습니다.
과제 피드백
학습 갈무리
리액트의 렌더링이 어떻게 이루어지는지 정리해주세요.
리엑트에서의 렌더링
- 브라우저에서 필요한 Dom Tree를 만드는 과정이다.
- 렌더링을 유발하는 단계
- createRoot의 실행 혹은 state업데이트시 발생.
- 렌더링으로 컴퍼넌트를 호출하는단계
- createRoot 실행시 Root컴퍼넌트 호출
- state update시 state가 속한 컴퍼넌트 호출
- 커밋 단계
- 첫커밋시: 모든 노드 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의 메모이제이션 도구들
- useMemo - 값 캐싱
const expensiveValue = useMemo(() => {
return heavyCalculation(props.data);
}, [props.data]); // props.data가 같으면 재계산 안 함
- useCallback - 함수 캐싱
const handleClick = useCallback(() => {
onClick(id);
}, [id, onClick]); // 의존성이 같으면 함수 재생성 안 함
- React.memo - 컴포넌트 캐싱
const MemoizedComponent = React.memo(({ name, age }) => {
return <div>{name} ({age})</div>;
}); // props가 같으면 컴포넌트 재렌더링 안 함
장단점
장점
- 복잡한 구조나 큰프로젝트에서 성능을 크게 개선, 리소스를 효율적으로 사용합니다.
- 복잡한 연산을 다시 안하고 저장했다가 필요시 사용합니다.
단점
- 올바른 사용이 중요합니다.
- 잘못사용하면 성능저하를 일으킬수 있습니다.
- 성능 병목현상이 실제로 발생하는 경우만 사용해야한다
컨텍스트와 상태관리에 대한 나의 생각을 적어주세요.
Context API
핵심 개념
- Props 드릴링을 방지
- 데이터 파이프라인 역할, 깊숙한 곳 까지 데이터를 전달해주는 통로역할을 함 단순하게 전달한다고 보면 됨.
언제 사용하는게 좋을까요
- 간단한 상태 전달이 목적일때,
- 상태 로직이 복잡하지 않을때,
- useReducer 와 사용시 외부 라이브러리 없이 내장 기능으로만 상태관리도 하고싶을때 사용.
- 컴포넌트 구조 설계를 고민하고 사용합니다.
상태관리 라이브러리
- 선택적 구독: 필요한 것만 골라서 최적화시키는 기능이 내장
- 체계적 관리: 앱 상태가 복잡하고 클 때 구조적으로 관리
- 렌더링 최적화: 성능 최적화가 중요할 때 세밀한 제어 가능
리뷰 받고 싶은 내용
- useMemo,useCallback,react.memo를 구현하면서 메모이제이션은 항상 성능을 보장하지 않는다는 점이였는데 이런 최적화 기법들을 어떤 기준에서 적용하시는지 궁금합니다.
과제 피드백
성진님 고생하셨습니다! 작성해주신 파이버 재조정에 대해서도 좀 더 구체적으로 살펴보면 큰 도움이 될 것 같은데요! 이 부분에 대해서 저번에 공유드렸던 것처럼 잘 설명되어있는
- https://react.gg/visualized
- https://jser.dev/series/react-source-code-walkthrough 요런 사이트들도 한번 살펴보면 이해에 큰 도움이 될 것 같아요.
추가로 이 과제를 진행하는데 있어 핵심적인 부분 중 하나는 상태 관리 였던 것 같은데, 좀 더 공부해보셔야겠다고 남겨주셨으니 꼭! 살펴보시고 팀원분들과 함께 이야기 나눠보시면 좋을것 같네요.
추가로 질문 주셨던 것처럼 과거에는 모든 코드에 메모이제이션을 하는 파와 절대 하지않는다 파(?)로 나뉘어져서 막 논쟁이 있었던 것 같은데, 최근에는 그런 사람들이 많이 사라진 것 같아요. 일반적으로 저희가 따르는 최적화 논리대로 섣부른 최적화는 하지 않되, 성능적으로 이슈가 발생하는 지점이 생긴다면 그 지점에 대해서 파악한 뒤 그 부분만 국지적으로 처리하는게 적절한 사용 방법인 것 같아요. 내부적으로 이미 처리가 어느정도 되어있고 최근 들어서는 모든걸 해결해주는 관점에서 개발을 하는 것은 아니지만 컴파일러의 발전도 지속되고 있잖아요. 필요한 부분이 발견되면 그 때 적용하는 형태로 하면 좋을 것 같습니다.
고생하셨고 다음 주도 이번 주처럼 화이팅입니다!