개발조각

[React] useMemo 본문

React

[React] useMemo

개발조각 2023. 9. 15. 23:04
728x90
반응형

useState, useEffet 기본적인 리액트 훅을 사용에 익숙해졌다면 컴포넌트 성능을 최적화(Optimmization)하는 방법에 대해 알아야 된다.

최적화를 위해 사용되는 대표적인 훅으로 useMemo와 useCallback이 있다.

 

useMemo 개념 설명


useMemo에서 Memo라는 말은 Memoization(메모이제이션)을 의미한다.

메모이제이션이란

동일한 값을 리턴하는 함수를 반복적으로 호출해야 된다면

맨 처음 값을 계산할 때 해당값을 메모리에 저장해서 필요할 때마다 또다시 계산하지 않고 메모리에 꺼내서 재사용하는 기법

 

간단히 말하자면

자주 필요한 값을 맨처음 계산할 때 캐싱해 둬서 그 값이 필요할 때마다 다시 계산하는 것이 아니라   캐시 해서 사용해 주는 것이다.

 

꼭 기억해야 되는 점

function Component(){
    const value = calculate();
    return <div>{value}</div>
}

<Component />
function calculate(){
	return 10
}

 함수형 컴포넌트는 말 그대로 함수라는 사실

🎨 렌더링 → Component 함수 호출 → 모든 내부 변수 초기화

그리고 함수형 컴포넌트가 렌더링 된다는 것은 그 함수가 호출 된다는 것이다.

함수는 호출될때마다 함수 내부에 있는 모든 변수들이 초기화된다.

 

대부분의 리액트 컴포넌트는 state와 props의 변화에 의해 수많은 렌더링을 거친다.

컴포넌트가 렌더링이 될 때마다 value라는 변수가 초기화가 되기 때문에 calculate함수는 반복적으로 호출된다.

만약 calculate함수가 어떤 무거운 일을 하는 함수라면 굉장히 비효율적일 것이다.

(왜냐하면 calculate함수는 무의미한 계산을 반복해서 value라는 변수에 같은 값을 반복적으로 할당하기 때문이다.)

 

useMemo를 사용해서 메모이제이션을 해주면 이런 상황을 간편하게 해결할 수 있다.

function Component(){
    const value = useMemo(
        () => calculate(), []
    )
    
    return <div>{value}</div>
}

<Component />
🎨 렌더링 → Component 함수 호출, Memoization → 🎨 렌더링 → Component 함수 호출, Memoize된 값을 제사용

useMemo는 처음 계산된 결과 값을 메모리에 저장해서 컴포넌트가 반복적으로 렌더링이 된데도

계속 calculate 다시 호출하지 않고 이전에 미리 계산된 결과 값을 메모리에 꺼내와서 재사용할 수 있게 해 준다.

 

useMemo 구조


useMemo는 두 개의 인자를 받는다.

const vallue = useMemo(()=>{
	return calculate();
}, [item]);

첫 번째 인자는 콜백함수, 두번째인자는 배열(의존성 배열)을 받는다.

첫번째 인자인 콜백함수

메모이제이션할 해줄 값을 계산해서 리턴해주는 함수이며, 이 콜백함수가 리턴해주는 값이 useMemo가 리턴해주는 값이 된다.

두 번째 인자인 의존성 배열

useMemo는 의존성 배열에 있는 요소의 값이 업데이트될 때만 콜백함수를 다시 호출해서 메모이제이션된 값을 업데이트해서 다시 메모이제션을 해준다.

만약 빈배열을 넘겨주면 맨 처음 컴포넌트가 마운트되었을때만 값을 계산하고 이후에는 항상 메모이제이션된 값을 꺼내서 사용하게 된다.

 

useMemo, 꼭 필요할 때만!

무분별하게 남용하면 오히려 성능에 무리가 갈 수 있다.

useMemo를 사용한다는 건 값을 재활용하기 위해서 따로 메모리를 소비해서 저장을 해놓는다는 것이다.

