[12팀 금정민] Chapter 1-3 React, Beyond the Basics#52
Open
KumJungMin wants to merge 26 commits into
Open
Conversation
KumJungMin
commented
Apr 10, 2025
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; |
Author
There was a problem hiding this comment.
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회) |
|---|---|
![]() |
![]() |
- 처음에는
React.memo로 그냥 다 처리하면 안되나? 하는 생각이 들었는데 목적이 다르니까 안되는 거였습니다!
왜 모두 React.memo로 대체할 수 없는가?
- 메모이제이션 대상이 다름:
- React.memo는 전체 컴포넌트를 감싸 props 수준에서 비교합니다.
- 반면에 useMemo는 컴포넌트 내부에서 특정 값, 객체, 함수를 캐싱합니다.
- 그래서 스타일 객체나 비용이 많이 드는 계산 결과는 useMemo로 캐싱해 동일한 참조를 유지해 하위 컴포넌트의 불필요한 리렌더링을 예방하는 게 좋습니다.
- 컴포넌트 재사용성 vs 내부 계산 최적화:
- React.memo를 사용해도 컴포넌트 내에서 매번 새로운 객체나 함수를 생성하는 경우에는 그 값들이 props나 상태로 전달될 때 참조가 달라져 다시 렌더링되는 문제가 발생할 수 있습니다. (memo는 얕은 비교를 하니까!)
- 이 경우 useMemo를 통해 안정된 참조를 유지하는 것이 필요합니다.
- 성능 최적화할 때 더 세밀하게 가능:
- useMemo는 특정 값만 메모이제이션해 상황에 맞게 성능 최적화를 진행할 수 있습니다.
- 모든 컴포넌트를 React.memo로 감싸는 것은 컴포넌트 렌더링 자체에 대한 최적화를 수행하는 것이지,
- 내부의 계산 또는 객체 생성 최적화를 대체하지는 못한다고 합니다.
KumJungMin
commented
Apr 10, 2025
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; |
Author
There was a problem hiding this comment.
lazy initialization을 알게 된 점
1. 생애 주기 내에서 최초 1번만 계산한다.
- Lazy initialization은 값을 컴포넌트의 생애주기 내에서 최초 한 번만 계산하는 기법입니다.
- 그래서 외부 라이브러리를 사용할 때, 초기화 비용이 큰 객체(예: 복잡한 설정값이나 많은 데이터를 포함하는 객체)를 생성할 때 이 기법을 활용하면 그 값을 재사용할 수 있습니다. (참고)
2. 이럴 때 유용하다.
material-ui의 useMediaQuery 훅를 기반으로 이해해보았습니다.
- 초기 media query의 결과(예: matchMedia(query).matches 등)를 계산할 때, 조건에 따라 값을 결정합니다.
- 만약 클라이언트에서 noSsr 옵션이 켜져 있고 matchMedia가 사용 가능한 경우 실제 값을,
- 그렇지 않으면 서버 측 렌더링에 적합한 값을 반환하도록 구현되어 있습니다.
왜 Lazy Initialization을 썼을까?
- useMediaQuery 훅은 media query의 계산을 한 번만 하면 되고, 이후 리렌더링에서 동일한 값을 재사용할 수 있습니다.
- 그리고 클라이언트와 서버 환경에 따라 서로 다른 초기 값을 적용해야 하는 상황에서,
- 초기 계산을 처음 렌더링 시 한 번만 수행하는 Lazy Initialization 패턴이 적합하다는 점을 알게 되었습니다.
그럼, 이 패턴은 어떤 상황에서 써야 할까?
- 비용이 많이 드는 초기 계산이나 조건부 초기 값 설정이 필요한 경우
- SSR과 클라이언트 렌더링 사이의 차이를 안전하게 다루고자 할 때
- 초기 상태 설정에 여러 조건이 포함되어 있고, 한 번만 계산해도 충분한 경우
KumJungMin
commented
Apr 10, 2025
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); |
Author
There was a problem hiding this comment.
useCallback에 대해 알게 된 점
1. 빈 배열 의존성을 넣으면 렌더링에 영향을 주지 않는다.
원리:
useCallback(fn, [])로 빈 배열을 전달하면, 해당 콜백 함수는 컴포넌트가 마운트될 때 한 번만 생성되어 이후 렌더링에서는 고정된 참조를 유지합니다.
이 때문에 일반적으로 렌더링에 영향을 주지 않는다고 이해했습니다.
사용 사례:
- 예를 들어, material-ui의 listbox에서는 빈 배열 의존성으로 getItemDomElement()를 생성하는 사례를 볼 수 있습니다.
- 이 코드는
getItemDomElement()가 한 번 생성된 이후에는 변경되지 않으므로, 다른 훅에서 사용할 때도 고정된 값으로 간주됩니다. - 그래서 useList 훅에서는 getItemDomElement()를 의존성에 추가할 필요가 없지 않을까? 라는 생각을 했습니다.
- 하지만, 실제로 useList 훅을 확인해보면, getItemDomElement()를 의존성 배열에 포함시키고 있어 의문이 들었습니다.
2. 왜 getItemDomElement를 의존성 배열에 넣어야 하는가에 대한 추측
- ESLint 룰 준수를 위해서?
- React의 ESLint 룰(react-hooks/exhaustive-deps)은 훅 내부에서 사용되는 모든 변수와 콜백을 의존성 배열에 명시하라고 권장한다고 합니다.
- 이는 현재는 빈 배열이라 변하지 않더라도, 미래에 코드가 변경될 가능성을 염두에 둔 안전장치로 작용하는 걸까요?
- 미래 변화에 대한 대비?
- 현재
getItemDomElement()는 빈 배열 의존성을 사용해 고정된 함수입니다. - 히지만, 혹시라도 이후 해당 함수가 내부적으로 다른 state나 ref에 의존하도록 변경되면,
- 이미 의존성 배열에 포함되어 있으면 수정 시 실수를 줄일 수 있기 때문일까요?
3. 궁금한 점
- 의존성을 명시하여 안정성을 보장하는 것과, 의존성 배열이 길어지면서 발생할 수 있는 성능 부담 사이에서 어떤 기준으로 균형을 잡아야 할지 궁금합니다.
- 예를 들어, 어떤 상황에서는 ESLint 규칙을 반드시 따르는 것이 좋고, 또 다른 상황에서는 약간의 규칙 위반(예: 의존성 생략)이 더 나은 성능을 제공하는지, 그리고 그 판단 기준은 무엇인지 알고 싶습니다 :)
Author
|
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.


과제 체크포인트
배포 링크
기본과제
심화 과제
과제 셀프회고