React.memo

kim98111·2022년 1월 24일
0

React

목록 보기
15/28
post-thumbnail

컴포넌트에 등록된 상태가 변경되면 함수 컴포넌트가 재평가 됩니다. 이때 return 문에 작성된 하위 컴포넌트까지 모두 재평가됩니다. 재평가가 필요하지 않는 하위 컴포넌트까지 모두 재평가하는 것은 불필요하며 이는 최적화가 필요한 부분입니다.


// App.js
import { useState } from 'react';
import DemoOutput from './DemoOuput.js';

function App() {
    console.log('App Running');

    const [showParagraph, setShowParagraph] = useState(false);
    
    const toggleParagraphHandler = () => {
        setShowParagraph(prevShowParagraph => !prevShowParagraph);
    }
    
    return (
        <div>
            <h1>Hi there!</h1>
            <DemoOutput show={false}/>
            <button onClick={toggleParagraphHandler}>Toggle Paragraph!</button>
        </div>
    );
}

export default App
// DemoOuput.js
const DemoOutput = props => {
    console.log('DemoOuput Running');

    return (
        {props.show && <p>DemoOutput</p>}
    );
}

export default DemoOuput;

App 컴포넌트에서는 DemoOuput 컴포넌트를 return 문에서 사용하고 있습니다. 만약 button 요소를 클릭하면 toggleParagraphHandler가 호출되고 내부에서는 showParagraph 상태값을 변경하는 상태 변경 함수인 setShowParagraph 함수가 호출됩니다.

하지만 사용자에게 보여지는 데이터는 어떤 것도 변경되지 않았습니다. 즉, 실제 DOM에서도 어떠한 변경도 일어나지 않습니다. 이는 리액트가 이전 가상돔과 현재 가상돔을 비교하는데 차이점이 존재하지 않기 때문에 실제 DOM을 조작하지 않았습니다.

문제점은 어떠한 것도 변경되지 않지만 App 컴포넌트에 등록된 상태가 변경되어 App 컴포넌트가 재평가되고 그에 따라 하위 컴포넌트인 DemoOuput 컴포넌트까지 재평가된다는 것입니다. 즉, 콘솔창에 App Running 문자열이 출력되고 다음 바로 DemoOuput Running이 출력되는 것을 확인할 수 있습니다.

그래서 우리는 DemoOuput 컴포넌트를 "특정 상황에서만 재평가"되도록 리액트에게 명령해야 합니다. 여기서 특정 상황이란 예를 들어, 컴포넌트가 받는 props가 바뀌었을 때만 재평가되도록 하는 것입니다.

우리는 리액트에게 이러한 명령을 내리기 위해서 특정 상황에만 재평가될 컴포넌트의 export 문에 React.memo 함수를 사용해야 합니다.
즉, 위 예제에서는 DemoOuput 컴포넌트 파일에서 export default DemoOuput;export default React.memo(DemoOutput);으로 작성해줍니다. 이때 인수로는 함수형 컴포넌트만 전달할 수 있습니다.


React.memo 함수로 함수형 컴포넌트를 최적화합니다. memo는 인수로 전달한 컴포넌트에 어떤 props가 전달되는지 확인하고 모든 props의 값을 확인한 다음 그 값이 "이전 props 값이랑 비교하여 값이 변경되었을 때만 컴포넌트를 재평가"하게 됩니다.
즉, 부모 컴포넌트가 재평가되더라도 하위 컴포넌트에게 전달하는 props 값이 바뀌지 않은 경우, 하위 컴포넌트는 재평가되지 않습니다.

DemoOuput을 아래 코드처럼 수정해보겠습니다.

// DemoOuput.js
import React from 'react';

const DemoOutput = props => {
    console.log('DemoOuput Running');

    return (
        {props.show && <p>DemoOutput</p>}
    );
}

// React.memo를 통해 최적화되도록 수정
export default React.memo(DemoOuput);

이제 버튼을 클릭하더라도 DemoOuput에게 전달되는 props값이 언제나 false로 하드 코딩되어 있으므로 재평가 되지 않습니다.
이는 콘솔창에 버튼을 클릭하면 App Running 문자열만 출력되고, DemoOuput Running은 출력되지 않는 것으로 확인할 수 있습니다.

참고로 만약 DemoOuput 컴포넌트의 return 문에 자식 컴포넌트가 존재하더라도 자식 컴포넌트도 당연히 재평가되지 않습니다.


이렇게 React.memo 메서드를 통해 불필요한 렌더링을 막을 수 있습니다. 여기서 의문점이 존재합니다. 만약 "모든 컴포넌트를 최적화하면 되지 않을까?" 라는 의문점이 생깁니다.

