Skip to content

[12팀 금정민] Chapter 1-3 React, Beyond the Basics#52

Open
KumJungMin wants to merge 26 commits into
hanghae-plus:mainfrom
KumJungMin:assgiment_
Open

[12팀 금정민] Chapter 1-3 React, Beyond the Basics#52
KumJungMin wants to merge 26 commits into
hanghae-plus:mainfrom
KumJungMin:assgiment_

Conversation

@KumJungMin
Copy link
Copy Markdown

@KumJungMin KumJungMin commented Apr 9, 2025

과제 체크포인트

배포 링크

기본과제

  • shallowEquals 구현 완료
  • deepEquals 구현 완료
  • memo 구현 완료
  • deepMemo 구현 완료
  • useRef 구현 완료
  • useMemo 구현 완료
  • useDeepMemo 구현 완료
  • useCallback 구현 완료

심화 과제

  • 기본과제에서 작성한 hook을 이용하여 렌더링 최적화를 진행하였다.
  • Context 코드를 개선하여 렌더링을 최소화하였다.

과제 셀프회고

별도 코멘트로 작성하였습니다.

Comment thread src/@lib/hooks/useMemo.ts
Comment on lines 5 to +20
export function useMemo<T>(
factory: () => T,
_deps: DependencyList,
_equals = shallowEquals,
): T {
// 직접 작성한 useRef를 통해서 만들어보세요.
return factory();
const memorizedRef = useRef<{ deps: DependencyList; value: T } | null>(null);

const isInitialRender = memorizedRef.current === null;
const memoizedDeps = memorizedRef.current?.deps || [];
const isDepsChanged = !_equals(memoizedDeps, _deps);

if (isInitialRender || isDepsChanged) {
memorizedRef.current = { deps: _deps, value: factory() };
}

return memorizedRef.current!.value;
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

memo, useMemo에 대해 알게된 점

1. React.memo에 대해 알게 되었다...!

얕은 비교를 한다

  • React.memo는 함수형 컴포넌트를 감싸는 고차 컴포넌트(HOC)로,
  • 전달받은 props가 이전과 동일한 경우에 해당 컴포넌트의 재렌더링을 건너뛰어 렌더링 비용을 줄입니다.
  • React.memo는 얕은(shallow) 비교를 사용합니다.
  • 그래서 객체나 배열 같이 복합 구조의 경우 참조가 변경되면 동일한 값이라도 재렌더링을 일으킬 수 있으니 주의해야함을 알게 되었습니다..
  • 실제 사용 예시가 궁금해 찾아보니, material‑ui의 status 컴포넌트에서 React.memo를 쓰고 있었습니다.
const Status = React.memo((props: StatusProps) => {
  const { status } = props;
  let label = status;
  if (status === 'PartiallyFilled') {
    label = 'Partial';
  }
  return (
    <Chip
      size="small"
      label={label}
      variant="outlined"
      sx={(theme) => ({
        lineHeight: 1,
        fontSize: '10px',
        fontWeight: 'bold',
        ...(status === 'Open' && {
          borderColor: 'primary.500',
          bgcolor: alpha(theme.palette.primary[500], 0.1),
          color: 'primary.600',
        }),
        ....
      })}
    />
  );
});
  • Status 컴포넌트는 단일 prop status에 의존하며, Chip 컴포넌트를 자식으로 가집니다.
  • Chip 컴포넌트는 여러 개의 props를 가지는 구조이나, status값에만 의존합니다.
  • 그래서 Chip컴포넌트가 여러 번 리렌더링될 필요가 없기 때문에 React.memo를 사용해 status가 변경될 때만 재렌더링하는 듯 했습니다.

memo와 훅을 같이 쓰면 좋다

  • 과제를 하다보니 알게 되었는데... memo만 단독으로 쓰기 보다는 훅과 같이 썼을 때 효과가 있는 게 알게 되었습니다.
  • 부모에서 생성된 콜백 함수를 useCallback으로 감싸 전달하면(아래 예시처럼),
  • React.memo와 결합되어 하위 컴포넌트의 불필요한 리렌더링을 예방할 수 있었습니다.
const Parent = () => {
  const [count, setCount] = useState(0);
  // count 상태를 변경하는 함수는 useCallback으로 감싸 재사용
  const handleClick = useCallback(() => setCount(c => c + 1), []);
  
  return <MemoizedChild onClick={handleClick} />;
};

const Child = ({ onClick }) => {
  console.log("Child rendered");
  return <button onClick={onClick}>Click me</button>;
};

2. memo 대신 useMemo를 많이 쓰는 이유는...!

ui 라이브러리에서는 주로 뭘 쓸까?

  • useMemo는 momo와 비슷하지만, 비용이 많이 드는 계산이나 객체 생성을 메모이제이션하는 데 유용하다고 합니다.
  • 그래서 material-ui에서는 memo, useMemo 중 어떤 걸 많이 쓰는지 궁금해졌습니다.
  • material-ui 컴포넌트에서는 memo보다 useMemo를 많이 사용하고 있었습니다.
memo 사용빈도(24회) useMemo 사용빈도(106회)
스크린샷 2025-04-10 오후 9 37 45 스크린샷 2025-04-10 오후 9 38 03
  • 처음에는 React.memo로 그냥 다 처리하면 안되나? 하는 생각이 들었는데 목적이 다르니까 안되는 거였습니다!

왜 모두 React.memo로 대체할 수 없는가?

  1. 메모이제이션 대상이 다름:
  • React.memo는 전체 컴포넌트를 감싸 props 수준에서 비교합니다.
  • 반면에 useMemo는 컴포넌트 내부에서 특정 값, 객체, 함수를 캐싱합니다.
  • 그래서 스타일 객체나 비용이 많이 드는 계산 결과는 useMemo로 캐싱해 동일한 참조를 유지해 하위 컴포넌트의 불필요한 리렌더링을 예방하는 게 좋습니다.
  1. 컴포넌트 재사용성 vs 내부 계산 최적화:
  • React.memo를 사용해도 컴포넌트 내에서 매번 새로운 객체나 함수를 생성하는 경우에는 그 값들이 props나 상태로 전달될 때 참조가 달라져 다시 렌더링되는 문제가 발생할 수 있습니다. (memo는 얕은 비교를 하니까!)
  • 이 경우 useMemo를 통해 안정된 참조를 유지하는 것이 필요합니다.
  1. 성능 최적화할 때 더 세밀하게 가능:
  • useMemo는 특정 값만 메모이제이션해 상황에 맞게 성능 최적화를 진행할 수 있습니다.
  • 모든 컴포넌트를 React.memo로 감싸는 것은 컴포넌트 렌더링 자체에 대한 최적화를 수행하는 것이지,
  • 내부의 계산 또는 객체 생성 최적화를 대체하지는 못한다고 합니다.

Comment thread src/@lib/hooks/useRef.ts
Comment on lines +3 to +6
/** ref의 lazy initialization 사용 */
export function useRef<T>(initialValue: T): { current: T } {
// React의 useState를 이용해서 만들어보세요.
return { current: initialValue };
const [ref] = useState(() => ({ current: initialValue }));
return ref;
Copy link
Copy Markdown
Author

@KumJungMin KumJungMin Apr 10, 2025

Choose a reason for hiding this comment

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

lazy initialization을 알게 된 점

1. 생애 주기 내에서 최초 1번만 계산한다.

  • Lazy initialization은 값을 컴포넌트의 생애주기 내에서 최초 한 번만 계산하는 기법입니다.
  • 그래서 외부 라이브러리를 사용할 때, 초기화 비용이 큰 객체(예: 복잡한 설정값이나 많은 데이터를 포함하는 객체)를 생성할 때 이 기법을 활용하면 그 값을 재사용할 수 있습니다. (참고)

2. 이럴 때 유용하다.

material-ui의 useMediaQuery 훅를 기반으로 이해해보았습니다.

  • 초기 media query의 결과(예: matchMedia(query).matches 등)를 계산할 때, 조건에 따라 값을 결정합니다.
  • 만약 클라이언트에서 noSsr 옵션이 켜져 있고 matchMedia가 사용 가능한 경우 실제 값을,
  • 그렇지 않으면 서버 측 렌더링에 적합한 값을 반환하도록 구현되어 있습니다.
스크린샷 2025-04-10 오후 9 55 14



왜 Lazy Initialization을 썼을까?

  • useMediaQuery 훅은 media query의 계산을 한 번만 하면 되고, 이후 리렌더링에서 동일한 값을 재사용할 수 있습니다.
  • 그리고 클라이언트와 서버 환경에 따라 서로 다른 초기 값을 적용해야 하는 상황에서,
  • 초기 계산을 처음 렌더링 시 한 번만 수행하는 Lazy Initialization 패턴이 적합하다는 점을 알게 되었습니다.

그럼, 이 패턴은 어떤 상황에서 써야 할까?

  • 비용이 많이 드는 초기 계산이나 조건부 초기 값 설정이 필요한 경우
  • SSR과 클라이언트 렌더링 사이의 차이를 안전하게 다루고자 할 때
  • 초기 상태 설정에 여러 조건이 포함되어 있고, 한 번만 계산해도 충분한 경우

Comment on lines +4 to +8
export function useCallback<T extends (...args: unknown[]) => unknown>(
factory: T,
_deps: DependencyList,
) {
// 직접 작성한 useMemo를 통해서 만들어보세요.
return factory as T;
deps: DependencyList,
): T {
return useMemo(() => factory, deps);
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

useCallback에 대해 알게 된 점

1. 빈 배열 의존성을 넣으면 렌더링에 영향을 주지 않는다.

원리:

  • useCallback(fn, [])로 빈 배열을 전달하면, 해당 콜백 함수는 컴포넌트가 마운트될 때 한 번만 생성되어 이후 렌더링에서는 고정된 참조를 유지합니다.
    이 때문에 일반적으로 렌더링에 영향을 주지 않는다고 이해했습니다.

사용 사례:

  • 예를 들어, material-ui의 listbox에서는 빈 배열 의존성으로 getItemDomElement()를 생성하는 사례를 볼 수 있습니다.
  • 이 코드는 getItemDomElement()가 한 번 생성된 이후에는 변경되지 않으므로, 다른 훅에서 사용할 때도 고정된 값으로 간주됩니다.
  • 그래서 useList 훅에서는 getItemDomElement()를 의존성에 추가할 필요가 없지 않을까? 라는 생각을 했습니다.
스크린샷 2025-04-10 오후 10 02 53
  • 하지만, 실제로 useList 훅을 확인해보면, getItemDomElement()를 의존성 배열에 포함시키고 있어 의문이 들었습니다.
스크린샷 2025-04-10 오후 10 03 12



2. 왜 getItemDomElement를 의존성 배열에 넣어야 하는가에 대한 추측

  1. ESLint 룰 준수를 위해서?
  • React의 ESLint 룰(react-hooks/exhaustive-deps)은 훅 내부에서 사용되는 모든 변수와 콜백을 의존성 배열에 명시하라고 권장한다고 합니다.
  • 이는 현재는 빈 배열이라 변하지 않더라도, 미래에 코드가 변경될 가능성을 염두에 둔 안전장치로 작용하는 걸까요?
  1. 미래 변화에 대한 대비?
  • 현재 getItemDomElement()는 빈 배열 의존성을 사용해 고정된 함수입니다.
  • 히지만, 혹시라도 이후 해당 함수가 내부적으로 다른 state나 ref에 의존하도록 변경되면,
  • 이미 의존성 배열에 포함되어 있으면 수정 시 실수를 줄일 수 있기 때문일까요?

3. 궁금한 점

  • 의존성을 명시하여 안정성을 보장하는 것과, 의존성 배열이 길어지면서 발생할 수 있는 성능 부담 사이에서 어떤 기준으로 균형을 잡아야 할지 궁금합니다.
  • 예를 들어, 어떤 상황에서는 ESLint 규칙을 반드시 따르는 것이 좋고, 또 다른 상황에서는 약간의 규칙 위반(예: 의존성 생략)이 더 나은 성능을 제공하는지, 그리고 그 판단 기준은 무엇인지 알고 싶습니다 :)

@KumJungMin
Copy link
Copy Markdown
Author

KumJungMin commented Apr 12, 2025

메모용

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.

1 participant