그렇기 때문에 불필요한 값들까지 모두 메모이제이션을 해버린다면 성능저하가 악화될 수 있다.

그래서 useMomo는 필요할 때만 적절하게 사용하는 것이 중요하다.

 

예제 1


function App() {
    return (
        <>
            <UseMemo1 />
        </>
    );
}

const hardCalculate = (number) => {
    console.log("어려운 계산!");
    for (let i = 0; i < 999999999; i++) {} // 생각하는 시간
    return number + 10000;
};

const UseMemo1 = () => {
    const [hardNumber, setHardNumber] = useState(1);
    const hardSum = hardCalculate(hardNumber);

    return (
        <div>
            <h3>어려운 계산기</h3>
            <input
                type="number"
                value={hardNumber}
                onChange={(e) => setHardNumber(parseInt(e.target.value))}
            />
            <span>+ 10000 = {hardSum}</span>
        </div>
    );
};

export default App;

함수형 컴포넌트는 함수이기 때문에 렌더링이 되면 함수를 호출한다.

함수가 호출이 되면 내부에 있는 변수는 초기화가 된다.

그렇다는 건 UseMemo1가 렌더링이 된다는 것은 UseMemo1가 호출된다는 것을 의미하고,

함수가 호출이 되면 함수 내부에 있는 변수는 초기화가 된다.

 

UseMemo1이 반복적으로 렌더링이 된다는 것은 함수 내부에 있는 변수(hardSum)를 계속 초기화된다.

이 말은 hardCalulate가 반복적으로 불려서 hardSum에다가 반복적으로 값을 할당해 준다는 의미이다.

브라우저의 있는 숫자를 바꾸면 컴포넌트가 업데이트되기 때문에 UseMemo1는 다시 업데이트가 된다.

 

콘솔에 찍힌 다음 숫자가 바뀜

렌더링 될 때마다 hardCalulate를 계속 호출해 주는지 확인해 보면 맨 처음에 렌더링이 되었을 때 함수가 불려진다.

hardCalulate함수는 의미 없는 for문 때문에 시간이 좀 걸린다.

그래서 증가 버튼을 클릭하면 바로 증가되는 게 아니라 조금 딜레이 된다.

컴포넌트가 렌더링이 될 때 hardSum이 다시 초기화되려면 hardCalulate함수에서 다시 리턴 받아야 하는데

시간이 걸려 1초 뒤에 hardSum이 들어오게 되고 span태그에 보이게 된다.

 

쉬운 계산기 추가

function App() {
    return (
        <>
            <UseMemo1 />
        </>
    );
}

const hardCalculate = (number) => {
    console.log("어려운 계산!");
    for (let i = 0; i < 999999999; i++) {} // 생각하는 시간
    return number + 10000;
};
const easyCalculate = (number) => {
    console.log("짱 쉬운 계산!");
    return number + 1;
};

const UseMemo1 = () => {
    const [hardNumber, setHardNumber] = useState(1);
    const [easyNumber, setEasyNumber] = useState(1);

    const hardSum = hardCalculate(hardNumber);
    const easySum = easyCalculate(easyNumber);

    return (
        <div>
            <h3>어려운 계산기</h3>
            <input
                type="number"
                value={hardNumber}
                onChange={(e) => setHardNumber(parseInt(e.target.value))}
            />
            <span> + 10000 = {hardSum}</span>

            <h3>쉬운 계산기</h3>
            <input
                type="number"
                value={easyNumber}
                onChange={(e) => setEasyNumber(parseInt(e.target.value))}
            />
            <span> + 1 = {easySum}</span>
        </div>
    );
};

export default App;

쉬운 계산기는 for문이 없이 +1해 주는 계산기 임으로 딜레이 되지 않고 바로 화면에 나올 것 같지만

어려운 계산기처럼 1초 딜레이 된 후 숫자가 증가한다.