memo 메서드를 통해 최적화 하는 데 조건이 존재합니다. 위 예제로 설명을 하면, App 컴포넌트가 재평가될 때마다 DemoOuput 컴포넌트는 새로 전달된 props와 이전에 전달된 props 값을 비교합니다.
즉, 리액트는 두 가지 일을 하게 됩니다. 첫 번째로는 "이전 props 값을 보관"하고, 두 번째로는 "props 값을 비교"하는 일입니다. 이는 어느정도 비용일필요한 동작입니다.

어떤 컴포넌트를 최적화하느냐에 따라 효율이 달라집니다. "컴포넌트를 재평가하는 효율을 prop을 비교하는 효율과 바꾸는 것"인데 어느 쪽이 더 효율적인지는 알 수 없습니다. 이는 사용하는 prop의 수와 컴포넌트의 복잡성, 컴포넌트의 자식 컴포넌트 수에 달려있습니다.

만약 자식 컴포넌트가 많아 컴포넌트 트리가 깊은 경우 React.memo가 유용하게 동작합니다. 컴포넌트 트리의 상위에서 컴포넌트 트리의 불필요한 가지를 재실행하는 것을 방지해주기 때문입니다.
반면 컴포넌트의 prop가 부모 컴포넌트가 재평가할 때마다 변경된다면 React.memo를 사용하는 것은 좋지 않습니다. 결과적으로 컴포넌트가 다시 실행되면서 props 값을 비교한 내용이 저장되는 비용이 많이 들어가기 때문에 좋지 않습니다.

즉, 모든 컴포넌트에 React.memo를 사용하기보다는 컴포넌트 트리의 뿌리를 골라서 자식 컴포넌트의 가지를 모두 잘라내는 것이 더 효율적입니다.


주의할 점이 있습니다. 아래 예제를 통해 살펴보겠습니다.

// App.js
import React, { useState } from 'react';
import DemoOutput from './DemoOutput.js';
import Button from '../UI/Button.js';

function App() {
    console.log('App Running');

    const [showParagraph, setShowParagraph] = useState(false);
    
    const toggleParagraphHandler = () => {
        setShowParagraph(prevShowParagraph => !prevShowParagraph);
    }
    
    return (
        <div>
            <h1>Hi there!</h1>
            <Output show={false}/>
            <Button onClick={toggleParagraphHandler}>Toggle Paragraph!</Button>
        </div>
    );
}

export default App

이번에는 button 요소 대신에 Button 컴포넌트를 사용했습니다. Button 컴포넌트는 onClick prop으로 이벤트 핸들러만 전달받습니다. 이러한 경우 Button 컴포넌트는 App 컴포넌트가 재평가되더라도 Button 컴포넌트는 언제나 동일한 이벤트 핸들러를 전달받고, 변경되는 데이터도 없기때문에 재평가하지 않아도 되므로 React.memo를 사용하는 것이 효율적으로 보입니다.

Button 컴포넌트를 다음과 같이 작성합니다.

// Button.js
import React from 'react';

const Button = props => {
    console.log('Button Running');

    return (
        <button onClick={props.onClick}>Toggle Paragraph!</ button>
    );
}

export default React.memo(Button);

이후 버튼을 클릭하면 App 컴포넌트가 재평가되면서 콘솔창에 App Running 문자열이 출력되고 Button 컴포넌트는 재평가되지 않을것이라고 예상했지만 예상과는 달리 재평가되어 콘솔창에 Button Running 문자열도 출력되는 것을 확인할 수 있습니다.

이는 App 컴포넌트가 재평가될 때 toggleParagraphHandler 함수 정의도 다시 평가되어 새롭게 메모리에 할당됩니다. 즉, App 컴포넌트다 재평가될 때마다 App 함수 안에 정의한 함수는 "매번 새롭게 생성"됩니다. 그러므로 Button 컴포넌트에게 onClick prop으로 전달되는 toggleParagraphHandler 함수는 "매번 새로운 값으로 리액트가 인식"하여 Button 컴포넌트도 "재평가"가 됩니다.
React.memo는 마지막에 현재 props를 이전 props를 비교합니다. 비교할 때 비교 연산자(===)을 사용합니다.

참고로 원시 타입 경우 컴포넌트가 재평가된다고 하더라도 무조건 새로운 원시값을 다시 생성하는 것이 아니라, 메모리에 동일한 원시값이 존재하는 경우 그 원시값이 저장되어 있는 메모리 주소값을 사용합니다. 즉, 원시값이 저장된 메모리의 주소값을 재사용합니다. 하지만 참조 타입의 경우 컴포넌트가 재평가될 때마다 무조건 새로운 참조 값을 다시 생성하게 됩니다. 이러한 이유로 인해서 props로 전달한 함수의 경우 매번 새로운 값으로 인식되어 재평가됩니다.

profile
Frontend Dev

0개의 댓글