이전 시간에 React의 동작 원리에 대해서 잠깐 다뤘었다.
component를 재렌더링하는 과정에서 불필요하게 자식 component까지 포함시킨다고 이야기했었다.
작은 단위의 project라면 큰 상관이 없겠지만 더 큰 어플리케이션이라면 좀 더 최적화가 필요할 수 있다.
따라서 개발자는 특정한 상황일 경우에만 재실행하도록 지시할 수 있어야 할 것이다.
이번 시간에는 그 방법에 대해서 알아보자.
React.memo()
이런 행동 방식은 사람이 생각하고 원하는 행동 방식에 가까울 수 있다.
그 사용 방법은 생각보다 간단하다. 우선 확인이 필요한 component을 지정한 후 React.memo()
로 감싸주면 끝이다.
import React from 'react' ;
...
export default React.memo(DemoComponent) ;
이러한 방법은 함수형 component에서만 사용가능하며 class 기반의 component에서는 작동하지 않는다.
React.memo()
는 parameter로 들어간 어떤 props가 입력되는지 확인한다.
입력된 모든 신규 값을 확인 후 기존의 props와 비교하도록 React에 전달한다. 그 값이 바뀐 경우, component를 재평가하게 된다.
그리고 부모 component가 변경되었지만 자식 component의 값이 바뀌지 않았다면 component 실행을 건너뛴다.
불필요한 재렌더링을 피하기 위해서 이렇게 최적화를 이뤄낼 수 있다.
여기서 새로운 질문이 하나 생기는 데 "이렇게 최적화가 가능하다면 왜 모든 component에 기본으로 적용하지 않는가?"이다.
그 이유는 이러한 최적화에는 비용이 따르기 때문이다.
기존의 props와 새로운 값을 비교하는 과정에서 React는 기존의 props를 저장할 공간과 비교하는 작업이 필요하며,
각각의 작업에 필요한 개별적인 성능 비용이 요구된다. 여기서 성능 효율은 어떤 component를 최적화하느냐에 따라 달라지게 된다.
이는 component를 재평가하는 데 필요한 성능 비용과 props를 비교하는 성능 비용을 맞바꾸는 것이다.
props의 개수가 높으면 최적화하는데 비용이 증가할 것이다.
또한, component의 복잡도와 자식 component의 숫자에 따라 재렌더링의 비용이 증가할 것이다.
물론, 자식 component가 많아서 component tree가 매우 크다면 React.memo()
는 매우 유용하게 사용될 것이다.
이와 반대로 부모 component를 매번 재평가할 때마다 component에 변화가 있거나 props의 값이 변하는 경우라면 React.memo()
는 큰 의미를 갖지 못한다.
왜냐하면 component의 재렌더링이 어떻게든 필요하기 때문이다.
Problem Occurred
그런데 문제가 있다. 사용자 지정 Button component에 React.memo()
를 사용할 때에는 계속 실행된다는 것이다.
이는 props의 값이 변한다는 것을 의미하는데 props는 onClick이 전부이기 때문이다. (사실 props.children까지 포함하면 2개)
값이 변한 것도 아니며, 텍스트도 같고 실행되는 함수도 같다. 그럼에도 왜 실행이 마음대로 되지 않을까?
이는 사용자 지정 component가 아닌 App component에서 실행되서 그렇다. App component는 JavaScript 함수이기에
React에 의해 호출된 함수는 여전히 일반 함수처럼 실행되는데 이는 App component에 있는 모든 함수가 다시 실행된다는 것이다.
이것은 매우 중요한 것으로 버튼에 전달되는 onClick 함수도 매번 재생성되는 것을 의미한다.
App 함수의 모든 렌더링에서 새로운 함수로 재사용되지 않는다. 매번 다시 만드는 새로운 함수이기 때문에 이전과 같은 함수가 아닌 것이다.
다만, boolean, 숫자, 문자열과 같은 JavaScript 원시 값들을 비교할 때에는 잘 수행하는 것을 볼 수 있다.
// show는 boolean
props.show === props.previous.show
실제로 내부적으로 이런 방법으로 쓰이지는 않지만 대게 이런 식이라고 생각하면 된다.
기술적으로 두 개의 boolean 값은 다른 값이지만 원시 값이라면 이런 비교가 가능하다.
원시값이 아닌 함수나 객체의 경우 저장되어 있는 메모리의 값을 비교하게 되기 때문에 원하는 것처럼 비교할 수 없게된다.
객체의 내부 값이 같더라도 같은 메모리가 아니라면 다른 객체로 간주하게 된다.
물론 이런 문제를 해결할 수 없는 것은 아니다.
useCallback()
이는 객체를 생성하고 저장하는 방식을 조금 변경해주면 가능하다.
useCallback()
은 기본적으로 component 실행 전반에 걸쳐 함수를 저장할 수 있게 하는 hook이다.
React에 함수를 저장할 것이고 매번 실행될 때마다 이 함수를 재생성할 필요없다는 것을 알릴 수 있다.
이렇게 되면, 함수 객체가 동일한 메모리에 위치하게 됨으로 React.memo()
를 통한 비교 작업이 가능해진다.
선택된 함수를 React의 내부 저장 공간에 저장하여 component가 실행될 때마다 이를 재사용할 수 있게 된다.
그 사용법은 매우 간단한데 저장하려는 함수를 감싸기만 하면 된다.
import { useCallback } from 'react' ;
...
const toggleHandler = useCallback(() => {
fn
}, [dependency]) ;
함수를 첫 번째 parameter로 전달하게되면 useCallback()
은 저장된 함수를 return
해준다.
해당 component가 다시 실행되더라도 useCallback()
으로 저장된 함수를 찾아서 재사용하게 된다.
따라서, 절대 변경되지 말아야하는 함수가 있다면 useCallback()
을 사용해서 함수를 저장하면 된다.
useCallback()
은 useEffect()
와 마찬가지로 의존성 배열인 두 번째 parameter가 존재한다.
의존성 배열에 들어간 특정 변수의 값이 변하면 useCallback()
내부의 함수도 재렌더링된다.
하지만 이전과 동일한 함수 객체인 것을 보장하기 위해서 사용할 때에는 그럴 필요가 없다.
내용이 없는 의존성은 React에게 이 함수는 절대 변경되지 않는 것이라고 말하는 것과 같다.
때문에 component가 다시 렌더링되어도 항상 같은 함수 객체를 사용되게끔 해준다.
useMemo()
useCallback()
과 마찬가지로 2개의 parameter가 필요하며 첫 번째 parameter에는 함수가 들어가야한다.
하지만 함수를 기억하게하는 것은 아니며, 이 함수를 통해 저장하고 싶은 값을 return해 줄 것이다.
두 번째 parameter는 의존성 배열로 지정된 값에 변경 사항이 생길 때마다 업데이트된다.
import { useMemo } from 'react';
...
const sortedList = useMemo(() => {
fn
}, [dependencies]);
...
보통 useMemo()
는 useCallback()
을 사용하는 것보다 훨씬 덜 자주 사용된다.
이는 함수 형태가 훨씬 더 유용하고 데이터를 기억하는 것보다 더 자주 필요하기 때문이다.
물론, 데이터 재계산 같은 성능 집약적 작업 때문에 데이터를 저장해야하는 경우도 필요하지만 그 이외의 경우라면 별로 사용되지 않는다.
useCallback()
과 하는 일이 조금 다른데, 주로 복잡한 계산을 하거나 큰 리스트를 확인 해야할 때 사용되면 좋다.
What is the difference between useCallback() and useMemo()?
그래서 useCallback()
과 useMemo()
의 차이가 뭐라고??
useCallback()
의 경우, '함수'를 return
하는 반면에useMemo()
의 경우, 함수를 받아와 그 함수를 실행하여 나온 '결과 값'을 return
한다.
주된 사용 목적
useCallback()
- 함수가 재생성되는 것을 방지함.useMemo()
- 함수의 연산량이 많이 이전 결과값을 다시 사용하기 위함.
이번 시간에는 React.memo()
와 useCallback()
와 useMemo()
에 대해 알아봤다.
화면을 렌더링하는 데 필요한 성능 비용은 당연하겠지만 앱 크기에 따라 달라진다.
매우 작은 앱, 매우 작은 component tree를 가진 경우에는 이런 방식의 최적화는 별로 의미가 없을 것이다.
하지만 큰 규모의 앱이라면 이와 같이 불필요한 재평가를 넘어갈 수 있다면 큰 가치를 지닐 것이다.
또한, 모든 component를 React.memo()
로 감쌀수는 없기에 재렌더링 빈도수가 낮은 특정 component를 택해서 사용하면 된다.
React.memo()
로 안되는 함수나 객체 같은 경우에는 useCallback()
나 useMemo()
를 활용할 수 있다.