Skip to content

[6팀 김수현] Chapter 1-3. React, Beyond the Basics#35

Open
suhyeon57 wants to merge 15 commits intohanghae-plus:mainfrom
suhyeon57:main
Open

[6팀 김수현] Chapter 1-3. React, Beyond the Basics#35
suhyeon57 wants to merge 15 commits intohanghae-plus:mainfrom
suhyeon57:main

Conversation

@suhyeon57
Copy link

@suhyeon57 suhyeon57 commented Jul 21, 2025

과제 체크포인트

배포 링크

https://suhyeon57.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 개선

과제 셀프회고

기술적 성장

const ref = useRef<{ deps: DependencyList; value: T }>({
  deps: _deps,
  value: factory(), // 매 렌더마다 실행됨!
});

useMemo를 처음에는 이렇게 구현했는데, 매 렌더마다 실행되는 오류를 가지고 있었다. 의존성에 상관없이 실행되니 useMemo의 역할을 하지 못하는 상황이 발생

다시 제대로 구현

const ref = useRef<{ deps: DependencyList; value: T } | null>(null);
  if (!ref.current || !_equals(ref.current.deps, _deps)) {
    ref.current = {
      deps: _deps,
      value: factory(),
    };
  }

useShallowState.js

import { useState, useCallback } from "react";
import { shallowEquals } from "../equals";

export const useShallowState = <T>(initialValue: Parameters<typeof useState<T>>[0]) => {
  const [first, setFirst] = useState(initialValue);
  const customSetState = useCallback((next: T) => {
    setFirst((prev) => {
      if (!shallowEquals(prev, next)) {
        return next;
      }
      return prev;
    });
  }, []);
  return [first, customSetState] as const;
};

이 코드에서 타입을 명확하게 지정해서 타입스크립트 오류를 방지 위해 아래처럼 변경

import { useState, useCallback } from "react";
import { shallowEquals } from "../equals";
//얕은 비교를 통해 상태를 관리 
export const useShallowState = <T>(initialValue: T) => {
  const [first, setFirst] = useState<T>(initialValue);
  const customSetState = useCallback((next: T) => {
    setFirst((prev) => {
      if (!shallowEquals(prev, next)) {
        return next;
      }
      return prev;
    });
  }, []);
  return [first, customSetState] as const;
};

ToastProvider.tsx

export const ToastProvider = memo(({ children }: PropsWithChildren) => {
  const [state, dispatch] = useReducer(toastReducer, initialState);
  const { show, hide } = createActions(dispatch);
  const visible = state.message !== "";

  const hideAfter = debounce(hide, DEFAULT_DELAY);

  const showWithHide: ShowToast = useCallback((...args) => {
    show(...args);
    hideAfter();
  }, []);

  const contextValue = useMemo(() => ({ ...state, show: showWithHide, hide }), [state, showWithHide, hide]);

  return (
    <ToastContext value={contextValue}>
      {children}
      {visible && createPortal(<Toast />, document.body)}
    </ToastContext>
  );
});

처음 시도한 코드는 useCallback과 useMemo를 설정해줬다. 하지만, Context value 객체가 바뀌면 하위 컴포넌트가 리렌더링 되는 현상이 발생했다.
두 번째 방법으로 useRef로 show/hide를 고정해도, state가 바뀌면 Context value가 바뀌어 리렌더링 되는 문제점을 발견했다.
그래서 action과 state Context를 분리해야 하며,
하위 컴포넌트가 action만 구독하면 show/hide가 변하지 않는 한 리렌더링되지 않는 것을 알게 되었다.
state는 토스트 메시지 변화에만 영향을 주도록 분리하고, (state는 메세지가 바뀔 때를 위해 분리를 해주어야 한다.)
하위 컴포넌트들은 action 요소만 구독을 하기 때문에 show와 hide가 변하지 않으면 다시 렌더링이 되지 않는다.

수정 코드

//action context 설정
const ToastActionsContext = createContext<{
  show: ShowToast;
  hide: Hide;
}>({
  show: () => null,
  hide: () => null,
});

//state context 설정
const ToastStateContext = createContext<{
  message: string;
  type: ToastType;
}>({
  ...initialState,
});
  const { show, hide } = useRef(createActions(dispatch)).current; //useRef를 사용하였기 때문에 리렌더링과 무관하게 고정된 값 유지
  return (
    <ToastActionsContext value={context}>
      <ToastStateContext value={state}>
        {children}
        {visible && createPortal(<Toast />, document.body)}
      </ToastStateContext>
    </ToastActionsContext>
  );

자랑하고 싶은 코드

아직은 없는 것 같습니다. 리팩토링을 통해 제 코드로 다시 만들 생각입니다.

개선이 필요하다고 생각하는 코드

이번 과제를 통해 실무에서 성능 최적화를 위한 훅 사용법을 더 명확히 이해할 수 있었습니다. 특히, 어떤 상황에서 어떤 훅을 사용해야 하는지를 고민해보는 계기가 되어 좋았습니다.
하지만 아직도 훅들이 언제, 어디서 적절하게 사용되는지에 대한 감각은 부족하다고 느꼈습니다.
이를 보완하기 위해 벨로그에 직접 정리하며 복습할 예정입니다. 이러한 과정을 통해 실무에 더 효과적으로 적용할 수 있을 것이라 기대하고 있습니다.

