과제 체크포인트
배포 링크
https://hyunzsu.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 개선
과제 셀프회고
과제를 진행하면서 상세 문서화 작성했습니다.
기술적 성장
-
React hooks 내부 동작 원리 이해: useState, useRef, useMemo, useCallback의 내부 구현을 직접 작성하며 React의 렌더링 최적화 메커니즘을 체득
- 메모이제이션 패턴: useRef를 활용한 이전 값 저장과 shallowEquals를 통한 변경 감지 로직을 일관되게 적용
-
비교 알고리즘 구현: shallowEquals와 deepEquals 함수를 통해 JavaScript 객체 비교의 미묘한 차이점 학습
- 5단계 비교 프로세스: 참조 동일성 → 타입 검증 → null 체크 → 키 개수 비교 → 값 비교 순서로 최적화된 비교 로직 구현
-
최적화 전략 구현: useAutoCallback, useShallowState, useShallowSelector 등 실무에서 유용한 커스텀 훅 설계
- Stale Closure 방지: useAutoCallback에서 useRef와 useCallback 조합으로 항상 최신 값 참조하면서도 안정된 함수 참조 제공
- 참조 안정성: shallowEquals 기반으로 불필요한 리렌더링 방지하는 최적화 패턴 습득
자랑하고 싶은 코드
ToastProvider의 Context 분리
// Command Context와 State Context 분리
const ToastCommandContext = createContext<{
show: ShowToast;
hide: Hide;
}>({
show: () => null,
hide: () => null,
});
const ToastStateContext = createContext<{
message: string;
type: ToastType;
}>({
...initialState,
});
자주 변하는 상태(message, type)와 거의 변하지 않는 함수들(show, hide)을 별도 Context로 분리하여 불필요한 리렌더링을 방지하였습니다.
학습 효과 분석
React의 메모이제이션이 단순한 캐싱이 아닌, 참조 동일성 기반의 리렌더링 최적화 시스템임을 이해
학습 갈무리
리액트의 렌더링이 어떻게 이루어지는지 정리해주세요.
React의 렌더링 과정은 상태 변경 → 가상 DOM 생성 → 비교(Reconciliation) → 실제 DOM 업데이트 순으로 이루어집니다. 이번 과제에서 useState를 직접 구현하면서 렌더링 트리거의 핵심을 이해했습니다. React는 Object.is()로 이전 값과 새 값을 비교해서 다를 때만 리렌더링을 시작하죠. 그래서 setState([...arr])처럼 새로운 참조를 만들어야 변경을 감지할 수 있어요. 컴포넌트가 리렌더링되면 기본적으로 모든 하위 컴포넌트도 함께 리렌더링됩니다. 이때 Virtual DOM을 통해 이전 트리와 새 트리를 비교하고, 실제로 변경된 부분만 DOM에 반영하는 것이 React의 핵심 최적화입니다. 렌더링 최적화를 위해서는 memo로 컴포넌트 메모이제이션, useMemo로 값 메모이제이션, useCallback으로 함수 메모이제이션을 활용할 수 있습니다. 이들은 모두 얕은 비교를 통해 불필요한 재계산을 방지하는 원리로 동작합니다. 결국 React의 렌더링은 "참조 비교 기반의 변경 감지"와 "Virtual DOM을 통한 효율적인 업데이트"가 핵심이라고 정리할 수 있습니다.
메모이제이션에 대한 나의 생각을 적어주세요.
메모이제이션은 단순한 캐싱이 아니라 의존성 관리가 핵심이라는 걸 깨달았습니다. useCallback을 직접 구현하면서 의존성 배열의 중요성을 체감했어요. 의존성을 빼먹으면 stale closure 문제가 생기고, 너무 많이 넣으면 매번 새로운 함수가 생성되어 메모이제이션 효과가 사라지죠. 메모이제이션이 필요한 경우는 크게 두 가지입니다. 첫째는 expensive한 계산(복잡한 배열 처리, 수학 연산 등)을 반복하는 경우, 둘째는 자식 컴포넌트의 불필요한 리렌더링을 방지하는 경우죠. 하지만 메모이제이션을 남용하면 오히려 성능이 악화될 수 있습니다. 메모리 사용량이 늘어나고, 얕은 비교 연산 자체도 비용이기 때문이에요. 특히 자주 변경되는 값을 메모이제이션하면 비교 비용만 추가되는 상황이 발생합니다.
컨텍스트와 상태관리에 대한 나의 생각을 적어주세요.
Context와 상태관리가 필요한 이유는 prop drilling 문제와 전역 상태 공유 때문입니다. 깊은 컴포넌트 트리에서 상태를 전달하려면 중간 컴포넌트들이 불필요하게 props를 받아야 하고, 이는 코드의 복잡성을 크게 증가시키죠. 하지만 Context는 양날의 검입니다. ToastContext와 ModalContext를 개선하면서 깨달은 건데, Context 값이 변경되면 Provider 하위의 모든 컴포넌트가 리렌더링됩니다. 이는 성능상 큰 문제가 될 수 있어요. Context 설계 시 가장 중요한 것은 변경 빈도에 따른 분리입니다. 자주 변하는 상태(input value, loading state)와 거의 변하지 않는 상태(theme, user info)는 별도의 Context로 관리해야 합니다. 또한 useShallowSelector를 구현하면서 느낀 건데, "필요한 데이터만 구독"하는 것이 상태관리의 핵심이에요. Context의 대안으로는 상태를 적절히 끌어올리기(lifting state up), 컴포넌트 합성 패턴, 또는 외부 상태관리 라이브러리 사용이 있습니다. 특히 복잡한 전역 상태는 Zustand나 Redux 같은 라이브러리가 더 효율적인 구독 메커니즘을 제공하죠. 결론적으로 단순한 상태는 useState로, 중간 복잡도는 Context로, 복잡한 전역 상태는 전용 라이브러리로 접근하는 것이 현실적인 선택이라고 생각합니다. Context는 만능 해결책이 아니라 특정 상황에 적합한 도구로 봐야 해요.
리뷰 받고 싶은 내용
1. deepEquals 순환 참조 처리의 필요성과 구현 방법
현재 deepEquals는 순환 참조 처리 없이 단순 재귀로 구현되어 있습니다.
for (const key of keysA) {
if (!(key in recordB)) return false;
if (!deepEquals(recordA[key], recordB[key])) return false; // 순환 참조 시 스택 오버플로우
}
React의 props나 state에서 순환 참조가 발생하는 실제 사례가 얼마나 빈번한지, 그리고 WeakSet을 사용한 순환 참조 추적 로직의 성능 오버헤드와 메모리 사용량 증가를 고려했을 때 실용적인 개선인지 궁금합니다. 라이브러리 수준에서는 안정성을 위해 필요하지만, 애플리케이션 수준에서는 불필요한 복잡성일 수도 있을 것 같은데 어떤 기준으로 판단하면 좋을까요?
2. useAutoCallback의 개발자 경험
의존성 배열 관리의 복잡성을 해결하기 위해 useAutoCallback을 구현했습니다.
export const useAutoCallback = <T extends AnyFunction>(fn: T): T => {
const fnRef = useRef(fn);
fnRef.current = fn;
return useCallback((...args: Parameters<T>) => {
return fnRef.current(...args);
}, []) as T;
};
이 접근법이 실제 개발 실무에서 얼마나 유용할지 궁금합니다! 개발자는 의존성을 신경 쓰지 않아도 되는 편의성을 얻지만, 함수가 매번 새로 생성되어도 참조는 안정적이므로 "진짜 변경된 것만 감지"하는 React의 최적화 철학과는 거리가 있는 것 같아 질문합니다.
과제 피드백
안녕하세요 지수, 수고하셨습니다! 이번 과제는 React의 내장 훅들을 직접 구현해보면서 프레임워크가 어떻게 상태를 관리하고 최적화하는지 이론을 넘어 몸으로 깊이 이해하는 것이 목표였습니다.
코드 스타일이 정말로 최소한의 코드 구현만 하면서 간결하게 작성하려고 하는 스타일이 눈에 띄는데 참 잘했습니다! 가능하다면 가독성이 확보되는 수준에서 코드의 양은 줄일 수 있는게 좋죠. 계속 그러한 코드 스타일을 유지해주길 바래요!
렌더링, 메모제이션, 컨텍스트와 상태관리등은 React의 핵심 개념이며 해당 개념들을 실제로 구현을 해보니 왜 메모제이션을 필요할 때에만 하라고 하고 만능이 아닌 것인지, 코드를 해부하고 원리를 이해하는 것을 통해서 개발 과정과 선택 과정이 더 선명해지는 계기가 되었기를 바랍니다.
Q) deepEquals 순환 참조 처리의 필요성과 구현 방법
=> React 생태계에서 순환 참조는 실제로 그리 흔하지 않습니다. React가 아무래도 객체를 데이터로만 다루는 함수형 프로그래밍을 지향하다보니 props나 state는 대부분 직렬화 가능한 데이터인 경우가 많죠.
성능 측면에서는 WeakSet 연산 자체는 빠르지만 메모리 오버헤드가 있습니다. 일반적인 애플리케이션에서는 현재 구현으로 충분하고, 순환 참조가 예상되는 특수한 경우에만 추가하는 것이 실용적이에요.
언제나 성능과 메모리, 간결함과 복잡함의 트레이드 오프는 언제나 개발에서의 딜레마라 생각합니다. 대개는 최대한 간결한 구조를 유지하고자 하다가 문제가 제보(?)되다보면 점점 복잡해지거나 개선하는 식으로 만들어지곤 합니다.
Q) useAutoCallback의 개발자 경험과 실무 활용도
=> 해당 방식은 함수를 자식 컴포넌트의 props으로 넘길때에는 useCallback등으로 참조로 만들지 않으면 언제나 새로운 함수가 전달이 되므로 memo가 되지 않는 문제를 해결하는데 도움을 줍니다. 함수는 useMemo와 달리 비싼 로직이 아니니까요. 의존성 배열을 명시하지 않고 언제나 최신 함수를 전달하지만 참조는 유지하므로써 자식 컴포넌트로 props도 메모제이션이 가능하도록 하는 편의성 유틸리티 함수죠.
=> 편의냐? 아니면 명확성이냐? 도 개발자간의 취향이 갈리는 부분입니다. 물론 여기서 useAutoCallback의 경우 공식적인 hook이 아니라 학습을 위한 그리고 커뮤니티에서 만들어진 방식이고, useCallback은 명시적이고 공식적인 방식이다보니 아무래도 useCallback을 사용하겠죠? 대부분은 많이 사용하는 것을 사용하는 것을 선호하니까요.
이러한 개발자적인 고민들 철학에 대한 나의 취향 그리고 근거나 확신등을 고민을 통해서 만들어가는 것도 좋은 경험이 될거에요. 화이팅입니다!