티스토리 뷰

React에서 컴포넌트는 쉽게 말해 '함수'를 말한다. 함수는 값이 변경되는 등 변화가 생겼을 때, 다시 호출된다.

문제는 'State'의 값이 호출될 때마다 초기화된다는 것이다. 간단한 상황일 때는 문제가 없으나, 복잡한 계산과 같은 경우일 때는 성능에 문제가 발생하게 된다.

 

이 때, 내용을 기억하라는 의미의 'Memoization'을 담은 'useMemo'를 사용한다면, 매번 호출되는 것이 아닌 해당 함수에 변화가 발생했을 때만 다시 렌더링되도록 하는 것이다.

 

아래의 예시 코드를 보면,

const expansiveCalculate = (numbers) => {
	console.log("계산중...");
    return numbers.reduce((acc, curr) => acc + curr, 0);
};

function MemoExample() {
	const [numbers, setNumbers] = useState([1, 2, 3, 4, 5]);
    const [addNumber, setAddnubmer] = useState("");
    const sum = expansiveCalculate(numbers);
    
    const handleAddNumber = () => {
    	setNumbers([...numbers, parseInt(addNumber)]);
        setAddNumber("");
    }
    
    return (
    	<div>
        	<input
            	type="text"
                value={addNumber}
                onChange={(e) => setAddNumber(e.target.value)}
            />
            <button onClick={handleAddNumber}>Add</button>
            <p>Sum: {sum}</p>
        </div>
    )
}

 

배열로 숫자들이 담긴 'numbers' 변수와 입력값을 담는 'addNumber' 값이 있다.

그리고, 'numbers'의 값을 모두 더해 반환하는 'expansiveCalculate' 함수와 입력값을 'numbers'에 담는 'handleAddNumber' 함수가 있다.

 

위의 코드를 실행하고 값을 입력하고 나면 어떤 일이 일어날까?

콘솔을 확인하면 알 수 있을 것이다. 값을 입력할 때마다 "계산중..."이라는 내용이 계속해서 나타날 것이고, 버튼을 눌러도 나타날 것이다. 즉, 값에 변화가 나타날 때마다 함수 안에 있는 변수들의 호출이 발생한다는 것이다.

 

위의 기능은 크지 않아 성능에 큰 문제가 없지만, 만약 오래 작업해야 할 기능이라면 문제가 발생할 것이다.

 

예를 들어, 아래처럼 반복문을 하나 넣어보자. 의미없이 반복되도록 만든 반복문이다.

const expansiveCalculate = (numbers) => {
	console.log("계산중...");
    for (let i = 0; i < 1000000000; i++) {}
    return numbers.reduce((acc, curr) => acc + curr, 0);
};

 

위 처럼 코드를 추가하고, 다시 실행하여 숫자를 입력한다면 정말 오랜 시간이 걸릴 것이다. 왜냐하면 위의 반복문이 동작하기 때문이다. 이렇듯, 무거운 작업이 있다면 성능에 큰 문제가 발생하게 되는 것이다.

더 문제는 위의 함수가 아무때나 호출이 된다는 것이다. 숫자를 입력할 때마다 함수를 재호출해 위의 함수도 재호출하게 되는 것처럼 말이다.

 

이를 방지하기 위해, 'expansiveCalculate' 함수에 'useMemo'를 적용해보자.

function MemoExample() {
	const [numbers, setNumbers] = useState([1, 2, 3, 4, 5]);
    const [addNumber, setAddnubmer] = useState("");
    const sum = useMemo(() => expansiveCalculate(numbers), []);
    
    const handleAddNumber = () => {
    	setNumbers([...numbers, parseInt(addNumber)]);
        setAddNumber("");
    }
    
    return (
    	<div>
        	<input
            	type="text"
                value={addNumber}
                onChange={(e) => setAddNumber(e.target.value)}
            />
            <button onClick={handleAddNumber}>Add</button>
            <p>Sum: {sum}</p>
        </div>
    )
}

 

위 처럼, 'useMemo'를 적용하면 함수에서 리턴하는 값을 저장해두게 된다. 그렇지만, 저장과 동시에 업데이트로 필요하다. 위의 내용에서 업데이트를 하게 되는 경우는 'numbers'의 값이 변경될 때마다 업데이트되도록 해야 한다. 반대로, 'addNumber'에 변화가 일어나더라도 업데이트가 일어나지 않도록 해야 한다.

 

    const sum = useMemo(() => expansiveCalculate(numbers), [numbers]);

 

간단하다. 위 처럼, 의존성 배열에 'numbers'를 추가하여 'numbers'에 변화가 일어날 때만 업데이트되도록 하면 된다.

그래서 정말 필요할 때만 업데이트가 되도록 하게 되는 것이다.

 

또 다른 상황에서도 'useMemo'를 사용할 수 있다.

 

const TodoPage = () => {
	const [number, setNumber] = useState(0);
    const [plusNumber, setPlusNumber] = useState(0);
    
    console.log("TodoPage");
    
    const changeTodo = () => {
    	setNumber(Math.random() * 100);
    }
    
    const changePlusNumber = () => {
    	setPlusNumber((prev) => prev + 1);
    }
    
    return (
    	<div>
        	<p>Number: {number}</p>
            <p>plusNumber: {plusNumber}</p>
            <button onClick={changeTodo}>Change Number</button>
            <button onClick={changePlusNumber}>Change PlusNumber</button>
            <Child todo={number} />
        </div>
    )
}

export default TodoPage;
const Child = ({ todo }) => {
	console.log("Chlid");
    return <div>Child: {todo}</div>
}

export defalut Child;

 

위의 코드를 보면, 하나는 랜덤으로 숫자를 저장하고, 다른 하나는 단순히 숫자를 하나씩 증가하여 저장한다. 그리고, 랜덤 숫자를 전달받는 Child의 자식 함수가 있다.

 

랜덤 숫자에 변경이 있을 때마다 Child 함수도 당연히 재호출될 것이다. 왜냐하면, 그 랜덤 숫자를 전달받기 때문이다. 그렇다면 문제는 무엇일까?

 

바로, 'plusNumber'에 변화가 일어나도 Child 함수가 재호출된다는 것이다. 둘이 아무 상관이 없는데도 말이다. 'TodoPage'라는 부모 함수가 호출이 일어나니, 자식 함수인 'Child' 함수도 호출이 발생하는 것이다.

 

만약, Child 함수가 무거운 함수라면 성능에 큰 문제가 발생할 것이다. 이를 방지하기 위해 'props'가 변할 때만 자식 함수가 재호출(재렌더링)이 될 수 있도록 만들 수 있다.

 

바로, 'React.memo()'를 사용하는 것이다.

 

const Child = ({ todo }) => {
	console.log("Chlid");
    return <div>Child: {todo}</div>
}

export defalut React.memo(Child);

 

위의 'React.memo()'는 컴포넌트 전체를 기억한다. 전달받은 'props'에 변화가 없으면 재호출하지 않고, 기존의 값을 나타낸다.

그러나, 위의 'React.memo()'에도 함정이 존재한다.

 

아래처럼 코드를 하나 추가한다고 가정해보자.

 

const TodoPage = () => {
	const [number, setNumber] = useState(0);
    const [plusNumber, setPlusNumber] = useState(0);
    // 추가
    const customTodo = { changedTodo: number + "Random" }
    
    console.log("TodoPage");
    
    const changeTodo = () => {
    	setNumber(Math.random() * 100);
    }
    
    const changePlusNumber = () => {
    	setPlusNumber((prev) => prev + 1);
    }
    
    return (
    	<div>
        	<p>Number: {number}</p>
            <p>plusNumber: {plusNumber}</p>
            <button onClick={changeTodo}>Change Number</button>
            <button onClick={changePlusNumber}>Change PlusNumber</button>
            <Child todo={customTodo} />
        </div>
    )
}

export default TodoPage;

 

'customTodo'라는 객체를 담는 변수를 추가했다. 그리고 자식 함수인 Child에 이 변수를 전달하도록 수정했다.

 

const Child = ({ todo }) => {
	console.log("Chlid");
    return <div>Child: {todo.changedTodo}</div>
}

export defalut React.memo(Child);

 

그리고, Child 함수에서 전달받아 위 처럼 사용하였다. 그리고 다시 실행하여 확인해보면 기존과 다르게 'plusNumber'에 변화가 일어나면 Child도 재호출된다. 분명히 'React.memo()'를 적용했는데도 말이다.

 

이것이 바로 치명적인 단점이다.

데이터 타입에는 두 가지가 있는데, 바로 '원시형 타입'과 '참조형 타입'이다.

 

  • '원시형 타입'은 말 그대로 숫자, 문자, 참, 거짓 등의 값을 말한다.
  • '참조형 타입'은 정확한 값을 특정하기 어려워 값이 저장되어 있는 주소를 참조하는 값을 말한다. 예를 들어, 배열과 객체가 있다.

 

'원시형 타입'은 재렌더링, 재할당이 되어도 값에 변화가 없다. 그러나 '참조형 타입'은 주소값이기 대문에 함수가 재호출될 때마다 주소값이 계속 변하는 문제가 있다. 그래서 변수 입장에서는 값이 변경되었다고 판단하기 때문에 호출이 발생하게 된다.

 

즉, 'React.memo()'를 사용해도 주소값이 변경되어 값이 바뀌었다고 인식하기 때문에 재호출이 발생하는 것이다. 이를 방지하기 위해 'useMemo'를 사용할 수 있다.

 

const customTodo = useMemo(() => {
	return { changedTodo: todo + "Random", name: "qwer", age: 4 };
}, [number]);

 

위 처럼, 'useMemo'를 사용하게 되면 참조 동일성을 유지하게 되어, number의 값이 바뀌지 않으면 기존의 값(주소값)이 그대로 유지가 된다.

 

그렇다면, 재호출을 방지하고 내용을 기억하니 'useMemo'를 자주 사용해야 겠다고 생각이 들 수 있다. 이는 오히려 좋지 않은 생각이다. 기억이 많아지면 공간 확보 문제로 낭비할 수 있고, 예상치 못한 손실이 발생할 수 있다.

 

그렇기 때문에 정말 성능적으로 문제가 발생하거나, 웹사이트 속도에 큰 문제가 발생하는 부분에만 고려해보는 것이 좋다. 일반적으로 'useMemo'와 'useCallback'을 사용하는 경우는 드물고, 거의 없으니 잘 생각해봐야 한다.