학습 효과 분석

ToastProvider.tsx
(useContext에 대해)

context는 props drilling 없이 컴포넌트 트리 전체에 데이터를 제공

//state context 설정
const ToastStateContext = createContext<{
  message: string;
  type: ToastType;
}>({
  ...initialState,
});

//action context 설정
const ToastActionsContext = createContext<{
  show: ShowToast;
  hide: Hide;
}>({
  show: () => null,
  hide: () => null,
});

--> 초기값 설정 시 provider 없이 사용할 수 있다.
초기값 없이 사용할 시 provider 컴포넌트를 생성하고, 커스텀 훅에서 예외 처리를 해줘야 한다.

<ToastActionsContext value={context}>
  <ToastStateContext value={state}>
    {children}
    {visible && createPortal(<Toast />, document.body)}
  </ToastStateContext>
</ToastActionsContext>

현재 코드에서 provider 컴포넌트가 없는 이유는 초기값을 설정해주었기 때문이다.

커스텀 훅

export const useToastCommand = () => {
  const { show, hide } = useContext(ToastActionsContext);
  return { show, hide };
};

export const useToastState = () => {
  const { message, type } = useContext(ToastStateContext);
  return { message, type };
};

--> 자주 사용하는 로직을 커스텀 훅으로 묶어서 사용한다. 외부에서도 사용할 수 있다.
커스텀 훅으로 사용했기 때문에 외부에서도 아래처럼 사용이 가능하다.

const toast = useToastCommand();

과제 피드백

React Hook을 실무에서 사용할 때에는 제대로 알지 못하고 쓰는 기분이 들었는데, 어떤 부분에서 렌더링이 최적화 되는지 깊게 파악할 수 있어서 좋았습니다.
그리고, 과제에서 다룬 내용을 실무에도 바로 적용해 보았습니다.
기존 윈도우 크기 추적 로직을 useSyncExternalStore를 활용해 리팩토링했고, 보다 안정적이고 예측 가능한 방식으로 개선할 수 있었습니다.

학습 갈무리

리액트의 렌더링이 어떻게 이루어지는지 정리해주세요.

React의 렌더링은 Virtual DOM(가상 DOM) 을 기반으로 동작합니다. 컴포넌트의 상태(state)나 props가 변경되면, React는 변경된 값을 기반으로 새로운 Virtual DOM을 생성합니다.

이후 이전 Virtual DOM과 새 Virtual DOM을 비교(diff 알고리즘)하여 변경된 부분만 찾아 실제 DOM에 최소한으로 반영합니다. 이 과정을 통해 DOM 조작의 비용을 줄이고 성능을 최적화합니다.

또한 React에서는 useMemo, useCallback, React.memo 등과 같은 메모이제이션 기법을 통해 불필요한 렌더링을 줄일 수 있습니다.

그리고 useEffect 등의 Hook을 통해 렌더링 이후의 사이드 이펙트 처리를 제어할 수 있으며, 이 역시 렌더링 흐름에서 중요한 역할을 합니다.

메모제이션에 대한 나의 생각을 적어주세요.

메모이제이션은 동일한 연산이 반복될 때 성능을 최적화할 수 있는 중요한 기법이라고 생각합니다. 이를 사용하지 않으면 불필요한 렌더링이나 계산이 발생할 수 있고, 결과적으로 앱 성능에 영향을 줄 수 있습니다.

특히 useMemo는 계산 비용이 큰 연산 결과를 캐싱하는 데 유리해서, 단순히 값을 보존하는 useRef보다 성능 측면에서 더 적절할 수 있습니다.

실제로 ToastProvider 과제를 진행하면서 메모이제이션을 통해 리렌더링을 줄이고 성능을 향상시킬 수 있다는 점을 체감했습니다.

컨텍스트와 상태관리에 대한 나의 생각을 적어주세요.

Context는 props drilling 없이 컴포넌트 트리 전체에 데이터를 전달할 수 있게 해주는 기능입니다. 이를 통해 상위 컴포넌트에서 하위 컴포넌트로 일일이 props를 전달하지 않아도 되므로 코드가 간결해지고 유지보수가 쉬워집니다.

또한 Context와 상태관리 도구를 함께 사용하면 전역 상태를 효과적으로 관리할 수 있습니다. 적절하게 분리하지 않으면, 하위 컴포넌트가 불필요하게 자주 리렌더링되는 문제가 발생할 수 있습니다.

따라서 Context는 단순히 데이터를 공유하는 역할을 넘어, 상태관리 전략과 함께 사용함으로써 리렌더링을 최소화하고, 더 효율적인 앱 구조를 만드는 데 큰 도움이 된다고 생각합니다.

리뷰 받고 싶은 내용

