[1팀 이은지] Chapter 1-3. React, Beyond the Basics#49
Open
angielxx wants to merge 22 commits intohanghae-plus:mainfrom
Open
[1팀 이은지] Chapter 1-3. React, Beyond the Basics#49angielxx wants to merge 22 commits intohanghae-plus:mainfrom
angielxx wants to merge 22 commits intohanghae-plus:mainfrom
Conversation
- 이전 상태를 useStore의 getSnapshot 내부에서 계산하고 비교
Amelia-Shin
reviewed
Jul 26, 2025
Comment on lines
+19
to
+21
| const open = useCallback((newContent: ReactNode) => setContent(newContent), []); | ||
|
|
||
| const close = () => setContent(null); | ||
| const close = useCallback(() => setContent(null), []); |
Author
There was a problem hiding this comment.
provider에서 제공하는 메서드 함수가 변경될 일이 없어서 의존성 배열이 없는 useCallback으로 메모이제이션 하는 것이 적절하다고 생각했습니다!
Amelia-Shin
reviewed
Jul 26, 2025
Comment on lines
+8
to
+15
| // 항상 같은 함수 참조를 반환 | ||
| const stableCallback = useRef((...args: unknown[]) => { | ||
| // 여기서 fn(...args)를 하면 클로저가 생성 | ||
| // 최신 상태, props를 사용하는 fn을 받아도 처음 fn만 사용하게 됨 | ||
| return fnRef.current(...args); | ||
| }); | ||
|
|
||
| return stableCallback.current as T; |
There was a problem hiding this comment.
useRef를 사용해서 한 방법이 인상깊어요! useMemo로 바꿔서 쓴다면 어떤 차이점이 있는지도 궁금해요!
DEV4N4
reviewed
Jul 26, 2025
| const subscribe = (fn: Listener) => { | ||
| listeners.add(fn); | ||
|
|
||
| return unsubscribe; |
Amelia-Shin
reviewed
Jul 26, 2025
| // deps와 value는 1:1 대응 | ||
| const memoizedState = useRef<{ value: T; deps: DependencyList } | null>(null); | ||
|
|
||
| const compareFunc = _equals || shallowEquals; |
There was a problem hiding this comment.
compareFunc 를 _equals 또는 shallowEquals 둘 중 하나로 지정해서 쓸 수 있을 거같은데, || 조건으로 묶은 이유가 있을까요?
jinsoul75
reviewed
Jul 26, 2025
| export function useRef<T>(initialValue: T): { current: T } { | ||
| // useState를 이용해서 만들어보세요. | ||
| return { current: initialValue }; | ||
| const [ref] = useState(() => ({ current: initialValue })); |
DEV4N4
reviewed
Jul 26, 2025
| if (typeof a !== typeof b) return false; | ||
|
|
||
| if (typeof a !== "object" || a === null || typeof b !== "object" || b === null) { | ||
| return a === b; |
There was a problem hiding this comment.
Suggested change
| return a === b; | |
| return Object.is(a, b); |
이렇게 했어도 괜찮았을 것 같아요!
Amelia-Shin
reviewed
Jul 26, 2025
Comment on lines
+1
to
+9
| function isArray(value: unknown): value is unknown[] { | ||
| return Array.isArray(value); | ||
| } | ||
|
|
||
| function isObject(value: unknown): value is Record<string, unknown> { | ||
| return typeof value === "object" && value !== null && !isArray(value); | ||
| } | ||
|
|
||
| export { isArray, isObject }; |
areumH
reviewed
Jul 26, 2025
Comment on lines
+8
to
+9
|
|
||
| export { isArray, isObject }; |
There was a problem hiding this comment.
역시 조건문에 공통으로 쓰이는 함수를 따로 분리하니까 깔끔하네요!! 👍
Comment on lines
+19
to
+29
| const snapshot = useSyncExternalStore(subscribe, getSnapshot); | ||
|
|
||
| function subscribe(onStoreChange: () => void): () => void { | ||
| const unsubscribe = storage.subscribe(onStoreChange); | ||
|
|
||
| return () => unsubscribe(onStoreChange); | ||
| } | ||
|
|
||
| function getSnapshot(): T | null { | ||
| return storage.get(); | ||
| } |
There was a problem hiding this comment.
저와 다르게 따로 분리하여 연결해주는거도 각각의 역할이 더 잘 보이는 것 같아서 좋은 것 같아요!! 각자의 장점이 있는거로,, 👏
Comment on lines
+61
to
+62
| <ToastContext.Provider value={value}> | ||
| <ToastActionContext.Provider value={action}> |
There was a problem hiding this comment.
마이너) ToastContext 가 리렌더링을 더 유발하니까 아무래도 ToastContext가 안쪽에 있는게 더 낫지 않을까 하는 의견 내봅니다.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.

과제 체크포인트
배포 링크
https://angielxx.github.io/front_6th_chapter1-3/
기본과제
equalities
hooks
High Order Components
심화 과제
hooks
context
과제 셀프회고
새로 학습한 내용
1. 실제 React가 hook을 관리하는 방식
Hook을 구현하기 전 실제 React에서는 어떻게 하고 있는지 알아봤습니다. React는 훅을 상태 배열(Linked List)과 인덱스를 기반으로 동작하는 것을 알게 됐습니다. 이 내용을 통해 왜 hook을 조건문이나 반복문 안에서 쓰면 안되는지 이해하게 됐습니다.
🔸 핵심 개념
“React는 각 훅이 어느 순서로 호출되었는지 기억하고, 렌더링마다 그 순서에 맞는 상태값을 꺼내서 반환한다.”
🔸 Hook 상태 저장 구조 (개념적 예시)
hooks
currentHookIndex
React의 내부에서 Hook 상태는 실제로 객체 형태로 저장되며, 연결 리스트 형태로 되어 있습니다. 이것을 내부에서는 workInProgressHook이라는 포인터로 순차적으로 탐색하며 useState, useEffect 등에서 상태를 저장하거나 꺼냅니다.
🔸 예시: useState 구현 흉내
만약 컴포넌트가 이렇게 생겼다면
React는 이 컴포넌트를 렌더링할 때, hooks[] 배열을 다음처럼 구성합니다:
2. hook의 호출 순서가 중요한 이유
1번 내용을 기반으로 hook 호출 순서가 왜 중요한 지 hook 상태 관리 구조를 기반으로 살펴보겠습니다.
❌ hook의 호출 순서가 꼬이는 예:
1차 렌더링 시 condition === true라면
2차 렌더링 시 condition === false라면
React는 각 컴포넌트의 상태를 렌더링 “순서(index)”로 저장하고 꺼내오기 때문에, Hook의 호출 순서가 바뀌면 완전히 잘못된 상태를 꺼내오게 됩니다!
🔸 React의 실제 hooks 구조 맛보기:
React는 내부적으로 hooks[] 배열이 아닌, **연결 리스트(Linked List)**로 훅 상태를 저장합니다:
각 Hook은 다음 Hook을 .next로 가리키고 memoizedState는 훅의 실제 상태값
🔸 React가 연결 리스트를 사용하는 이유
2. React의 얕은 비교
얕은 비교, 깊은 비교에 대해 정확하게 이해하지 못하고 있는 것 같아 비교함수를 구현하기 전에 얕은 비교와 깊은 비교의 개념과 React에서 쓰이는 얕은 비교의 사용이유와 예시를 학습했습니다.
🔸 얕은 비교 (shallow comparison)
객체의 참조값 또는 1단계 프로퍼티만 비교
숫자, 문자열 등의 원시타입은 값을 비교하고 배열, 객체 등 참조 타입은 값 혹은 속성을 비교하지 않고, 참조값을 비교합니다.
예: ===, Object.is, shallowEqual() 등
🔸 깊은 비교(deep comparison)
객체의 모든 하위 프로퍼티를 재귀적으로 비교
예: Lodash의 _.isEqual(), 커스텀 재귀 함수 등
🔸 예시:
🔸 얕은 비교가 사용되는 곳
React는 퍼포먼스를 위해 기본적으로 얕은 비교를 사용합니다.
🔸 얕은 비교가 사용하는 이유
3. Object.is
React 내부적으로 Object.is를 사용한다고 2번을 통해 알게 됐는데, 비교 함수를 구현할 때 Object.is를 사용하자니 정확히 Object.is가 어떻게 동작하는지 몰라 먼저 정리해봤습니다.
Object.is()는 자바스크립트에서 값의 **동일성(sameness)**을 비교하는 내장 함수로, 자주 쓰이는 ===과 유사하지만, 미묘한 차이점이 존재한다.
🔸 Object.is()란?
+0 vs -0,NaN vs NaN===결과Object.is()결과1,1truetrue[],[]falsefalsenull,undefinedfalsefalse+0,-0truefalseNaN,NaNfalsetrue🔸 왜 Object.is를 사용해야 하는가?
+0과 -0을 구분해야 하는 경우 : 수학적 연산에서 양/음 0을 구분해야 할 때 유용
NaN을 자기 자신과 비교 가능하게
🔸 Object.is Polyfill
어떤 분이 디스코드에서 폴리필 얘기를 하셔서 궁금해서 찾아봤습니다.
Object.is()는 ES6(ECMAScript 2015)에서 도입된 함수이기 때문에, **구형 브라우저(특히 IE)**에서는 지원하지 않을 수 있습니다. 그래서 구형 브라우저에서도 같은 동작을 하도록 대체 함수를 만들어 사용할 수 있습니다.이렇게 하면 모든 코드에서 Object.is()를 안전하게 호출할 수 있게 됩니다.
4. useSyncExternalStore 학습
태어나서 처음 들어보는 useSyncExternalStore 훅을 사용했지만, 생성 배경이나 해결해주는 문제들에 대해선 자세히 몰라 과제를 모두 완성한 후 내용을 더 찾아봤습니다. (영감을 주신 오하늘님께 감사를..글 재밌게 잘 읽었습니다.)
🔸 useSyncExternalStore란?
외부 상태 저장소(external store)의 상태를 React 컴포넌트와 동기적으로 연결(sync)해주는 훅
🔸 왜 useSyncExternalStore가 생겼는가?
useSyncExternalStore는 React 18에서 새롭게 추가된 훅인데, 이 훅이 생겨난 배경은 실제 React 사용자들의 문제점을 해결하기 위해서라고 합니다.
React 개발자들이 외부 상태(store)를 연결하기 위해 기존에 아래와 같은 방식을 사용한다면,
이 방식에서 발생하는 문제점은,
예를 들어:
useEffect는 서버에서는 실행되지 않기 때문에 서버에서 클라이언트로 넘어올 때 상태가 달라지는 “hydration mismatch” 발생할 수 있습니다.
🔸 useSyncExternalStore가 해결한 방법
getSnapshot()은 렌더링이 시작되기 전에 호출되어, 컴포넌트가 항상 최신 상태를 기준으로 렌더링되도록 보장합니다.
useSyncExternalStore는 외부 상태가 렌더링 중에 변경되었는지를 감지하고,
렌더링이 커밋되기 전에 변경이 감지되면 렌더링을 무효화하고 다시 시작합니다.
세 번째 인자인 getServerSnapshot()을 통해 서버에서 사용할 초기 상태를 미리 정의할 수 있습니다.
hydration mismatch(서버-클라이언트 불일치) 오류를 방지할 수 있습니다.
🔸 사용법
hook 구현 및 트러블 슈팅
1. useRef
useRef는 리렌더링을 트리거하지 않는 내부적으로 값이 유지되는 객체를 반환해야 합니다. 클로저, 글로벌 변수 등 다양한 방법을 시도해봤지만 모두 제대로 동작하지 않아, React의 훅 시스템을 이용하여 ref 객체를 생성하는 방식을 사용했습니다.
useState에 함수를 인자로 넘기면, 이 함수는 컴포넌트가 처음 마운트될 때 딱 한 번만 실행되기 때문에 하나의 useRef의 인스턴스의 값을 유지할 수 있는 객체 하나를 만들어 사용할 수 있습니다.
이 방법이 맘에 들지 않아 React의 useRef 구현 방식을 살펴보았으나 hooks의 memoizedState에 useRef의 상태를 보관한다고 하여, 실제로 구현해보고 싶은 마음이 있었지만 이번 과제에서 실제로 구현해보기에는 무리일 것 같아 학습으로만 남깁니다!
2. useMemo
"계산된 값과 의존성 배열을 저장해놓고, 의존성 배열의 변경 여부를 확인하고 그에 따라 새로 계산하거나 이전에 계산한 값 반환"하는 것을 useMemo의 요구사항으로 정의했습니다.
❌ 첫번째 시도 : 매번 재계산함
원인
✅ hasMemoMounted 변수를 함수 외부에 저장
hasMemoMounted를 useMemo 함수 외부에 두어, 함수가 재호출되더라도 이전 렌더링 시점을 기억하게 만들면, 매번 재계산되지 않고 의존성 비교를 통해 메모이징된 값을 재사용할 수 있게 됩니다.
모든 테스트를 통과하지만 문제가 있습니다...hasMemoMounted를 useMemo 바깥에 두면, 모든 컴포넌트 인스턴스가 이 전역 값을 공유하게 되어 버그가 발생할 수 있습니다.
✅ 근본적인 해결: 렌더링 컨텍스트별 상태 저장
React는 각 컴포넌트마다 자체적인 Hook 상태 저장소를 가지고 있고, 각 훅 (useMemo, useState, useRef 등)의 호출 순서를 따라 인덱스를 기반으로 상태를 구분하게 하려면, useRef를 사용할 수 밖에 없을 것으로 결론을 지었습니다.
3. useAutoCallback
요구사항 "useAutoCallback으로 만들어진 함수는, 참조가 변경되지 않으면서 항상 새로운 값을 참조한다."를 분석해보았습니다.
❓ "참조가 변경되지 않는다"란?
함수 객체의 참조(주소)가 변하지 않는다는 뜻입니다. 즉, 리렌더링이 여러 번 일어나도, callback === callback이 항상 true입니다.
❓ "항상 새로운 값을 참조한다"란?
함수 내부에서 사용하는 값(예: state, props 등)이 항상 최신 값을 참조한다는 뜻입니다. 즉, 함수가 오래전에 만들어졌더라도, 내부에서 사용하는 값은 "현재 시점의 최신 값"입니다.
❓ 왜 이런 패턴이 필요할까?
❌ 1차 시도
원인
✅ 해결 : 최신 fn을 useRef로 저장
내부에서 사용하는 fnRef.current는 항상 최신 fn을 가리키므로, 콜백 함수가 항상 최신 상태/props를 사용할 수 있습니다.
4. useStore
useStore를 구현하면서 제가 생각했던 구현 방향성은 아래와 같습니다.
🔸 useSyncExternalStore의 동작 원리
❌ 동일 상태값으로 업데이트해도 리렌더링되는 문제
원인
✅ 해결
1. 장바구니 추가 시 ProductCard가 모두 재렌더링
❌ 같은 memo 컴포넌트에서 다른 prevProps, props가 찍힘
위 구조에서 prevProps, props를 관리자 도구에서 찍어보면 아래처럼 각 ProductCard 컴포넌트가 렌더링될 때마다 prevProps와 props가 서로 다른 객체로 출력되는 현상을 확인했습니다.
❌ 원인
✅ 해결
2. 장바구니를 추가하거나 삭제했을 때, 토스트 호출로 인하여 리렌더링
ToastProvider 최적화 후에도 계속 ProductCard가 재렌더링되는 문제가 있었습니다. ToastProvider에 사용된 useMemo 콜백에 콘솔로그 찍어봤는데 장바구니 담기 버튼 클릭할 때마다 콘솔 찍히는 거 확인하고, useMemo가 제대로 동작하고 있지 않은 것을 발견했습니다.
❌ useMemo 이전 구현 방식
원인
해결
❓ 잘못 구현했음에도 테스트 통과한 이유
학습 효과 분석
학습 갈무리
리액트의 렌더링이 어떻게 이루어지는지 정리해주세요.
리액트의 렌더링 과정
렌더링 최적화 방법
메모이제이션에 대한 나의 생각을 적어주세요.
메모이제이션은 "비싼 연산의 결과를 저장해두고, 동일한 입력이 들어오면 저장된 값을 재사용하는 최적화 기법"입니다.
언제 필요할까?
사용하지 않으면?
장점
단점
❌ useCallback 남용 예시
-> 로직이 단순한 함수에 useCallback 쓰는 건 과도한 최적화, 특히 자식 컴포넌트가 memo되지 않은 경우엔 의미 없음
결론
컨텍스트와 상태관리에 대한 나의 생각을 적어주세요.
컨텍스트와 상태관리가 필요한 이유
사용하지 않으면?
장점
단점
대안/주의점
결론
리뷰 받고 싶은 내용