과제 체크포인트
배포 링크
https://jheejindev.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 개선
과제 셀프회고
기술적 성장
- 이번 과제를 통해 useMemo, useCallback, shallowEquals,deepEquals 등 React 내부 훅들의 작동 원리를 이해하고, 이를 직접 구현해보는 경험을 했습니다.
- Context 기반 전역 상태에서 불필요한 리렌더링 문제를 인지하고, 이를 memo, useMemo, useCallback으로 해결하며 최적화 방향에 대해 실질적인 개선 경험을 쌓았습니다.
- 또한 얕은 비교(shallow compare), 깊은 비교(deep compare)의 차이를 체감하고, 상태 변경에 따른 리렌더링 여부를 제어하는 데 직접 활용했습니다.
자랑하고 싶은 코드
이번 과제에서는 자랑할 만한 코드는 없지만, 앞으로 더 열심히 개선하고 발전해나가겠습니다!
개선이 필요하다고 생각하는 코드
Context로 전역 상태 관리를 해보고 나니 렌더링 최적화에 관심이 생겼습니다. 기존에는 Context를 사용해 Toast 상태를 관리했으나, Provider 아래 모든 컴포넌트가 상태 변경 시 불필요하게 리렌더링되는 단점이 있었습니다. 새 브랜치에서 createStore를 활용해 전역 상태를 구현하고, 필요한 컴포넌트만 구독하도록 하여 불필요한 리렌더링을 줄이는 작업을 시험 삼아 진행해 보았습니다.
전역 토스트 상태를 위한 커스텀 스토어와 리듀서 정의
// Toast 상태 타입 정의
export interface ToastState {
type: "success" | "error" | "warning" | "info";
visible: boolean;
message: string;
}
// 액션 타입 정의
export const TOAST_ACTIONS = {
SHOW_TOAST: "toast/show",
HIDE_TOAST: "toast/hide",
} as const;
// 초기 상태
const initialState: ToastState = {
type: "info",
visible: false,
message: "",
};
// 상태 변경 로직 정의 (reducer 방식)
const toastReducer = (state: typeof initialState, action: any) => {
switch (action.type) {
case TOAST_ACTIONS.SHOW_TOAST:
return {
...state,
visible: true,
message: action.payload.message,
type: action.payload.type || "info",
};
case TOAST_ACTIONS.HIDE_TOAST:
return {
...state,
visible: false,
message: "",
type: action.payload?.type || "info",
};
default:
return state;
}
};
// 전역 상태 store 생성
const toastStore = createStore(toastReducer, initialState);
export default toastStore;
토스트 상태를 구독하고 표시/숨김 기능을 제공하는 커스텀 훅
import { useRef } from "react";
import toastStore, { TOAST_ACTIONS } from "../stores/toast";
import { useStore } from "@hanghae-plus/lib";
import type { ToastState } from "../stores/toast";
// 토스트 상태 구독 훅 (type, message, visible만 추려서 반환)
export const useToastState = () => {
return useStore(toastStore, (state) => ({
type: state.type,
message: state.message,
visible: state.visible,
}));
};
// 토스트 표시 및 숨김 제어 훅
export const useToast = () => {
const timeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null); // 자동 숨김을 위한 타이머 ref
// 토스트 표시 함수
function show({ type, message }: { type: ToastState["type"]; message: string }) {
toastStore.dispatch({ type: TOAST_ACTIONS.SHOW_TOAST, payload: { type, message } });
// 기존 타이머 초기화
if (timeoutRef.current) {
clearTimeout(timeoutRef.current);
}
// 3초 후 자동 숨김
timeoutRef.current = setTimeout(() => {
toastStore.dispatch({ type: TOAST_ACTIONS.HIDE_TOAST });
timeoutRef.current = null;
}, 3000);
}
// 수동으로 토스트 숨김
function hide() {
if (timeoutRef.current) {
clearTimeout(timeoutRef.current);
timeoutRef.current = null;
}
toastStore.dispatch({ type: "HIDE_TOAST" });
}
return {
show,
hide,
};
};
이번 리팩토링의 결과, ToastProvider 하위에서 발생하던 불필요한 리렌더링이 줄어들었고, Toast와 무관한 영역에서의 성능 개선 가능성을 확인했습니다. 다만, Profiler 상에서 여전히 ProductList 컴포넌트의 리렌더링이 감지되었으며, 이는 Context 외부의 리렌더링 트리거로 인한 것으로 추정됩니다 (HomePage나 PageWrapper 등의 상태 변화). 향후에는 이 부분도 추가적인 memo 처리나 상태 분리를 통해 개선해야 할 것 같습니다!
학습 효과 분석
-
사실 기존에는 기존에는 해당 훅들의 핵심 개념과 사용 목적 정도만 알고 있었고, 단순히 "언제 써야 하는지" 정도에만 집중했으며useMemo나 useCallback을 언제 사용해야 할지 고민이 생기면, 큰 생각 없이 일단 사용하는 경우가 많았습니다. 하지만 이번 과제를 통해 이러한 막연한 최적화 시도는 오히려 성능에 부정적인 영향을 줄 수 있음을 깨달았습니다.
-
React 공식 문서에서도 useCallback과 useMemo는 특정 경우에만 의미 있는 최적화 도구라고 강조합니다. useCallback은 memoized 컴포넌트에 함수를 prop으로 전달하거나, 다른 Hook의 의존성으로 사용될 때만 유의미합니다. useMemo 또한 계산 비용이 큰 값, memoized 컴포넌트에 넘기는 값, Hook의 의존성으로 사용하는 값에만 효과적입니다. 이러한 기준을 명확히 이해하고 나니, 불필요한 최적화 대신 진짜 필요한 곳에만 useMemo와 useCallback을 적용할 수 있게 되었고, 내부 로직을 코드로 구현하면서 리렌더링의 원인과 이를 방지하는 메커니즘까지 이해할 수 있었습니다.
-
상태 변경이 바로 리렌더링으로 이어지지 않도록, 메모리제이션과 비교 로직을 적절히 조합하는 것이 리액트 성능 최적화의 핵심이라고 생각합니다. 특히 Context 기반으로 상태를 관리하면서는 useMemo나 useCallback만으로는 해결되지 않는 리렌더링 문제를 직접 경험했습니다. 그 과정에서 불필요한 구독은 분리하고, 꼭 필요한 부분만 구독하도록 설계하는 것이 얼마나 중요한지 체감할 수 있었습니다. 단순히 코드가 동작하는 데서 그치는 것이 아니라, “왜 이게 리렌더링되지?”라는 시선으로 코드를 바라보는 습관을 가지게 되었습니다.
과제 피드백
-
React의 렌더링 최적화(메모이제이션, 얕은/깊은 비교, 커스텀 훅 등)를 직접 구현하며 원리를 깊이 이해할 수 있게 한 점이 좋았습니다.
-
기능 구현이 필요한 파트에서는 정확하게 요구사항을 만족시키는 로직을 고민하게 만들었고, 반대의 경우는 "왜 이렇게 동작하는가?", "이 상황에서는 어떻게 더 나은 구조를 설계할 수 있을까?" 같은 질문을 스스로 할 수 있게 한점이 좋았습니다. 과제를 진행하면서 단순히 개념만 알고 사용하는 데서 그치지 않고, 왜 이렇게 되고, 그럼 어떻게 해야 할까? 라는 질문을 반복하며 스스로 사고의 깊이를 확장할 수 있었던 점이 이번 과제에서 얻은 가장 큰 배움이었습니다.
학습 갈무리
리액트의 렌더링이 어떻게 이루어지는지 정리해주세요.
- 초기 렌더링 (Initial Rendering)
- ReactDOM.createRoot(...).render(
)로 루트 컴포넌트를 마운트합니다. - App 및 하위 컴포넌트들을 재귀적으로 호출해 React Element 트리(가상 DOM) 생성합니다.
- React가 가상 DOM을 바탕으로 실제 DOM을 만들어 브라우저에 삽입합니다.
- 비교(Reconciliation) 과정 없이 그대로 DOM 생성합니다.
- 업데이트 렌더링 (Re-rendering)
- setState, useState, useReducer, props 변경 등으로 상태 변화 발생합니다.
- 해당 컴포넌트와 자식 컴포넌트들이 다시 렌더링됨니다. (함수형 컴포넌트는 함수 전체가 재실행)
- React가 변경 전/후 가상 DOM을 비교(diff)하여 차이(reconciliation) 계산합니다.
- 실제로 바뀐 부분만 실제 DOM에 반영합니다.(DOM diff + patch)
Reconciliation(리컨실리에이션)이란?
-
리액트에서 Reconciliation은 "이전 가상 DOM과 새 가상 DOM을 비교해서 실제로 바뀐 부분만 찾아내는 과정"을 의미합니다.
-
컴포넌트의 상태(state)나 props가 변경되면, 리액트는 전체 UI를 다시 렌더링하는 대신
- 변경 전 가상 DOM과 변경 후 가상 DOM을 빠르게 비교(diff)합니다.
- 바뀐 부분만 실제 DOM에 최소한으로 반영(patch)합니다.
-
Reconciliation의 핵심은 "최소한의 DOM 변경"과 "빠른 비교 알고리즘(주로 key, 타입, props 등)"입니다.
메모이제이션에 대한 나의 생각을 적어주세요.
메모이제이션은 불필요한 계산이나 함수 재생성을 막아 렌더링 성능을 개선하는 기법이라는 것은 알지만, 실제로 언제, 어디에 적용해야 하는지 명확히 판단하기가 쉽지 않았습니다. 특히, 무분별하게 메모이제이션을 남발하면 오히려 성능에 악영향을 줄 수 있다는 점에서 신중함이 필요하다는 것을 깨달았습니다.
-
의존성 배열 관리의 중요성 useMemo와 useCallback의 의존성 배열을 정확히 관리하지 않으면 원하는 시점에 값이 갱신되지 않거나, 반대로 불필요하게 다시 계산 되어 효과가 없을 수 있습니다. 그래서 항상 의존성 배열을 꼼꼼히 점검하고, 필요한 값들을 빠뜨리지 않아야 합니다.
-
컴포넌트 리렌더링과 메모이제이션의 관계 메모이제이션을 쓰더라도 컴포넌트가 자주 리렌더링되면 무용지물일 수 있다는 점도 깨달았습니다. 그래서 상태 관리 방법을 함께 고민하 며, 메모이제이션은 성능 최적화의 한 부분일 뿐 전체 구조를 함께 고려해야 한다는 점을 배워야 한다고 생각합니다.
컨텍스트와 상태관리에 대한 나의 생각을 적어주세요.
-
저는 처음에는 Context가 상태를 저장하고 관리하는 역할이라고 오해했습니다. 하지만 학습을 통해 Context는 상태 자체를 관리하는 것이 아니라, useState, useReducer, 혹은 상태 관리 라이브러리에서 정의한 상태를 여러 컴포넌트에 전달해주는 역할이라는 것을 이해하게 되었습니다. 이러한 점에서 Context는 "상태 관리"보다는 "상태 전달"이라는 표현이 더 적절하다고 느꼈습니다.
-
Context API는 상태가 변경될 경우, 해당 Context의 Provider 내부에 있는 모든 하위 컴포넌트가 리렌더링된다는 특성이 있습니다. 이로 인해 성능 최적화가 어려워질 수 있으며, 특히 상태 변경이 빈번하거나 규모가 큰 애플리케이션에서는 리렌더링으로 인한 성능 저하가 발생할 수 있습니다.
-
반면, 상태 관리 라이브러리는 상태의 부분 구독, 비동기 처리, 미들웨어 적용 등 다양한 고급 기능을 제공하며, 복잡한 상태 로직을 더 체계적으로 분리하고 관리할 수 있도록 돕습니다. 따라서 규모가 큰 프로젝트나 여러 개발자가 협업하는 환경에서는 Context API보다는 전용 상태 관리 라이브러리를 사용하는 것이 성능과 유지보수 측면에서 더 적합하다고 판단하게 되었습니다.
리뷰 받고 싶은 내용
-
Context 대신 createStore를 사용해 전역 상태를 분리했는데, 기존 Context 기반 상태 관리에서 발생하던 불필요한 리렌더링 문제를 효과적으로 줄일 수 있었습니다. 다만, createStore의 구독 범위를 어떻게 최적화해야 하는지, 상태 접근 시 필요한 최소한의 구독만 유지하는 좋은 패턴이 무엇인지 아직 명확하지 않습니다. 이와 관련해 권장하는 설계 방식에 대해 조언 받고 싶습니다.
-
저는 지금까지 React 프로젝트에서 Context보다는 주로 Zustand 같은 전역 상태 관리 라이브러리를 활용해 왔습니다. 실무에서도 큰 문제 없이 상태 관리가 잘 되었고, 성능 이슈도 거의 경험하지 못해 Context에 대해서는 상대적으로 크게 신경 쓰지 않았던 것 같습니다. 이번에 Context에 대해서도 배우고 이해했지만, 앞으로도 단순한 전역 상태에서도 Zustand 같은 상태 관리 라이브러리를 사용해도 크게 문제가 없을 것 같다는 생각이 듭니다. 코치님께서는 실무에서 꼭 Context를 사용해야 했던 특별한 상황이나 이유가 있으셨는지 궁금합니다.
과제 피드백
희진님 고생하셨어요! 회고를 보니 명확하게 과제를 통해 어떤 부분을 학습하셨는지 설득력있게 다가와서 저도 많이 보고 배웠네요. 개발자 도구를 통한 분석이나 원인을 파악하고 해결해나가는 모습도 좋았네요 :+1 말씀해주신 것처럼 해당 부분에 있어서 한번 꼭 찾아보셨으면 좋겠네요. 과제는 당연히 잘 작성해주셨습니다 ㅎㅎ
그럼 작성해주신 질문 이어서 답변 남겨볼게요.
Context 대신 createStore를 사용해 전역 상태를 분리했는데, 기존 Context 기반 상태 관리에서 발생하던 불필요한 리렌더링 문제를 효과적으로 줄일 수 있었습니다. 다만, createStore의 구독 범위를 어떻게 최적화해야 하는지, 상태 접근 시 필요한 최소한의 구독만 유지하는 좋은 패턴이 무엇인지 아직 명확하지 않습니다. 이와 관련해 권장하는 설계 방식에 대해 조언 받고 싶습니다.
음 제가 질문을 제대로 이해했는지 조금 헷갈리지만, 이런 식으로 스토어를 구성하다 보면..일반적으로 고민하는 것처럼 셀렉터 함수를 분리하고 별도로 필요한 데이터만 구독하는 형태로 운영되면 성능적인 측면에서도 좋을거에요. 또는 구독되는 아이템들을 기반으로 메모이제이션을 걸어 셀렉터를 생성할 수 도 있을 것 같구요. 이 과정에서 이런 최적화가 이전에 작성해주셨던 것처럼 실제 렌더링 성능에 어떤 영향을 미치고 개선을 했을 때 얼마나 나아지는지는 함께 체크해보면 좋을것 같아요!
저는 지금까지 React 프로젝트에서 Context보다는 주로 Zustand 같은 전역 상태 관리 라이브러리를 활용해 왔습니다. 실무에서도 큰 문제 없이 상태 관리가 잘 되었고, 성능 이슈도 거의 경험하지 못해 Context에 대해서는 상대적으로 크게 신경 쓰지 않았던 것 같습니다. 이번에 Context에 대해서도 배우고 이해했지만, 앞으로도 단순한 전역 상태에서도 Zustand 같은 상태 관리 라이브러리를 사용해도 크게 문제가 없을 것 같다는 생각이 듭니다. 코치님께서는 실무에서 꼭 Context를 사용해야 했던 특별한 상황이나 이유가 있으셨는지 궁금합니다.
실무적인 관점에서 최근 상태를 관리하는 방식은 서버 상태와 UI를 위한 전역 상태 관리로 나뉘는 것 같아요. 그럼 여기서 말씀해주시는 zustand같은 것들은 후자를 관리 하기 위한 상태일텐데요. 말씀해주신것처럼 context를 사용하다보면 직접 각 구역을 나눠 렌더링 최적화를 진행해줘야 하기 때문에 오히려 번거로운 지점이 많이 생기는 것 같아요. 과거에는 이런 전역 상태 관리 라이브러리의 크기가 크고 보일러 플레이트 코드가 많아졌었는데, 이런 지점은 zustand나 jotai로 들어서면서 매우 경량화가 되고 사용도 용이해져서 더 이상 저는 중요하지는 않다고 생각이 들더라구요.
개인적으로는 너무 프롭 드릴링이 발생하는 것이 아니라면 각 컴포넌트에서 관리를 하고 필요해지는 시점이 오면 그때 부담없이(?) 선택을 하면 되지 않을까 라는 생각입니다. 반드시 context를 사용해야만 하는 경우는 없었던 것 같아요.
이번 주 과제 너무 잘해주셨고 다음주도 이번주처럼 화이팅입니다~