이번에는 React.memo()가 무엇이고 어떤 역할을 하는지 코드를 통해 알아보자.
import React, { useState } from "react";
import Button from './components/UI/Button/Button';
import DemoOutput from "./components/Demo/DemoOutput";
import "./App.css";
function App() {
const [showParagraph, setShowParagraph] = useState(false);
console.log("APP RUNNING!");
const toggleParagraphHandler = () => {
setShowParagraph(prevShowParagraph => !prevShowParagraph);
}
return (
<div className="app">
<h1>Hi there!</h1>
<DemoOutput show={false} />
<Button onClick={toggleParagraphHandler}>Toggle Paragraph!</Button>
</div>
);
}
export default App;
import React from 'react';
import classes from './Button.module.css';
const Button = (props) => {
console.log("BUTTON RUNNING!");
return (
<button
type={props.type || 'button'}
className={`${classes.button} ${props.className}`}
onClick={props.onClick}
disabled={props.disabled}
>
{props.children}
</button>
);
};
export default Button;
import React from "react";
const DemoOutput = (props) => {
console.log("DEMOOUTPUT RUNNING!");
return <p>{props.show ? 'This is new!' : ''}</p>
}
export default DemoOutput;
먼저 초기 화면은 다음과 같다.
먼저, 초기 렌더링 시에 다음과 같이 콘솔에 출력된다.
"APP RUNNING!"
"DEMOOUTPUT RUNNING!"
"BUTTON RUNNING!"
자 이제 버튼을 눌러보자. 콘솔에 어떻게 출력이 될까?
"APP RUNNING!"
"DEMOOUTPUT RUNNING!"
"BUTTON RUNNING!"
똑같이 출력이된다..! 원래대로라면 "DEMOOUTPUT RUNNING"은 출력되면 안된다.
props로 전달되는 show의 값이 변하지 않기 때문이다.
그런데 왜 출력이 되는걸까?
App
함수는 상태가 변경되었기 때문에 재실행된다. App
내부에는 뭐가 있는가?
당연히 반환문이 있고 이것은 JSX 코드를 반환한다.
여기에 있는 모든 JSX 요소들은 결국 컴포넌트 함수에 대한 함수 호출과 같다.
그러니까, DemoOutput
컴포넌트에 대한 함수를 호출하고 Button
컴포넌트에 대한 함수를 출력한다.
부모 컴포넌트들이 변경되었고 자식 컴포넌트는 부모 컴포넌트의 일부이기 때문에, 자식 컴포넌트들 역시 다시 실행되고 재평가된다.
결국, props
의 값은 상관이 없다는 의미이다.
기억해야 될 것은, 부모 컴포넌트가 재실행되면 이의 모든 자식 컴포넌트들 역시 재실행, 재평가 된다.
그렇다면, 모든 자식 컴포넌트들을 재실행, 재평가하면 낭비가 아닐까?
간단한 어플리케이션에서는 전혀 문제가 되지 않지만, 더 큰 어플리케이션이라면 최적화가 필요하다.
따라서, 우리는 특정한 상황일 경우에만 자식 컴포넌트가 재실행하도록 리액트에 지시를 해야한다.
(여기서 특정한 상황은 props
가 변경되었을 때를 의미한다.)
그렇다면, 어떻게 하면 위와 같이 행동하도록 명령할 수 있을까?
일단, 특정 상황에서만 재실행되도록 하고자 하는 자식 컴포넌트를 지정한 뒤에 React.memo()
로 감싸주면 된다.
코드로 살펴보자.
import React from "react";
const DemoOutput = (props) => {
console.log("DEMOOUTPUT RUNNING!");
return <p>{props.show ? 'This is new!' : ''}</p>
}
export default React.memo(DemoOutput);
컴포넌트를 export
하는 행에 React.memo
로 wrap
을 하면 된다.
결과는 다음과 같다.
"APP RUNNING!"
"BUTTON RUNNING!"
짜잔, "DEMOOUTPUT RUNNING!" 문구가 출력되지 않았다. 성공적이다!
이렇듯 React.memo
를 사용하면 특정 상황에만 컴포넌트가 재실행, 재평가되도록 할 수 있다.
하지만 염두해야할 것은, React.memo
는 함수형 컴포넌트에만 가능하다는 것이다.
클래스 기반의 컴포넌트의 경우 React.memo
는 작동하지 않는다.
ex. 클래스 기반의 DemoOutput.js
class DemoOutput extends Component {
render () {
return ( ... );
}
}
export default DemoOutput;
그렇다면 React.memo
의 역할은 무엇인가?
React.memo
는 인자로 들어간 컴포넌트에 어떤 props가 입력되는지 확인하고, 입력되는 모든 props
의 최신(신규)값을 확인한 뒤 이를 기존의 props
의 값과 비교하도록 리액트에게 전달한다.
그리고 props
의 값이 바뀐 경우에만 컴포넌트를 재실행 및 재평가하게 된다.
그리고 부모 컴포넌트가 변경되었지만 자식 컴포넌트의 props
값이 바뀌지 않았다면, 자식 컴포넌트의 실행은 건너뛴다.
또한, 위의 상황에서 자식 컴포넌트안의 자식 컴포넌트도 실행이 되지 않는다.
그렇다면 React.memo
를 사용하면 불필요한 낭비를 막을 수 있는데,
왜 모든 컴포넌트에 적용하지 않을까?
그 이유는, 최적화 또한 비용이 따르기 때문이다.
React.memo
는 자식 컴포넌트를 가지는 부모 컴포넌트에 변경이 발생할 때 마다 자식 컴포넌트로 이동하여 기존 props
값과 새로운 값을 비교하게 한다.
그렇다면 리액트가 2가지 작업을 할 수 있어야 한다.
기존의 props
값을 저장할 공간이 필요하다.props
를 비교하는 작업도 필요하다.이 각각의 작업은 개별적인 성능 비용이 필요하다.
따라서, 성능의 효율은 어떤 컴포넌트를 최적화하느냐에 따라 달라진다.
React.memo
를 사용하지 않았을 때 컴포넌트를 재평가하는데 필요한 성능 비용과 props
를 비교하는 성능 비용(React.memo
를 사용했을 때)을 서로 맞바꾸는 것과 같다.
그리고 이는 props
의 개수와 컴포넌트의 복잡도, 그리고 자식 컴포넌트의 개수에 따라 달라지므로 어느 쪽의 비용이 더 높다고는 판단하기가 어렵다.
만약, 자식 컴포넌트가 많아서 컴포넌트 트리가 매우 크다면, React.memo
는 매우 유용하게 쓰인다. 그리고 컴포넌트 트리의 상위에 위치해있다면 전체 컴포넌트 트리에 대한 쓸데없는 재렌더링을 막을 수 있다.
예시 코드에서 버튼이 변경될 일이 없다는 건 자명하기 때문에, 이를 재평가하는 것은 가치가 없다.
그렇다면, 이번에는 Button.js
에 React.memo
를 적용해보자.
import React from 'react';
import classes from './Button.module.css';
const Button = (props) => {
console.log("BUTTON RUNNING!");
return (
<button
type={props.type || 'button'}
className={`${classes.button} ${props.className}`}
onClick={props.onClick}
disabled={props.disabled}
>
{props.children}
</button>
);
};
export default React.memo(Button);
결과는 어떻게 될까?
"APP RUNNING!"
"BUTTON RUNNING!"
예상과는 다르게 "BUTTON RUNNING!"이 출력된다. 왜 이럴까?
출력이 된다는 말은 Button
의 props
값이 계속 바뀐다는 뜻이다.
<Button>
을 살펴보면 onClick
이라는 props
하나밖에 없다.
실행되는 함수는 항상 같은 것인데, memo가 동작하지 않는 이유는 리액트에서 흔하게 발생하는 오류 중 하나이기 때문이다.
App
컴포넌트는 어쨌든 간에 하나의 함수이기 때문에, 일반적인 자바스크립트 함수처럼 재실행된다.
이때 조금 다른 것은, 함수가 사용자가 아닌 리액트에 의해 호출된다는 것이다.
그렇지만 여전히 일반 함수처럼 실행되는데, 이 말은 즉슨 App 내부의 모든 코드가 다시 실행된다는 의미이다.
버튼에 전달되는 함수도 매번 재생성된다. 이는 App 함수의 모든 렌더링, 또는 모든 실행 사이클에서 완전히 새로운 함수로 취급된다.(재사용하지 않음 ❌)
왜 새로운 함수로 취급되냐하면, 매번 다시 만드는 상수이기 때문이다.
분명 App
내부의 모든 코드가 다시 실행된다고 했다.
그렇다면 이 상수도 새로 만들어지고 이 말은 새로운 함수가 만들어진다는 의미이다.
기능은 똑같지만, 이전과는 다른 새로운 함수인 것이다.
어? 그렇다면 아까 DemoOutput의 props에 전달된 false도 재생성되는 것 아닌가?
그러면 React.memo가 왜 동작하는걸까? false와 함수가 다른 점은 뭘까?
자, false
는 boolean 타입
이고 자바 스크립트에서 boolean
값은 원시 값이라고 칭한다.
예시 코드에서 React.memo
는 props
의 값을 확인하고 props.show
를 직전의 값인 props.previous.show
와 비교한다. (실제로 이렇게 작동하는 것 아님 ❌, 설명을 위해...)
이 비교는 일반 비교 연산자를 통해 진행한다. 그렇기 때문에 false === false
로 비교해서 true
가 나오기 때문에 이 둘은 같은 값이다.
(기술적으로는 서로 다른 boolean이지만, 이렇게 비교가 가능하다.)
하지만 배열이나 객체, 함수를 비교한다면 살짝은 달라진다.
자바스크립트에서 함수는 하나의 객체에 불과하다.
따라서, App
함수가 실행될 때 마다 새로운 함수 객체가 생성이 되고 이 함수 객체가 DemoOutput
의 onClick props
로 전달이 된다.
이렇게 되면, props.onClick
과 props.previous.onClick
을 비교하게 되는 것이다.
이 두 함수 객체가 완전히 똑같은 기능을 하더라도 자바스크립트에서는 절대 동일하지 않다.
이러한 자바스크립트의 동작 방식 때문에, React.memo
를 사용해도 값이 변경되었다고 인식하는 것이다.
정말 정말 중요한 개념이다. 꼭 알아두자!
그렇다면, React.memo를 사용해도 객체나 배열 또는 함수에는 사용할 수가 없는걸까..?
React.memo
를 사용하면 자식 컴포넌트의 불필요한 재렌더링을 막을 수 있다!
하지만 최적화는 비용이 따르기 때문에 항상 적용하지 않는다.자식 컴포넌트의 변화가 정말 불필요한지, 프로젝트 규모 등을 고려해서 적절하게 사용해야 한다.
또한, 배열, 객체, 함수에서는
React.memo
는 동작하지 않는다.