heyday2024 님의 블로그
[React 숙련 1주차(3)] Memoization - (React.memo, useCallback, useMemo), Custom Hooks 본문
[React 숙련 1주차(3)] Memoization - (React.memo, useCallback, useMemo), Custom Hooks
heyday2024 2024. 11. 5. 21:30리-렌더링의 발생 조건
- 컴포넌트에서 state가 바뀌었을 때
- 컴포넌트가 내려받은 props가 변경되었을 때
- 부모 컴포넌트가 리-렌더링 된 경우 자식 컴포넌트는 모두
==> 리액트에서 리렌더링이 자주 발생한다?? 그것은 비효율적인 코드!!
==> 여기서 발생하는 cost를 최대한 줄여야 그 성능이 향상됨.
최적화(Optimization) 필요!!
==> 어떻게하면 불필요한 렌더링을 줄일 수있을까??
memoization을 이용하자!!!
1. memo(React.memo) : 컴포넌트를 캐싱
( 고차 컴포넌트(Higher-Order Component, HOC) : 다른 컴포넌트를 받아서 새로운 컴포넌트를 반환하는 함수 )
2. useCallback : 함수를 캐싱 (react hook)
3. useMemo : 값을 캐싱 (react hook)
(1) memo란?
리-렌더링의 발생 조건 중: ''부모 컴포넌트가 리렌더링 되면 자식컴포넌트는 모두 리렌더링 된다''
근데 이 조건에 결점이 있다!
만약, 자식은 바뀐게 없는데 렌더링이 되다면??
--> 굉장히 비효율적임.
==> React.memo를 사용해서 문제를 해결하자.
==> 이런 경우, count를 증가, 감소하기 위해 만들어지 위 버튼을 클릭하면, Box1, Box2, Box3도 모두 리렌더링된다.
App.jsx만 바뀌었는데 불필요한 하위 컴포넌트들의 리렌더링을 방지하기 위해!
React.memo를 사용!!
import { memo } from "react";
export default React.memo(Box1);
export default React.memo(Box2);
export default React.memo(Box3);
==> React.memo를 사용하면, 컴포넌트를 메모리에 저장해두고, 필요할 때마다 갖다 쓰게됨!
이렇게하면, 부모 컴포넌트의 state변경으로 인해 props가 변경이 일어나지 않는한 컴포넌트는 리렌더링 되지 않음 === 컴포넌트 memoization
(2) useCallback이란?
React.memo는 컴포넌트를 메모이제이션 했다면, useCallback은 인자로 들어오는 함수 자체를 기억(메모이제이션)함.
...
// count를 초기화해주는 함수
const initCount = () => {
setCount(0);
};
return (
<>
<h3>카운트 예제입니다!</h3>
<p>현재 카운트 : {count}</p>
<button onClick={onPlusButtonClickHandler}>+</button>
<button onClick={onMinusButtonClickHandler}>-</button>
<div style={boxesStyle}>
<Box1 initCount={initCount} />
<Box2 />
<Box3 />
</div>
</>
);
}
...
Box1이 만일, count를 초기화 해 주는 코드라고 가정
...
function Box1({ initCount }) {
console.log("Box1이 렌더링되었습니다.");
const onInitButtonClickHandler = () => {
initCount();
};
return (
<div style={boxStyle}>
<button onClick={onInitButtonClickHandler}>초기화</button>
</div>
);
}
...
Why??
initCount 함수를 App.jsx에 정의해주었기 때문에, App.jsx가 리렌더링되면서, 당연히 그 내부에 있는 iniitCount함수도 리렌더링됨
(즉, 계속 생성되니까 그 함수의 메모리주소가 바뀜--> 메모리 주소가 바뀐다??--> 상태가 변한거니까 Box1또한 리렌더링됨.)
(자바스크립트에서 함수도 객체의 한 종류임. 따라서 모양은 같아도 다시 만들어지면 참조하던 그 주소값이 달라지고, 이에 따라 하위 컴포넌트인 Box1.jsx는 props가 변경되었다고 인식함.)
const onInitButtonClickHandler = () => {
initCount();
};
==> 하지만, initCount는 늘 똑같이 카운트를 0으로 설정해주는 초기화 기능만 필요한데, 계속 리렌더링될 필요가 없음!!
==> 이런 경우, 함수를 메모리 공간에 따로 저장해놓고(캐싱), 특정 조건이 아닌 경우에는 변경되지 않도록 해야함!!
// 변경 전
const initCount = () => {
setCount(0);
};
// 변경 후
const initCount = useCallback(() => {
setCount(0);
}, []);
이때 useCallback 사용해주면됨!
<참고>
...
// count를 초기화해주는 함수
const initCount = useCallback(() => {
console.log(`[COUNT 변경] ${count}에서 0으로 변경되었습니다.`);
setCount(0);
}, []);
...
- 위 코드를 보면, 초기화 하는 함수에 console.log를 이용해서 초기화하기 전 count를 출력하려고한다.
근데 결과값은???
예상과 달리 7이 아닌 0이 찍히는 것을 볼 수 있다.
그 이유는 useCallback이 count가 0일 때의 시점을 기준으로 메모리에 함수를 저장했기 때문!!(즉, count가 변경되기 전 그 초기값만을 기억하고 이 init함수는 시간이 멈춰버린 것임!!!)
==> 때문에 만약 함수를 굳이 리렌더링 할 필요없는데 리렌더링이 발생하는 것을 방지하기 위해 useCallback을 쓰고, 또 그 안에서 변경되는 값을 추적하고자 한다면 dependency array를 사용해야함.
...
// count를 초기화해주는 함수
const initCount = useCallback(() => {
console.log(`[COUNT 변경] ${count}에서 0으로 변경되었습니다.`);
setCount(0);
}, [count]);
...
- count가 변경될떄 그 변경된 count를 결국 기억할 수 있게됨.
(3) useMemo란?
동일한 값을 반환하는 함수를 계속 호출해야 하면 필요없는 렌더링을 하는 것과 다름없음.
맨 처음 해당 값을 반환할 때 그 값을 특별한 곳(메모리)에 저장함.
이렇게 하면 필요할 때마다 다시 함수를 호출해서 계산하는게 아니라 이미 저장한 값을 단순히 꺼내와서 쓸 수 있는 것임. (=== 값 캐싱)
// as-is
const value = 반환할_함수();
// to-be
const value = useMemo(()=> {
return 반환할_함수()
}, [dependencyArray]);
- dependency Array의 값이 변경 될 때만 반환할_함수가 호출됨
- 그 외에 경우에는 그냥 캐싱되었던 값 그대로 가져와서 씀
const heavyWork = () => {
for (let i = 0; i < 1000000000; i++) {}
return 100;
};
- 예를 들어 컴포넌트 안에 이런 시간이 오래걸리는 무거운 작업이 있다고 가정할 때, 리렌더링 될 때마다 해당 함수는 결과 값을 내기 위해 많은 로딩 시간이 필요하다.
- 이런 경우, 어차피 결과값은 100으로 고정되어있으니까 그 결과값만 저장해서 반환해주면됨(캐싱)
- 이떄 useMemo() 사용해주기!!
const value = useMemo(() => heavyWork(), []);
- 해당 함수는 처음 (마운트과정) 렌더링 될 때에만 작업을 실행하고 그 결과값을 기억했다가 사용함(훨씬 효율적!!!)
import React, { useEffect, useState } from "react";
function ObjectComponent() {
const [isAlive, setIsAlive] = useState(true);
const [uselessCount, setUselessCount] = useState(0);
const me = {
name: "Ted Chang",
age: 21,
isAlive: isAlive ? "생존" : "사망",
};
useEffect(() => {
console.log("생존여부가 바뀔 때만 호출해주세요!");
}, [me]);
return (
<>
<div>
내 이름은 {me.name}이구, 나이는 {me.age}야!
</div>
<br />
<div>
<button
onClick={() => {
setIsAlive(!isAlive);
}}
>
누르면 살았다가 죽었다가 해요
</button>
<br />
생존여부 : {me.isAlive}
</div>
<hr />
필요없는 숫자 영역이에요!
<br />
{uselessCount}
<br />
<button
onClick={() => {
setUselessCount(uselessCount + 1);
}}
>
누르면 숫자가 올라가요
</button>
</>
);
}
export default ObjectComponent;
- useEffect를 이용해서 me의 정보가 바뀌었을 때만 발동되게끔 dependency array를 넣어놨는데, 엉뚱하게도 count를 증가하는 버튼을 누르면, 계속 log가 찍히는 것을 볼 수가 있음.....
왜 그럴까요?
위 예제에서 버튼이 선택돼서 uselessCount state가 바뀌게 되면 → 리렌더링이 되죠 → 컴포넌트 함수가 새로 호출됩니다 → me 객체도 다시 할당해요(이 때, 다른 메모리 주소값을 할당받죠) → useEffect의 dependency array에 의해 me 객체가바뀌었는지 확인해봐야 하는데 → 오잉?! 이전 것과 모양은 같은데 주소가 달라요! → 리액트 입장에서는 me가 바뀌었구나 인식하고 useEffect 내부 로직이 호출됩니다.
불변성과 관련이 깊어요.
만약, me가 기본형 데이터 형태로 저장되었으면, 아무리 리렌더링되어도, 이미 할당된 메모리 주소를 참조했을 것임. 그래서 useEffect 내부로직 호출 안됨.
let me = isAlive ? "생존" : "사망";
- 이유: 기본형 데이터는 불변성을 띄고, 참조형은 불변성을 띄지 않아서...
- 기본형 데이터는 값 자체를 메모리에 저장하므로 컴포넌트가 다시 렌더링되어도 메모리 주소가 변경되지 않음. 동일한 값을 다시 할당하면 리액트는 메모리 주소가 그대로라고 판단해, 값이 그대로 유지됨.
- 반면, 참조형 데이터(객체, 배열 등)는 렌더링할 때마다 새로운 객체로 간주되어 새로운 메모리 주소가 할당됨. 심지어 객체의 내용이 같아도 매번 새로운 주소를 가지게 되어 리액트는 이를 새로운 값으로 인식하게됨.
//값을 캐싱!!
//참조형 데이터의 메모리 주소 픽스하기
const me = useMemo(() => {
return { name: "Ted Chang", age: 21, isAlive: isAlive ? "생존" : "사망" };
}, [isAlive])
useEffect(() => {
console.log("생존여부가 바뀔 때만 호출해주세요!");
}, [me]);
- 이렇게 useMemo 사용으로, uselessCount가 증가되도 영향없게함.
==> useMemo는 최적화를 위해 필요한 경우에만 사용해야함. 너무 많이 사용하면 오히려 메모리 사용량이 증가해 성능이 떨어질 수 있음.(캐싱하는 것 때문에...)
===> 정말로 계산 비용이 많이 드는 작업이나 렌더링에 영향을 줄 때에만 사용하도록 하자!!!!
Custom Hooks
// src/App.jsx
import React from "react";
import { useState } from "react";
const App = () => {
// input의 갯수가 늘어날때마다 state와 handler가 같이 증가한다.
const [title, setTitle] = useState("");
const onChangeTitleHandler = (e) => {
setTitle(e.target.value);
};
// input의 갯수가 늘어날때마다 state와 handler가 같이 증가한다.
const [body, setBody] = useState("");
const onChangeBodyHandler = (e) => {
setBody(e.target.value);
};
return (
<div>
<input
type="text"
name="title"
value={title}
onChange={onChangeTitleHandler}
/>
<input
type="text"
name="title"
value={body}
onChange={onChangeBodyHandler}
/>
</div>
);
};
export default App;
위의 코드는 input을 구현하고 useState로 각 input의 value를 관리한다.
변화되는 값에 따라 리렌더링을 하기 위해 사용한 useState가 여러번 쓰이고 있는데, 여기서 조금 아쉬운 부분이 있다.
==> input의 개수가 증가하면 useState와 이벤트핸들러도 같이 증가하고 그로 인해 코드의 중복이 생긴다는 점.
지금은 input이 단지 2개이기때문에 복잡해보이지 않으나 만약 input이 수십개가 되면 중복코드가 점점 더 많이 발생할 것임!
==> 리액트에서는 위 예시처럼 반복되는 로직이나 중복되는 코드를 우리만의 훅, 즉 커스텀 훅을 통해서 관리할 수 있음.
<Custom hook 만들 때 고려해야하는 점!>
- 커스텀 훅의 함수 이름은 use로 시작하는 것이 좋습니다 (예: useInput).
- 파일 이름은 use로 시작할 필요는 없으며, 원하는 대로 지정할 수 있습니다.
// src/hooks/useInput.js
import React, { useState } from "react";
const useInput = () => {
// 2. value는 useState로 관리하고,
const [value, setValue] = useState("");
// 3. 핸들러 로직도 구현합니다.
const handler = (e) => {
setValue(e.target.value);
};
// 1. 이 훅은 [ ] 을 반환하는데, 첫번째는 value, 두번째는 핸들러를 반환합니다.
return [value, handler];
};
export default useInput;
- 커스텀훅이란, 우리가 컴포넌트에서 구현해왔던 useState와 핸들러를 이렇게 뽑아서 따로 빼놓은 것 함수인 것
// src/App.jsx
import React from "react";
import useInput from "./hooks/useInput";
const App = () => {
// 우리가 만든 훅을 마치 원래 있던 훅인것마냥 사용해봅니다.
const [title, onChangeTitleHandler] = useInput();
const [body, onChangeBodyHandler] = useInput();
return (
<div>
<input
type="text"
name="title"
value={title}
onChange={onChangeTitleHandler}
/>
<input
type="text"
name="title"
value={body}
onChange={onChangeBodyHandler}
/>
</div>
);
};
export default App;
기능은 커스텀훅을 만들기전과 동일하게 작동하되, 중복코드가 사라지고 전체적인 코드의 양도 감소함.
==>여러가지 기능을 구현하면서 중복되는 로직이 있으면 “이것을 어떻게 훅으로 빼볼 수 있을까” 라고 생각해보자!!!
'프론트엔드 부트캠프' 카테고리의 다른 글
[React 숙련 1주차(5)] Redux: todolist, RTK (1) | 2024.11.07 |
---|---|
[React 숙련 1주차(4)] Redux: useSelector, dispatch action 객체, payload, ducks (0) | 2024.11.06 |
[React 숙련 1주차(2)] 리액트 훅: useState, useEffect, useRef, useContext (4) | 2024.11.05 |
[React 숙련 1주차(1)] Styled-Components (2) | 2024.11.04 |
[React] 올림픽 메달 추적: 개인 프로젝트 정리(2) (0) | 2024.11.01 |