export const ToastProvider = memo(({ children }: PropsWithChildren) => {
  const [state, dispatch] = useReducer(toastReducer, initialState);
  const { show, hide } = useRef(createActions(dispatch)).current; //useRef를 사용하였기 때문에 리렌더링과 무관하게 고정된 값 유지
  const visible = state.message !== "";

  const hideAfter = debounce(hide, DEFAULT_DELAY);

  const showWithHide: ShowToast = useCallback((...args) => {
    show(...args);
    hideAfter();
  }, []); //의존성 배열을 빈 배열로 설정하여 최초 렌더링 시에만 실행되도록 함

  const context = useMemo(() => ({ show: showWithHide, hide }), [showWithHide, hide]); //showWithHide와 hide가 변하지 않으므로 변하지 않음

  return (
    <ToastActionsContext value={context}>
      <ToastStateContext value={state}>
        {children}
        {visible && createPortal(<Toast />, document.body)}
      </ToastStateContext>
    </ToastActionsContext>
  );
});

저는 아래 코드에서 createActions(dispatch)로 만든 show, hide 함수를 useRef로 감싸서 리렌더링과 무관하게 고정된 값으로 유지하고, 이후 useCallback 안에서 사용하고 있습니다. 이런 방법을 통해 useCallback에서 의존성 배열을 빈 배열로 두어도 lint 경고가 생기지 않는데, 이 방법을 사용해도 되는지 궁금합니다.
(현재는 팀원들의 도움으로, useMemo, useCallback 방법으로 변경하였습니다.)

이번 과제를 통해 React Hook이 어떤 역할을 하고, 어떻게 사용되는지에 대해 감을 잡을 수 있었습니다. 하지만 아직도 어떤 상황에서 어떤 Hook을 써야 하는지, 그리고 성능 최적화를 위해 어떤 판단 기준을 가져야 할지는 여전히 모호하게 느껴집니다.
혹시 Hook을 더 잘 이해하고 적재적소에 활용하기 위해 추천하시는 학습 방법이나 연습 방식이 있다면 알려주시면 감사하겠습니다!

@JHeeJinDev
Copy link

JHeeJinDev commented Jul 24, 2025

안녕하세요 수현님!

useRef로 함수 참조를 고정하는 방식은 Lint 경고도 없고 잘 작동하긴 하지만, React의 선언형 흐름(상태 기반 UI 갱신)을 우회해 직접 제어하는 명령형 방식이라 유지보수나 협업 관점에서는 주의가 필요한 부분이라고 생각할 수 있을꺼 같아요!

저는 이 부분을 React의 선언형 흐름을 유지하는 방향으로 구현해보고자, useRef 대신 useMemo와 useCallback을 조합해 아래와 같이 구현해봤습니다!

const { show, hide } = useMemo(() => createActions(dispatch), [dispatch]);

const hideAfter = useMemo(() => debounce(hide, DEFAULT_DELAY), [hide]);

const showWithHide: ShowToast = useCallback((...args) => {
  show(...args);
  hideAfter();
}, [show, hideAfter]);

이렇게 하면 ref 없이도 함수 참조를 안정적으로 유지할 수 있고, 의존성 배열 관리도 명확하게 관리할 수 있어 React스럽고 유지보수에도 더 좋다고 생각했습니다! 제 말이 틀렸거나 다르게 생각하시면 언제든 피드백 부탁드립니다 :)

@suhyeon57
Copy link
Author

오 희진님 !! 감사해요 이렇게 하는 방법도 있었군요 ㅜㅜ useRef, useMemo를 언제 어디서 써야할지 파악이 부족했나봐요... 감사합니다 ☺️

Comment on lines +9 to +16
return () => {
listeners.delete(fn);
};
};

const unsubscribe = (fn: Listener) => {
listeners.delete(fn);
};
// const unsubscribe = (fn: Listener) => {
// listeners.delete(fn);
// };

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

오? 이전 unsubscirbe 함수를 주석처리하고 구현체만 옮기신 이유가 따로 있을까요?

unsubscribe 함수를 호출하게 될 경우, 함수읽는 입장에서는 구독이 종료될때 구독을 해제한다라는 것을 명확하게 알수 있지 않을까요?!
그리고 나중에 unsubscirbe도 사용할수도 있구요.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

저는 그냥 바로 return할 생각이었는데 나중에 unsubscribe를 사용하려면 다시 구현을 해놔야겠네요 ㅎㅎ !

@pitangland
Copy link

LGTM!!!!!!!!!!!!!!!!!!!!!!

</ToastStateContext>
</ToastActionsContext>
);
});

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

수현님~~ 내부에서 사용된 메모이제이션 훅들을 직접 만든 useMemo, useAutoCallback을 이용해 리팩토링 해보시는 건 어떨까요?
훅 직접 만드셨는데 직접 사용하고 평가해보시면 좋을 것 같습니다 !

return true;
}
return true;
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1depth까지 순회하며 비교하는 로직은 shallowEquals 함수와 동일하고, 2depth이상부터 비교만 재귀적으로 deepEqauls함수를 사용하시니, shallowEquals 함수 사용으로 불필요한 로직을 제거하고 가독성을 높이면서 리팩토링 해보시는 건 어떨까요??

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants