반응형

💎useMemo 파헤치기

🤔그래서 useMemo가 뭐지?

리액트에서 useMemo를 사용하게 되면 불필요한 렌더링을 막고 앱을 최적화 시킬 수 있다.
여기서 Memo는 Memoization을 뜻하는데, 메모이제이션이란 간단하게 프로그램적으로 반복 행위가 있는 것을 메모리에 저장하여 가져다 사용해서 실행속도를 빠르게 처리하는 기술을 뜻한다.
즉, 한번 동작자체가 부하가 심한 행위가 있는 기능이면서 페이지의 렌더링이 심한 컴포넌트에서 적용하기에 안성맞춤 기술이다.
아직 텍스트만으론 잘 이해가 되질 않는다. 더욱 자세히 알아보겠다.

⚙️useMemo의 구성

function fn(){...}
useMemo(()=> fn(), []);

useMemo는 콜백함수와 의존성 배열 2개의 매개변수를 받는다.
첫번째는 저장(캐싱) 할 함수값을 넣는다.
두번째 의존성 배열부분에는 업데이트 되었을때 저장한 함수가 동작될 요소를 넣어준다.
즉, 배열에 저장된 요소가 업데이트 되면 그때 저장된 함수가 동작된다.
빈 배열을 넣게되면 페이지가 처음 로드되었을때 한번만 캐싱되고 이후부터 사용할 때만 캐싱된 함수를 호출해서 사용한다.

🙃그렇다면 무조건 쓰면 좋을까?

그렇다고 무조건 모든 객체나 원시타입들을 useMemo에 저장해놓고 쓰는게 좋을까?
대답은 No이다.
메모이제이션이기 때문에 결국 useMemo에 등록된 요소들은 결국 어딘가에 저장이 되는데, 개발을 하면서 상황에 맞지 않는 모든 것을 다 저장한다면 오히려 페이지에 부하가 발생하고 느려지는 상황을 초래할 수 도 있다.

😤그럼 언제 써야합니까?!

예시가 제일 빠른 확인이 가능할 것 같다.

import { useState } from "react";

function slow(ms: number) {
    const wakeUpTime = Date.now() + ms;
    while (Date.now() < wakeUpTime) {}
}

function calculator(arr: number[]) {
    console.log("calculator --- start");
    slow(100);
    if (arr.length === 0) return 0;
    return Math.floor(arr.reduce((a, b) => a + b, 0) / arr.length);
}

// useMemo 테스트 컴포넌트
export default function TestUseMemo() {
    const [number, setNumber] = useState("");
    const [inputNumber, setInputNumber] = useState<number[]>([]);
    return (
        <div>
            <div>
                <input
                    type="number"
                    name="input"
                    value={number}
                    onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
                        setNumber(e.target.value);
                    }}
                />
                <button
                    onClick={() => {
                        setInputNumber((prev) => [...prev, +number]);
                        setNumber("");
                    }}
                >
                    추가
                </button>

                <div>
                    <b>지금까지 등록된 값</b>
                    <ul>
                        {inputNumber.map((x) => (
                            <li>{x}</li>
                        ))}
                    </ul>
                </div>

                <div>
                    <b>등록된 평균값:</b>
                    {calculator(inputNumber)}
                </div>
            </div>
        </div>
    );
}

일단 부하가 심한 기능이라는 가정을 위해 일부러 slow라는 함수를 정의하여 calculator()내부에서 호출하여 강제로 동기화 기능을 처리하였다.
해당 부분때문에, 약 0.1초 정도의 부하만 줬는데도 부드럽지 않고 끊기는 느낌이 있다.

해당 예제는 input에 입력한 숫자를 추가하면 평균값을 보여주는 간단한 예제인데, 벌써부터 최악인 모습으로 input에 입력할때마다 onChange에서 변경된 state값에 의해 매번 페이지가 재렌더링이 일어나면서 느려터진 slow함수덕에 엄청 버벅거리는 모습을 경험할 수 있다.

실제로도 input에 "1234" 4번만 입력했는데 매번 calculator가 호출되고 있고 있다.

이럴때 바로 useMemo를 사용하면 추가버튼을 누를때만 해당 함수가 호출되어 최적화를 할 수 있다.

최적화하기

import { useState, useMemo } from "react";

function slow(ms: number) {
    const wakeUpTime = Date.now() + ms;
    while (Date.now() < wakeUpTime) {}
}

function calculator(arr: number[]) {
    console.log("calculator --- start");
    slow(100);
    if (arr.length === 0) return 0;
    return Math.floor(arr.reduce((a, b) => a + b, 0) / arr.length);
}

// useMemo 테스트 컴포넌트
export default function TestUseMemo() {
    const [number, setNumber] = useState("");
    const [inputNumber, setInputNumber] = useState<number[]>([]);

    /*
     * useMemo를 통해 최적화를 한다.
     * 두번째 매개변수 의존 배열에는 해당 값이 바뀔때만 호출될 수 있도록 설정한다
     * 첫번째 매개변수에는 콜백함수를 넣는데 두번째에 넣은 값이 바뀔때마다 해당 콜백이 호출된다.
     */
    const avg = useMemo(() => calculator(inputNumber), [inputNumber]);
    return (
        <div>
            <div>
                <input
                    type="number"
                    name="input"
                    value={number}
                    onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
                        setNumber(e.target.value);
                    }}
                />
                <button
                    onClick={() => {
                        setInputNumber((prev) => [...prev, +number]);
                        setNumber("");
                    }}
                >
                    추가
                </button>

                <div>
                    <b>지금까지 등록된 값</b>
                    <ul>
                        {inputNumber.map((x) => (
                            <li>{x}</li>
                        ))}
                    </ul>
                </div>

                <div>
                    <b>등록된 평균값:</b>
                    {avg}
                </div>
            </div>
        </div>
    );
}

이제 초기 렌더링될 때 한번 호출되고 input에 입력해보면 전혀 부하가 발생하지 않는 걸 볼 수 있다.
숫자를 입력하고 추가버튼을 누르면 그때 콜백함수가 호출되면서 calculaotr가 호출된다.

⚠️유의사항

어느정도 useMemo를 언제 사용해야할지와 사용법에 대해 알아보았는데, 조심해야할 부분이 있다.
자바스크립트에는 원시타입과 객체타입의 개념이 있다.
원시타입의 경우 특정 정수나 문자열이 있을 때, 해당 값을 주소값에 넣어두고 같은 값이라면 어떤 변수든 같은 주소값을 바라보기에 아래와 같이 비교 연산이 가능하다.

const test = "hello";
const test2 = "hello";
test === test2 // true 같은 주소값을 바라본다.

하지만 객체타입인 배열이나 객체의 종류의경우 비교연산시 문제가 발생한다.

const obj = {name: "mike"}; // 0x01 에 할당
const obj2 = {name: "mike"}; // 0x02에 할당
obj === obj2 // false 주소값이 서로 다르다.

shallow copy, deep copy의 개념이 필요한 부분인데 이부분에 대해서 더 자세히 알아보면 좋을 것이다.

객체타입의 오류

아래 예제를 보면 위에서 왜 객체 타입에 대한 개념을 언급했는지 알 수 있다.

import { useState, useEffect } from "react";

// useMemo 테스트 컴포넌트
export default function TestUseMemo() {
    const [name, setName] = useState("");
    const [regitName, setRegitName] = useState("");
    const obj = { name: regitName };

    useEffect(() => {
        console.log("regitName에 추가될 때만 호출되어야 합니다!");
    }, [obj]);

    return (
        <div>
            <div>
                <input
                    type="text"
                    name="input"
                    value={name}
                    onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
                        setName(e.target.value);
                    }}
                />
                <button onClick={() => setRegitName(name)}>변경</button>
                <div>
                    <b>입력한 이름</b>
                    <ul>{obj.name}</ul>
                </div>
            </div>
        </div>
    );
}

obj라는 객체를 useState에서 받아온 regitName값으로 정의하는 경우 해당 경우에도 매번 재 렌더링이 발생한다.
새로운 객체를 계속해서 새로 넣는다고 판단되기 때문인데, 이런 경우에도 useMemo를 통해 처리가 가능하다.

객체 개선하기

import { useState, useEffect, useMemo } from "react";

// useMemo 테스트 컴포넌트
export default function TestUseMemo() {
    const [name, setName] = useState("");
    const [regitName, setRegitName] = useState("");
    // const obj = { name: regitName };
    const obj = useMemo(() => {
        return { name: regitName };
    }, [regitName]);

    useEffect(() => {
        console.log("regitName에 추가될 때만 호출되어야 합니다!");
    }, [obj]);

    return (
        <div>
            <div>
                <input
                    type="text"
                    name="input"
                    value={name}
                    onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
                        setName(e.target.value);
                    }}
                />
                <button onClick={() => setRegitName(name)}>변경</button>
                <div>
                    <b>입력한 이름</b>
                    <ul>{obj.name}</ul>
                </div>
            </div>
        </div>
    );
}

위 처럼 obj객체를 useMemo로 감싸서 처리하면 추가 버튼을 누르는 경우에만 regitName이 동작하면서 useEffect의 콘솔값이 그때 찍히는걸 볼 수 있다.

반응형