이유는 UseMemo1 컴포넌트가 함수형 컴포넌트이기 때문이다.

쉬운 계산기의 숫자를 클릭하면 easyNumber state가 업데이트가 되고 렌더링이 일어나면서

hardSum, easySum 두 개의 변수 다 초기화가 되고 hardCalculate도 다시 불리게 된다.

그러므로 hardNumber를 바꾸거나 easyNumber을 바꾸면 hardCalculate함수 안에 있는 의미 없는 for문이 돌아게된다.

(비효율적임)

 

easyNumber state가 바뀌었을 때는 hardCalculate함수를 불리지 않게 하는 방법은 없을까?

useMemo를 사용하면 된다.

useMemo훅을 사용하면 어떠한 조건이 만족했을 때만 변수들을 초기화되게 할 수 있다.

해당 조건에 만족시키지 않았다면 UseMemo1이 렌더링이 되지 않았어도 다시 초기화시켜주는 것이 아니라 이전에 갖고 있던 값을 그대로 사용해 준다. (다른 말로 메모이제이션이라고 한다.)

 

useMemo로 변경하기


// const hardSum = hardCalculate(hardNumber);
const hardSum = useMemo(() => {
    return hardCalculate(hardNumber);
}, [hardNumber]);

어려운 계산기를 클릭하면 1초씩 딜레이되지만, 쉬운 계산기를 클릭하면 즉각적으로 바뀌는 것을 알 수 있다.

 

 하지만 리액트에서 1초 걸리는 함수를 컴포넌트 내부의 변수를 초기화시킬 일은 많지 않다.

useMemo가 빛을 내는 상황은 따로 있다.

 

예제 2 (useMemo를 써야 되는 상황)


function App() {
    return (
        <>
            <UseMemo2 />
        </>
    );
}

const UseMemo2 = () => {
    const [number, setNumber] = useState(0);
    const [isKorea, setIsKorea] = useState(true);

    const location = isKorea ? "한국" : "외국";

    // location값이 바뀔때마다 실행
    useEffect(() => {
        console.log("useEffect 호출");
    }, [location]);

    return (
        <div>
            <h2>하루에 몇끼 먹어요?</h2>
            <input
                type="number"
                value={number}
                onChange={(e) => setNumber(e.target.value)}
            />
            <hr />

            <h2>어느 나라에 있어요?</h2>
            <p>나라: {location}</p>
            <button onClick={() => setIsKorea(!isKorea)}>비행기를 타자</button>
        </div>
    );
};

비행기를 타자를 클릭해야지만 useEffect가 호출이 된다.

location이 string과 같은 원시타입이 아니라 오브젝트일 경우 달라진다.

 

// string타입에서 오브젝트로 변경
const location = {
    country: isKorea ? "한국" : "외국",
};

// 교체
<p>나라: {location.country}</p>

비행기를 타자를 클릭할 때 말고도 숫자를 증가시킬 때도 useEffect가 호출이 된다.

location은 변경되지 않고 number만 변경되었는데 왜 useEffect가 불리는 걸까?

 

자바스크립트 타입에 대해서

원시 (Primitive) 타입
String
Number
Boolean
Null
Undefined
BigInt
Symbol
객체 (Object) 타입
원시 타입을 제외한 모든 것
Object
Array

 

변수는 어떤 값을 넣을 수 있는 상자이다.

원시 (Primitive) 타입
const location = "korea"

location
----------
"korea"
----------

어떤 값을 원시 타입에다가 할당하면 그 값은 상자에 바로 들어가지만

객체 (Object) 타입
const location = {
   country: "korea"
}

location
----------
#12345
----------
      ↓
#12345
----------
{country: "korea"}
----------

어떤 값을 객체 타입에다가 할당하면 객체는 너무 크기 때문에 상자에 바로 넣어지는 게 아니라

어떤 메모리상 공간에  할당이 되어 메모리 안에 보관이 된다.

그리고 그 변수 안에는 메모리 주소가 할당이 된다.

 

