[4팀 김지혜] Chapter 1-3. React, Beyond the Basics#48
[4팀 김지혜] Chapter 1-3. React, Beyond the Basics#48adds9810 wants to merge 7 commits intohanghae-plus:mainfrom
Conversation
- useRef, useMemo, useCallback, useDeepMemo 커스텀 훅 구현 - shallowEquals, deepEquals 함수 직접 구현 및 타입 안전성 강화 - useMemo/Callback에서 의존성 비교 로직 개선 - dev 스크립트에 nvm 자동 적용 추가
- useAutoCallback: useRef + useCallback 패턴으로 참조 고정과 최신 값 참조 동시 구현 - useShallowState: 얕은 비교를 통한 불필요한 리렌더링 방지 및 함수형 setState 지원 - useCallback: 주석 정리 및 코드 간소화
- memo HOC: 클로저를 활용한 props 얕은 비교 및 메모이제이션 구현 - deepMemo HOC: memo 함수 재사용하여 깊은 비교 기반 메모이제이션 구현 - HOC 패턴을 통한 컴포넌트 리렌더링 최적화
- createObserver: useSyncExternalStore 호환 subscribe 함수 구현 - useShallowSelector: zustand 스타일 얕은 비교 메모이제이션 구현 - useStore: useSyncExternalStore와 useShallowSelector 연동 구현 - Observer 패턴 기반 상태 구독 시스템 완성
- Context를 Command와 State로 분리하여 리렌더링 최적화 - useMemo/useAutoCallback으로 함수 참조 고정 - SPA 라우팅을 위한 404.html 추가
|
지혜 님 파이팅!! |
|
PR 정말 상세히 잘 적어주시네요.. 배워갑니다! |
| // 다양한 타입(기본값, 배열, 객체 등)을 받아야 하므로 unknown 타입으로 선언 | ||
| export function shallowEquals(objA: unknown, objB: unknown): boolean { | ||
| // 1. 두 값이 정확히 같은지 확인 (참조가 같은 경우) | ||
| if (Object.is(objA, objB)) return true; |
There was a problem hiding this comment.
<자랑하고 싶어요>
처음 요구사항을 접했을 때는 단순한 비교 연산자(===)를 떠올렸지만, 주어진 자료들을 찾아보며 Object.is() 메서드를 알게 되었습니다. 이 메서드를 활용하여 NaN이나 -0과 같은 특수한 값들까지 정확하게 비교하도록 구현했습니다.
There was a problem hiding this comment.
저는 shallowEquals를 구현할 때 단순히 일치 연산자(===)를 사용하는 방식을 선택했는데, 단순한 기능을 구현하는 과정에서도 끊임없이 더 나은 방법이 있는지를 탐색하고 이를 적용하려는 모습이 상당히 짱짱걸입니다 지혜님 👏 (멋있음)
아 그리구 첨언해보자면, shallowEquals 함수 내부에서 objA, objB에 대한 타입 에러가 발생해서 as 단언문을 사용해주신 것으로 보이는데 타입 단언문을 사용하지 않고 해결하는 방법도 한 번 고려해보시면 어떨까요!?
| <ToastCommandContext value={commandValue}> | ||
| <ToastStateContext value={stateValue}> | ||
| {children} | ||
| {visible && createPortal(<Toast />, document.body)} | ||
| </ToastStateContext> | ||
| </ToastCommandContext> |
There was a problem hiding this comment.
<궁금해요>
Context를 사용할 때 ToastCommandContext와 ToastCommandContext.Provider 중 어떤 방식을 사용해야 하는지 궁금합니다. 저는 공식문서 보고 ToastCommandContext만 사용했는데, .provider를 붙여서 하신 분들이 많으셔서.. 어떻게 하셨고 왜 사용하셨는지 궁금합니다.
|
지혜님 PR 너무 상세히 잘 작성해주셔서 감탄하면서 봤읍니다아.. 이거시 냥집사인가요 PR을 읽으면서, 지혜님이 과제를 해결해나가시는 사고 방향이 상상이 간다는게 참 글을 잘 쓰신게 아닌가 하는 생각이 드네요. 저도 다음에 지혜님 처럼 의사 결정의 순간들을 모두 담아서 글을 작성하도록 노력해봐야 할 거 같아요! 근데 pr 작성하는 도중에 냥이가 노트북이나 컴퓨터 꺼버리면 어떻게 되나요 |
과제 체크포인트
배포 링크
https://adds9810.github.io/front_6th_chapter1-3/
기본과제
equalities
hooks
High Order Components
심화 과제
hooks
context
과제 셀프회고
이번 과제는 React를 그냥 쓰기만 하던 라이브러리에서 진짜 어떻게 돌아가는지 알 수 있게 해준 과제였습니다. 강의에서 배워서 알고있던 useRef, useMemo 같은 것들을 useState부터 하나하나 직접 만들어보면서, 이런 기능들이 왜 만들어졌는지 알 수 있었습니다.
useSyncExternalStore 써서 외부 상태 연결하고, Context API 쓸 때 성능 문제 생기는 것도 경험해보면서 최적화가 왜 중요한지 알게 됐습니다.
이 모든 과정이 그냥 따로따로 알던 지식들을 하나로 연결해주고, React가 실제로 어떻게 작동하는지 깊게 이해할 수 있게 해줬습니다. 또한 AI 사용하는 방식도 1, 2주차에는 무작정 의존하던 것에서 이번에는 문서 보고 시도한 다음에 AI한테 질문하는 식으로 더 나아졌습니다.
기술적 성장
강의에서 배웠던 훅들을 직접 구현해보니 useState 기반으로 동작한다는 것을 알게 되었습니다.
"참조는 고정하되 최신 값은 참조"라는 까다로운 요구사항을
useRef+useCallback조합으로 해결하는 패턴을 알게 되었습니다.useSyncExternalStore와 호환되려면
subscribe함수가 구독 취소 함수를 반환해야 한다는 스펙을 학습했습니다.기존 지식의 재발견/심화:
useMemo,useCallback의 실제 구현 방식 이해이론적으로만 알고 있던 메모이제이션이 실제로는
useRef를 사용해서 이전 값과 현재 값을 비교하고, 의존성이 변경되었을 때만 새로운 값을 계산하는 방식으로 동작한다는 것을 직접 구현해보며 이해할 수 있었다. 특히shallowEquals함수를 사용해서 의존성 배열을 비교하는 부분에서, 단순히===비교가 아닌 얕은 비교를 통해 객체의 내용을 비교하는 것이 중요하다는 것을 체감했습니다.React에서 함수 참조가 불안정하면 하위 컴포넌트가 불필요하게 리렌더링된다는 이론을 알고 있었지만, 실제로
ToastProvider에서 함수들이 매번 새로 생성되어ProductCard가 계속 리렌더링되는 문제를 겪어보면서 그 심각성을 직접 체감했다. 특히Context value로 전달되는 함수들의 참조 안정성이 얼마나 중요한지, 그리고useMemo,useCallback,useAutoCallback을 적절히 조합해서 사용해야 한다는 것을 실제 문제 해결 과정에서 깊이 이해할 수 있었습니다.구현 과정에서의 기술적 도전과 해결:
useAutoCallback을 만들 때 타입 때문에 고생했습니다. 처음에는 타입을 몰라서 이렇게 코드만 적었었습니다.:그런데 TypeScript에서 타입 에러가 나서 제네릭이라는 걸 처음 써봤는데, 함수의 타입을 미리 정해놓지 않고 나중에 정할 수 있게 하는 건데, 이게 생각보다 복잡했습니다.
처음에는
unknown[]라는 타입을 봤을 때 "이게 뭐지?" 싶었습니다. unknown은 "아무 타입이나 될 수 있다"는 뜻인데, 배열로 만들어서 함수의 인자들을 받는 거였다. 근데 이걸 다시 원래 함수의 타입으로 바꿔줘야 하는데, TypeScript가 자동으로 해주지 않아서as T라는 타입 단언을 써야 했습니다.이렇게 해서 "참조는 고정하되 최신 값은 참조"라는 요구사항을 만족할 수 있었습니다. 강의에서 배운 개념들이지만 실제로 구현해보니까 하나하나가 다 어려웠습니다. 이후로도
shallowEquals나createObserver같은 다른 함수들을 만들 때도 비슷한 타입 에러들을 종종 마주쳤는데, 하나씩 해결해나가면서 TypeScript를 조금씩 이해할 수 있게 되었습니다.자랑하고 싶은 코드
"참조는 고정하되 최신 값은 참조"라는 까다로운 요구사항을 useRef + useCallback 조합으로 해결한 부분이 가장 만족스럽습니다. 처음에는 타입 때문에 고생했지만, 제네릭과 unknown[], as T 타입 단언을 사용해서 TypeScript의 복잡한 타입 시스템을 해결할 수 있었습니다.
개선이 필요하다고 생각하는 코드
useMemo 과도한 사용
E2E 테스트 통과 과정에서 하나씩 메모이제이션을 추가하다 보니 이렇게 됐는데, useMemo를 이렇게 많이 써도 되는 건지 잘 모르겠습니다. 특히 { message: state.message, type: state.type } 같은 경우도 useMemo로 감싸야 하는 건지, 그리고 이런 식으로 계속 메모이제이션을 추가하는 게 올바른 방법인지 판단이 서지 않습니다. 더 나은 구조가 있다면 개선하고 싶습니다.
학습 효과 분석
가장 큰 배움이 있었던 부분
ToastProvider에서 함수들이 매번 새로 생성되어ProductCard가 불필요하게 리렌더링되는 문제를 해결하면서, Context value의 참조 안정성이 얼마나 중요한지 알게 되었습니다.useMemo나useCallback을 사용하는 것이 아니라, 모든 불안정한 참조를 하나씩 메모이제이션해야 한다는 것을 배웠습니다. 하나라도 빠뜨리면 전체가 다시 리렌더링되는 경험을 통해 메모이제이션의 중요성을 깊이 이해할 수 있었습니다.추가 학습이 필요한 영역
unknown[]타입과as T타입 단언을 사용할 때 타입 안전성에 대한 고민이 필요할 것 같습니다.실무 적용 가능성
useAutoCallback,useShallowSelector같은 커스텀 훅을 직접 구현하면서 재사용 가능한 로직을 설계하는 방법을 배웠습니다. 이는 실제 프로젝트에서 공통 로직을 추상화할 때 유용할 것 같습니다.과제 피드백
과제에서 좋았던 부분:
과제에서 어려웠던 부분:
학습 갈무리
리액트의 렌더링이 어떻게 이루어지는지 정리해주세요.
리액트의 렌더링은 컴포넌트의
state나props가 변경될 때 발생하며, 크게 세 단계로 이루어집니다.forceUpdate호출 시 렌더링이 실행됩니다.이 과정은 실제 브라우저가 화면을 새로 그려(렌더링 유발) 비용이 가장 크게 발생합니다.
이러한 과정을 최적화하기 위해 React는
memo,useMemo,useCallback과 같은 도구를 제공합니다. 이들은 렌더 단계에서 props나 의존성이 변경되지 않았을 경우, 이전 렌더 결과를 재사용하여 불필요한 가상돔 생성 및 Diffing 과정을 건너뛰게 해줍니다.메모이제이션에 대한 나의 생각을 적어주세요.
메모이제이션은 "비용이 비싼 연산의 결과를 저장해두고, 동일한 입력에 대해서는 저장된 결과를 재사용하는 기술"이라고 생각합니다.
필요한 시점:
장점과 단점:
결론적으로, 메모이제이션은 성능 저하가 실제로 발생하는 지점을 프로파일링 도구로 측정한 후, 꼭 필요한 곳에 전략적으로 사용하는 것이 중요합니다.(그렇지만 저는 이번 과제에서 남발한게 아닌가 하는 느낌;;;)
컨텍스트와 상태관리에 대한 나의 생각을 적어주세요.
컨텍스트와 상태관리는 "컴포넌트 트리 전반에 걸쳐 흩어져 있는 데이터를 효율적으로 관리하고 공유하기 위한 솔루션"이라고 생각합니다.
필요한 이유:
장점과 단점:
주의점 및 해결책:
상태관리 라이브러리 직접 구현 경험:
Observer 패턴을 기반으로 한 상태 관리 시스템을 구현하면서:
Context vs 상태관리 라이브러리:
이번 과제를 통해 둘 다 결국 "상태 변경 시 구독자들에게 알림"이라는 동일한 패턴을 사용한다는 것을 알게 되었습니다.
리뷰 받고 싶은 내용