[8팀 박창준] Chapter 1-3. React, Beyond the Basics#20
[8팀 박창준] Chapter 1-3. React, Beyond the Basics#20ckdwns9121 wants to merge 17 commits intohanghae-plus:mainfrom
Conversation
unseoJang
left a comment
There was a problem hiding this comment.
창준님 이번 1-3 과제도 수고 많으셨습니다.
창준님 PR을 보니 열정이...ㅋㅋㅋㅋ흐름 시각화에 메모이제이션 성능 측정까지 하시고...
ㅋㅋㅋㅋㅋㅋㅋ메모이제이션은 나중에 한번더 배우긴 할텐데 미리 선행학습하신것도 칭찬해드리고싶네요
이번 과제도 수고 많으셧고 다음 2-1과제 너무 무리하진 마시고 화이팅해주시면 감사하겠습니다~!
|
|
||
| const showWithHide: ShowToast = (...args) => { | ||
| const hideAfter = useMemo(() => debounce(hide, DEFAULT_DELAY), [hide]); | ||
| const showWithHide: ShowToast = useAutoCallback((...args) => { |
There was a problem hiding this comment.
창준님 여기서 useAutoCallback으로 자동으로 콜백 함수를 만들어주는 커스텀 훅을 잘 만들어주셧네요.
다음에 복습을 하시거나 리팩토링을 하시게되면 유열님이 useCallBack으로 안정성과 명시성을 더한 코드를 한번 확인해주시면 도움이 될것같아요!
| import { BASE_URL } from "./constants.ts"; | ||
| import { createRoot } from "react-dom/client"; | ||
|
|
||
| // |
There was a problem hiding this comment.
아 ㅋㅋ 이거 테스트 푸쉬할려고 쓴건데 어케 발견하셧대~
There was a problem hiding this comment.
전반적으로 잘 구조화된 shallow equality 유틸 함수인것같아요
함수 분리도 좋고 타입 안정성도 좋은것같습니다
There was a problem hiding this comment.
useRef를 기반으로 상태 변경을 감지하는 훅을 구현하셧네요
여기서는 useCallback으로도 구현할수도 있고 useRef 로도 구현할수 있는데 리액트에선 조금더 직관적이고 추적이 가능한 useCallback를 더 선호할것 같습니다.
하지만 선능에서 생각해본다면 창준님이 구성해주신 useRef 방식이 훨씬더 효율적입니다~!
이 두개의 차이점을 저도 이번 기회에 공부하고 갑니다.
There was a problem hiding this comment.
오호 setState가 함수이니 useCallback으로 해도 괜찮을거같네요 저는 ref에 함수 참조를 넣는 방식으로 구현해봤습니다. ㅎㅎ
과제 체크포인트
배포 링크
https://devchangjun.github.io/front_6th_chapter1-3/
기본과제
equalities
hooks
High Order Components
심화 과제
hooks
context
과제 셀프회고
기술적 성장
이번 과제를 진행하면서 기존에 사용하였던 React 훅들이 어떤 매커니즘으로 만들어졌고, 왜 이렇게 동작을 하는지 코드레벨에서 깊이있게 이해할 수 있었던 과제였습니다.
특히 메모이제이션에 대해서 더 깊이 고민해볼 수 있었습니다. 예전에는
useMemo,useCallback,useRef등 React에서 제공하는 훅들을 단순히 "최적화를 위한 도구" 정도로만 생각했었는데, 이번 과제를 통해좀 더 깊이있게 학습할 수 있었습니다.
useSyncExternalStore 훅에 대한 학습
공부하다 보니 재미있는 사실을 발견했습니다.
React가 성능과 사용자 경험을 높이기 위해 동시성 렌더링을 도입했는데, 아이러니하게도 이게 상태가 찢어지는(Tearing) 문제를 만들어냈더라고요. 즉, UX 문제를 해결하려고 도입한 기능이 새로운 문제를 만든 셈이죠. 그걸 또 해결하기 위해 등장한 게 바로 useSyncExternalStore라는 훅이었고요.
React는 단순히 새로운 기능만 쌓아가는 게 아니라 기존의 구조적 문제를 하나하나 개선해가며 점진적으로 아키텍처를 정제해가는 프레임워크라는 그런 진화과정을 엿볼 수 있었던것 같습니다.
useSyncExternalStore훅 관련된 내용은React 18부터 도입된 useSyncExternalStore 훅 살펴보기라는 포스팅에 정리하였습니다.
useAutoCallback와 같은 유연한 커스텀훅을 설계하는 사고방식 습득
과제를 진행하면서 제일 인상깊었던 코드는
useAutoCallback과 같은 커스텀 훅이였습니다.이전까지는 useCallback만 사용하면 함수의 재생성을 막을 수 있다고 막연히 생각했지만, 실제로는 의존성 배열에 따라 최신 상태를 참조하지 못하는 문제가 종종 있습니다. (의존성 누락).
그 문제를 useRef와 useCallback을 조합해서 항상 최신 함수를 참조하면서도 함수의 참조값은 변하지 않게 만드는 패턴이 매우 유용하다는 것을 직접 구현하며 체감할 수 있었습니다.
useAutoCallback은 아래와 같은 과정으로 구현하였습니다.1. useRef로 최신 함수 보관
2. useCallback으로 함수 참조를 고정
[](빈배열)이므로 최초 마운트 시에만 콜백을 생성하고, 이후에는 동일한 함수 참조를 반환합니다.3. 최종 구현된 코드
이 구현으로 인해서
useCallback의 단점인 state closure문제를 우회하면서도 불필요한 함수 재생성 없이 항상 최신상태를 참조할 수 있는 커스텀 훅을 구현하였습니다. 이 패턴을 활용해서 토스트에서showWithHide함수에 적용하여 타이머 리셋 문제를 개선할 수 있었습니다.이렇게 상황에 따라 유연하게 필요한 훅을 재조합하거나 필요시 새롭게 구현할 수 있는 사고방식을 기르게 되었습니다.
또한, 토스트 UI를 Context 기반으로 구현한
ToastProvider를 리팩토링하면서 불필요한 리렌더링 방지와 관심사 분리의 중요성을 직접 경험해볼 수 있었습니다.초기에는 상태(state)와 액션(show/hide)을 하나의 Context에 통합한 구조를 사용했는데, 이 경우
message가 변경되면 액션만 사용하는 컴포넌트도 불필요하게 리렌더링되는 이슈가 있었습니다. 이를 해결하기 위해 다음과 같은 리팩토링을 진행하였습니다.1. 상태와 액션을 별도의 Context로 분리
이전에는 하나의 Context에
message,show(),hide()등 모든걸 넣었는데 상태와 액션을 분리해 필요한 값만 구독해서 불필요한 렌더링을 방지하였습니다.2.
value객체는useMemo로 메모이제이션기존 코드는
value에 객체 리터럴을 바로 넘기는 방식이였습니다.이 객체는 매번 렌더링할 때 새로운 객체로 생성됩니다. 즉
이렇게 값은 같아도 참조하고 있는 메모리 주소가 다르기 때문에 이 컨텍스트를 사용하는 모든 하위 컴포넌트가 리렌더링이 되는 문제가 있었습니다.
그래서
useMemo를 활용해서show와hide함수가 불필요하게 재생성되지 않도록 고정시켰습니다.3. showWithHide를 useAutoCallback으로 고정된 참조 + 최신상태를 보장하게 구현
위에서 언급한
useAutoCallback을 통해 아래와 같이 리팩토링하였습니다.이 함수는 다음과 같은 이점을 가집니다.
show,hideAfter를 참조하므로 클로저에 의한 stale 상태 문제를 방지할 수 있습니다.show()를 연속으로 호출한다 해도debound가 정상적으로 리셋 되면서 가장 마지막 호출 기준으로 토스트 메시지가 닫히게 됩니다.학습 효과 분석
1. 리액트는 왜 얕은비교 방식을 채택했을까?
React는 상태 비교에서 기본적으로 **얕은 비교(shallow comparison)**를 사용합니다. 이는 단순히 "성능 때문에 그렇다"는 얘기보단 어떤 철학적인 설계에 가깝다고 생각했습니다.
===연산 및 한 레벨의 비교로 끝나기 때문에 예측 가능하고 빠릅니다.즉 React는 개발자에게 불변성 유지를 책임지게 하고, 프레임워크는 그 위에서 가볍고 빠른 비교를 제공하는 방식을 택했다고 생각합니다. 이로 인해 컴포넌트 리렌더링 판단, memo, useMemo, useCallback 같은 훅의 최적화가 모두 얕은 비교를 기준으로 설계되었다고 합니다.
2. useAutoEffect, useAutoMemo와 같은 훅은 만들 수 없을까?
이번 과제에서
useAutoCallback을 구현하면서 참조는 고정하면서 항상 내부에서 최신상태를 받아올 수 있는 훅도 만들 수 있지않을까? 그것을 만들면 완전 혁신아니야!? 라는 기대도 했습니다.이 아이디어를 기반으로
useAutoEffect같은 훅을 상상해봤습니다. 예를 들어, 의존성 배열 없이도 최신 상태를 반영하는 effect 훅을 만들 수 있다면 매번 의존성 배열을 일일이 나열하지 않아도 돼서 굉장히 편리할 것 같았습니다.그래서 아래와 같은 시도를 했습니다.
문제점
마찬가지로
useAutoMemo도 만들어볼 수 있을 것 같았지만문제점
이 훅들을 구현해보면서
useAutoCallback은 언제 실행될지를 사용자가 제어하기 때문에 최신 상태 참조에 의미가 있었지만, 제가 시도하려 했던 useAutoEffect, useAutoMemo는 React가 정한 타이밍에 실행되는 훅이기 때문에 내부에 최신 참조(ref)가 있어도 그 타이밍이 고정되어 있어서 최신 상태를 반영할 수 없는 구조적인 한계가 있다는것을 깨달았습니다.3. useShallowSelector의 동작 원리 분석
useShallowSelector의 동작 원리가 잘 이해 되지 않아서 천천히 디버깅해보며 학습한 내용을 정리하였습니다.
흐름 상세 설명
useStore(또는useRouter)를 호출할 때, selector 함수를 함께 전달합니다.useStore내부에서useShallowSelector(selector)를 호출하여, selector 기반의 shallowSelector 함수를 만듭니다.useSyncExternalStore를 통해 store의 subscribe로 상태 변화를 구독합니다.이 흐름을 시각화 하였습니다.
sequenceDiagram participant 컴포넌트 participant useStore participant useShallowSelector participant store participant shallowEquals 컴포넌트->>useStore: selector 전달하며 호출 useStore->>useShallowSelector: selector 전달 useStore->>store: subscribe 등록 useStore->>store: getState()로 상태 조회 useStore->>useShallowSelector: shallowSelector(state) 호출 useShallowSelector->>shallowEquals: 이전 값과 selector(state) 결과 비교 alt 값이 다름 shallowEquals-->>useShallowSelector: false useShallowSelector-->>useStore: 새 값 반환 (prevSelectedRef 갱신) useStore-->>컴포넌트: 새 값 전달, 리렌더링 발생 else 값이 같음 shallowEquals-->>useShallowSelector: true useShallowSelector-->>useStore: 이전 값 반환 useStore-->>컴포넌트: 이전 값 전달, 리렌더링 없음 end store-->>useStore: 상태 변경 시 notify (구독자 콜백 호출) useStore->>useShallowSelector: shallowSelector(state) 재호출 (이전과 동일 흐름 반복)4. 실제 ProductList에 메모이제이션을 도입하여 성능 측정
이 컴포넌트는 무한 스크롤 구조로, 스크롤이 일정 위치에 도달할 때마다 새로운 데이터를 계속 fetching 해오는 구조입니다.
products상태가 갱신될 때마다ProductList가 리렌더링되고, 그에 따라 내부의ProductCard들도 전부 리렌더링 되는 문제가 있었습니다.성능 측정 시나리오
13.5s for 96.2ms– 프레임 드랍 위험 있음12.3s for 27.4ms– 개선됨, 여전히 최적화 여지 있음ProductCard가 매번 전부 리렌더링됨 (key=...)MemoizedComponent로 감싸진 컴포넌트들은 대부분<0.1ms, 재렌더링 회피됨0.3~0.5ms씩 소모되는 카드들이 20개 이상 → 누적 비용 큼<0.1ms로 스킵되며 누적 비용이 대폭 감소메모이제이션을 적용하지 않았을 때는
ProductCard가 props가 같음에도 모두 다시 렌더링되어 96.2ms에 달하는 프레임이 발생했고,이는 사용자가 스크롤을 내릴 때 프레임 드랍이나 버벅임을 느낄 수 있는 수준입니다.
반면, memo를 적용한 후에는 동일 props를 가진 카드들이 렌더링을 건너뛰게 되면서 전체 렌더링 시간은 27.4ms로 약 3.5배 이상 감소했습니다. 특히 각 ProductCard 컴포넌트가 <0.1ms 수준으로 최적화되어 있어 불필요한 리렌더링이 일어나지 않았음을 확인할 수 있었습니다.
메모이제이션을 적용한 코드는 여기서 확인할 수 있습니다.
과제 피드백
React의 훅을 직접 단계별로 구현해보면서 동작원리를 자세히 익힐 수 있어서 좋았습니다.
학습 갈무리
리액트의 렌더링이 어떻게 이루어지는지 정리해주세요.
리액트의 렌더링은 어떻게 이루어질까?에 정리하였습니다.
메모이제이션에 대한 나의 생각을 적어주세요.
React에서
useMemo,useCallback,memo와 같은 메모이제이션 도구는 성능 최적화를 위한 수단이지, 반드시 적용해야 하는 규칙은 아니라고 생각합니다.개발자들 사이에서 자주 나오는 얘기 중 하나는 다음과 같은 토론입니다.
저는 이 중에서 성능 이슈가 발생했을 때 최적화하자에 가깝습니다.
실제로 React useMemo의 공식문서에도 아래와 같은 내용이 있습니다.
❌ 무분별한 메모이제이션의 문제점
1. 의존성 배열 실수
useMemo(() => ..., [a, b])처럼 작성했을 때, 의존성 하나만 빠져도 오래된 값을 계속 참조하게 됩니다. 특히 협업 중 리팩토링 시, 눈에 띄지 않는 버그로 발전하기 쉽습니다.2. 디버깅과 테스트 복잡도 증가
stale value와 관련된 버그는 눈에 띄지 않기 때문에 추적이 어렵고, 테스트 코드에서도 예외적인 흐름을 만들어냅니다.
3. 불필요한 메모리 사용과 렌더링 최적화 실패
모든 것을 메모이제이션한다고 해서 항상 성능이 좋아지는 것은 아닙니다. 오히려 메모이제이션 오버헤드가 렌더링 비용보다 커질 수도 있다고 생각합니다.
✅ 내가 따르는 메모이제이션 원칙
불변성을 지켜야 하는 상황에서만 메모이제이션을 쓴다.
예: Context의 value 객체, 복잡한 계산 로직 등
렌더링 병목이 실제로 관측되는 경우에만 메모이제이션을 적용한다.
예: 수천 개의 리스트 렌더링, 연산이 무거운 경우 등
불필요한 최적화는 하지 않는다.
나중에 코드를 이해하는 사람에게는 오히려 방해 요소가 될 수 있다.
메모이제이션은 "언제든 쓰면 좋은 도구"가 아니라 "쓸 줄 아는 사람이 신중하게 선택해야 하는 칼"이라고 생각합니다.
이 관점을 바탕으로 실무나 과제에서도 메모이제이션 도구를 무분별하게 남용하지 않고, 필요한 곳에만 적절히 적용하는 습관을 가지려 하고 있습니다.
컨텍스트와 상태관리에 대한 나의 생각을 적어주세요.
많은 사람들이 React의 Context를 전역 상태 관리 도구로만 이해하지만 실제로는 의존성 주입(DI) 시스템에 더 가까운 개념이라고 생각합니다. Context는 상위 컴포넌트에서 특정 값을 설정해 하위 컴포넌트로 직접 전달할 수 있게 해주며, 이 구조는 전통적인 DI 패턴과 매우 유사합니다. 실제로 많은 라이브러리들이 이 구조를 적극적으로 활용하고 있습니다.
1. ThemeProvider
하위의 모든 Component에서
theme객체를 참조할 수 있게 해주는 구조로 전형적인 설정값 주입의 대표적인 예시입니다.2. TanStack Query – QueryClientProvider
API 캐싱과 상태 관리를 담당하는 QueryClient를 트리 전체에 주입하여 하위 컴포넌트에서는 별도의 설정 없이 useQuery, useMutation 등의 훅을 통해 해당 클라이언트를 자동으로 참조하고 사용할 수 있게됩니다.
3. i18next – I18nextProvider
번역 객체(i18n 인스턴스)를 하위 컴포넌트에 주입해서 어디서든 다국어 기능을 사용할 수 있게 합니다.
이러한 패턴들은 모두 다음과 같은 공통점을 가집니다
이것은 전형적인 의존성 주입 패턴이며, React의 Context가 전역 상태 관리 도구라기보다 애플리케이션의 구성 요소를 주입하는 용도에 더 적합하다는 걸 보여주는 대표적인 예시들입니다.
결론
리뷰 받고 싶은 내용
Q1. 업무를 진행하실 때 메모이제이션을 다 해놓은 편인가요? 아니면 성능 이슈가 발생했을 때 적용하시나요?
Q2. 만약 성능이슈가 발생해서 메모이제이션을 적용하신 경험이 있다면 한번 소개해주시면 감사합니다!
Q3. 컨텍스트와 상태관리에 대한 코치님의 생각도 궁금합니다.