원시 (Primitive) 타입
const locationOne = "korea"
const locationTwo = "korea"

locationOne === locationTwo
> true

같은 원시값을 가지고 있는 두 가지 변수를 비교 연산자로 비교를 하면 true가 나온다.

왜냐하면 변수라는 상자 안에 있는 값이 동일하기 때문이다. 

객체 (Object) 타입
const locationOne = {
   country: "korea"
}

const locationTwo = {
   country: "korea"
}

locationOne === locationTwo
> false 

하지만 똑같아 보이는 객체를 넣어준 변수를 비교하면 false가 나온다.

왜냐하면 변수라는 상자 안에는 메모리상의 주소가 들어있고 두 객체는 다른 주소에 저장이 되어있기 때문이다.

다른 주소를 비교하는 거니까 false가 나오는 것이다.

 

하루에 몇 끼 먹어요?를 증가시키면 number state가 바뀌기 때문에 렌더링이 되는 거고

location변수는 아래와 같은 오브젝트를 또다시 할당하게 된다.

{
    country: isKorea ? "한국" : "외국",
};

 이 오브젝트가 눈으로 보기에는 똑같아 보일지라도 사실은 다른 오브젝트인 것이다.

(재할당할 때마다 다른 오브젝트를 할당하게 되는 것이다.)

이 오브젝트는 이전의 오브젝트와는 다른 메모리상 공간에 저장이 된다.

그리고 location이라는 변수는 또 생성된 오브젝트를의 주소를 참조하게 된다.

 

useEffect(() => {
    console.log("useEffect 호출");
}, [location]);

그래서 useEffect, React의 관점에서는 location(useEffect의 의존성배열) 안에 들어있는 주소가 바뀌었기 때문에

똑같이 생긴 오브젝트라도 location은 바뀌었다고 인식하게 된다. (location이 참조하고 있는 주소가 바뀌었기 때문에)

 

해결하려면 렌더링이 되었을 때 location변수가 초기화되는 것을 막아주면 된다.

location변수는 isKorea가 바뀌었을 때만 초기화가 되게 만들면 된다.

useMemo를 사용해서 location을 메모이제이션해보자

// const location = {
//     country: isKorea ? "한국" : "외국",
// };

const location = useMemo(() => {
    return {
        country: isKorea ? "한국" : "외국",
    };
}, [isKorea]);

이제 비행기를 타자를 클릭할 때만 useEffect 호출된다.

이렇게 되면 효과적으로 컴포넌트를 최적화해줄 수 있다.

 

최종코드

function App() {
    return (
        <>
            <UseMemo2 />
        </>
    );
}

const UseMemo2 = () => {
    const [number, setNumber] = useState(0);
    const [isKorea, setIsKorea] = useState(true);

    const location = useMemo(() => {
        return {
            country: isKorea ? "한국" : "외국",
        };
    }, [isKorea]);

    // location값이 바뀔때마다 실행
    useEffect(() => {
        console.log("useEffect 호출");
        // 뭔가 오래걸리는 작업...
    }, [location]);

    return (
        <div>
            <h2>하루에 몇끼 먹어요?</h2>
            <input
                type="number"
                value={number}
                onChange={(e) => setNumber(e.target.value)}
            />
            <hr />

            <h2>어느 나라에 있어요?</h2>
            <p>나라: {location.country}</p>
            <button onClick={() => setIsKorea(!isKorea)}>비행기를 타자</button>
        </div>
    );
};

export default App;

 

 이 글은 별코딩 리액트 훅 강의를 보고 정리한 글입니다.

728x90
반응형

'React' 카테고리의 다른 글

[React] useReducer  (0) 2023.09.19
[React] useCallback  (0) 2023.09.19
[React] useContext + Context API  (0) 2023.09.15
[React] useRef_DOM 요소 접근  (0) 2023.09.14
[React] useRef_변수 관리  (0) 2023.09.14
Comments