리액트는 state가 변경되면 관련된 모든 component가 rerendering되는 특징을 가진다. 이는 만약 가장 상위 component에서 state를 변경하였을 경우, 해당 component의 자식 component들도 모두 재실행된다는 것을 의미한다. 이러한 작동은 작은 단위의 프로젝트에서는 큰 문제가 없지만 큰 단위의 프로젝트에서는 성능 저하의 문제를 일으키기도한다.
React.memo
따라서, 리액트에서는 특정 컴포넌트를 특정 경우에만 실행하도록 지정할 수 있는 tool을 제공한다. 그것이 바로
react.memo()
이다. 해당 component가 가지는 prop이 변경되었을 경우에만 재실행하도록 하는 경우를 말한다.
react.memo
는 함수형 컴포넌트에서만 사용 가능하다. class 기반 component에서는 사용 불가능하다. 사용법은 간단하다. component를 export할 때 react.memo로 감싸주기만 하면된다.const Component(props) => { return ... } export default React.memo(Component);
React.memo()
의 인자로 들어오는 component에 어떤 props가 입력되는지 확인하고, 기존의 prop값과 새롭게 들어온 prop값을 비교한다. 이때 prop의 값이 바뀐 경우에만 컴포넌트를 재실행한다.부모 component가 변경되었지만, React.memo로 감싸진 자식 component의 props는 변경되지 않았다면 자식 component의 재실행은 스킵된다.
이때 React.memo()로 감싸진 component의 자식 component가 있다면, 이 자식 component도 memo가 적용되어 재실행되지 않는다.
React.memo
는 기존의 prop과 새로운 prop을 비교하여 동작하기 때문에 React.memo
를 사용하면 추가적인 공간이 필요하다. 우선 기존의 prop을 저장해 둘 공간과, 새로운 prop과 비교하는 동작이 추가로 작동해야 한다. 따라서 컴포넌트를 재실행할 때 드는 비용과, props만을 비교하는 비용 중 어떤 것이 더 효과적인지 생각해서 적용해야 한다!
<Button>
component를 React.memo
로 설정해두었다고 가정해보자.
// App.js
function App() {
const [showParagraph, setShowParagraph] = useState(false);
const toggleParagraphHandler = () => {
setShowParagraph(prevShowParagraph => !prevShowParagraph);
};
return (
<div className="app">
<h1>Hi there!</h1>
<DemoOutPut show={false} />
<Button onClick={toggleParagraphHandler}>Toggle Paragraph!</Button>
</div>
)
}
// Button.js
const Button = (props) => {
console.log('Button Running');
return (
<button onClick={props.onClick}>
{props.children}
</button>
)
}
이 경우 버튼을 클릭하면 React.memo
에 의해 console에 'Button Running'이 찍히지 않을 것이라고 예상할 것이다. 하지만, 실제로 콘솔을 열어보면 "Button Running"이 매번 찍히고 있음을 확인해볼 수 있다. 왜 이런 것일까?
리액트는 결국 자바스크립트 함수임을 알아야 한다. 자바스크립트 함수와 궁극적으로 다른 부분이라고 한다면 리액트가 함수를 호출한다는 부분인데, 이 부분을 제외하면 일반적인 자바스크립트 함수처럼 재실행된다. 이 말은 즉
App component
내부의 코드들이 모두 재실행된다는 것인데, 이 경우Button
의 prop으로 넘기는toggleParagraphHandler
함수 또한 재생성된다는 것이다. 왜냐하면 이 함수도 결국 const에 담긴 하나의 값이기 때문에 App이 재실행되면 내부에 있는 함수 또한 새롭게 만들어지게 되는 것이다.
그렇다면, <DemoOutPut>
에 있는 false
값도 새로 만들어진다는 의미인데, 왜 <DemoOutPut>
는 React.memo
가 적용되고, <Button>
은 적용되지 않는 것일까?
그 이유는! false
값은 원시값이기 때문이다! React.memo
가 이전 prop과 새로운 prop의 값을 비교할 때 ===
를 사용하여 값을 비교한다. 이때 원시값의 경우 ===
에 의해 아무리 새로 생성되어도 값이 같다면 prop이 변하지 않았다고 판단한다.
하지만 배열, 객체, 함수와 같은 참조값들을 비교할 때는 ===
에 의해 같은 값으로 비교되지 않는다. 따라서 false값과 달리 함수값은 새롭게 만들어지는 경우 아예 다른 값으로 판단되기 때문에 button의 prop값이 변한 것으로 판단되어 button
이 재실행되는 문제가 발생한다.
아니다! 이를 해결하기 위해 useCallback
을 활용하여 함수 재생성을 방지할 수 있다.
const reUsableFunc = useCallback(func, [dependencies])
react.memo
를 적용하고 싶은 component에 prop으로 전달하는 함수를 생성하고 저장하는 방식을 useCallback
**을 이용하여 생성한다면, 함수 prop에도 React.memo를 사용할 수 있다!
useCallback
useCallback
을 통해 함수를 생성한다면, 해당 함수를 저장해두는 역할을 하기 때문에App()
이 재실행될 때마다 함수가 재생성되지 않도록한다. 이렇게하면 동일한 함수 객체가 동일한 메모리 번지에 저장되기 때문에React.memo
를 통한 비교작업에서 함수가 변하지 않은 것으로 인식될 수 있다.즉 우리가 선택한 함수를 리액트의 내부 저장 공간에 저장한 후 함수 객체가 실행될 때마다 재생성하는것이 아니라, 해당 저장공간에 저장된 함수를 재사용하도록 한다.
useCallback
은useEffect
처럼 2개의 인자를 받는다. 첫번째 인자는 우리가 재생성을 방지하려는 함수 자체이고, 두번째 인자는 dependency 배열이다.useEffect
와 동일한 dependencies를 생각하면 된다. 함수 내부에서 사용하는 모든 값들을 dependency로 넣어주면 된다.
사용하려는 함수를 useCallback
으로 감싸주면 된다.
아래 예시의 경우 첫 번째 인자로 받는 함수에서 사용하는 값은 state를 update하는 setter함수 뿐이다. 이 경우 useEffect
에서와 같이 dependency로 함수를 넣는 것을 생략할 수 있다. dependency에는 보통 값 (state값, prop값) 등이 사용됨을 기억하자.
이와같이 dependency가 빈 배열일 경우 어떠한 경우에도 해당 함수가 변경되지 않음을 의미한다.
// App.js
import {useCallback} from 'react';
function App() {
const [showParagraph, setShowParagraph] = useState(false);
// useCallback()을 이용하여 감싸주기
const toggleParagraphHandler = useCallback(() => {
setShowParagraph(prevShowParagraph => !prevShowParagraph);
}, []);
return (
<div className="app">
<h1>Hi there!</h1>
<DemoOutPut show={false} />
<Button onClick={toggleParagraphHandler}>Toggle Paragraph!</Button>
</div>
)
}
내가 설정한 함수는 모든 재실행 주기마다 같은 로직을 통해 작동하는데 dependency를 작성해야하는 이유가 뭘까? 내 함수를 새로운 함수로 업데이트 해야하는 경우가 있나? 라는 생각이 들 수 있다. 이는 자바스크립트의 closure 개념과 연관있다.
토글을 띄우는 버튼A를 활성화하는 버튼B가 있다고 가정해보자. 즉 B를 먼저 눌러야지만 A 버튼이 눌리는 것이다.
function App() {
// toggle 띄우는 state (A)
const [showParagraph, setShowParagraph] = useState(false);
// toggle 띄우는 버튼 활성화 state (B)
const [allowToggle, setAllowToggle] = useState(false);
// 버튼 A의 onClick
const toggleParagraphHandler = useCallback(() => {
if (allowToggle) {
setShowParagraph((prevShowParagraph) => !prevShowParagraph);
}
}, []);
// 버튼 B의 onClick
const allowToggleHandler = () => {
setAllowToggle(true);
}
return (
...생략
)
}
이때 useCallback
을 사용하여 지정한 toggleParagraphHandler
의 경우 내부에서 사용되는 allowToggle
의 값이 계속해서 변경되는 state임에도 불구하고, useCallback
의 작용으로 함수가 생성되면서 리액트 저장공간 내부에 저장되어버리기 때문에 allowToggle
의 state값이 업데이트 되지 않고 기존의 false값으로 유지된다.
즉 업데이트되는 allowToggle
의 state값에 따라 A버튼을 활성화/비활성화 시켜야하는데 useCallback
을 바르게 사용하지 않아서 아무리 B버튼을 눌러 allowToggle
의 state값을 변경한다고 한들 A버튼이 활성화되지 않게되는 것이다.
따라서 useCallback
에서 dependencies를 사용하는 경우는 어떠한 state값 등에 따라서 함수를 변경하여야 할 때이다. 이에 따라 위 코드를 다시 작성해보면
...생략
// 버튼 A의 onClick
const toggleParagraphHandler = useCallback(() => {
if (allowToggle) {
setShowParagraph((prevShowParagraph) => !prevShowParagraph);
}
}, [allowToggle]);
즉
toggleParagraphHandle
r를 리액트 내부에 저장하되,allowToggle
의 값이 바뀔 경우 함수를 재생성해야 함을 알려주는 것이다. (useEffect랑 똑같음!)
컴포넌트를 재실행할 경우, 내부에 있는 모든 코드를 재실행하고 싶지 않을 수 있다. 예를 들어 전달받은 배열을 sort하는 로직이 있는 경우, 매번 sort를 하게되면 데이터가 커질 수록 성능에 과부하가 올 수 있다. 이처럼 재실행되는 컴포넌트 내부에서 일부 코드는 재실행되지 않게 하기 위해 사용하는 것이 useMemo()
이다.
useMemo
는 useCallback
이 함수를 저장한 것처럼 특정 값을 저장할 수 있게 해준다. 위의 예시에서처럼 sort하는 로직의 경우 sort된 배열의 결과값을 useMemo
를 통해 저장해둘 수 있다.
useMemo
useMemo
의 첫번째 인자로는 function을 받는다. 이 함수 자체를 기억하는 것은 아니고, 이 함수가 리턴하는 값을useMemo
를 통해 기억하게 된다. 두 번째 인자로는useCallback
과 같이 dependencies 배열을 받는다. 즉 첫번째 인자로 받은 function에서 사용하는 값을 dependencies에 넣고, 해당 값이 변경될 때에는 함수 내부의 return값이 변경되도록한다.
// App.js
import {useState, useCallback, useMemo} from 'react';
function App() {
...생략
// 참조값이 재생성되는 것을 막기 위해 useMemo 사용
const listItems = useMemo(() => [5,3,1,10,9], []);
return (
<div className="app">
// 기존대로 할 경우 참조값 재생성되어 원하는 결과 얻지 못함
// <DemoList title={listTitle} items={[5,3,1,10,9]} />
<DemoList title={listTitle} items={listItems}
...생략
</div>
)
}
이때 <DemoList>
에서 items로 받는 값이 배열임에 주의하자. 위에서 보았듯이 참조값을 전달할 경우에는 컴포넌트가 재실행될 때 재생성되기 때문에 겉으로 보기에는 같은값인듯 보여도 메모리상에서는 다른 주소에 저장된 다른 값이기 때문에 재실행될 때 item이 업데이트 되었다고 간주하여 DemoList 컴포넌트 내부에서 useMemo()
를 활용하여 재실행을 막아둔 sort하는 logic이 재실행될 것이다. 따라서 App.js에서도 useMemo
를 활용하여 참조값인 배열이 재생성되는 것을 막아야한다.
// DemoList.js
import {useMemo} from 'react';
const DemoList = (props) => {
const {items} = props;
const sortedList = useMemo(() => {
return props.items.sort((a,b) => a - b);
}, [items])
return (...생략)
}