React memo, useMemo, useCallback

떡ol·2023년 8월 25일
post-thumbnail

memo란

React는 특정 값이 변경되면 화면을 다시 렌더링 하여 변경된 화면을 그려줍니다. 그런데 이 부분이 때로는 원치 않는 랜더링으로 성능의 저하를 가져올 수 있습니다.
예를 들어 두 개의 input 창이 있을 때 React는 두 개의 input 중 한 개의 input 창에서 값의 변화가 일어나도 랜더링이 발생됩니다. 이렇게 state 값이 변할 때 컴포넌트의 리렌더링을 방지하기 위해 값이 변경되는 것에만 메모이제이션값을 반환하는 코드가 memo(React.memo, useMemo)입니다.

useMemo

위에서 설명한것을 예제로 실습해볼려고 합니다. input 테그를 두개를 만들어 사용해봅니다.

const OptimizerTest = () => {
    const [val1, setVal1] = useState(0)
    const [val2, setVal2] = useState(0)


    const handleAdd1 = () => {
        setVal1(prev => prev + 1)
    }

    const handleAdd2 = () => {
        setVal2(prev => prev + 1)
    }

    const computedVal = val1 * val1
    console.log('computedValue', computedVal)

    return (
        <>
            <div>val1: {val1}</div>
            <div>val2: {val2}</div>
            <div>val3: {computedVal}</div>
            <br />
            <button type="button" onClick={handleAdd1}>
                Add val1
            </button>
            <button type="button" onClick={handleAdd2}>
                Add val2
            </button>
        </>
    )
}

val1 버튼을 누르나, val2 버튼을 누르나 console의 진행상황은 계속 적히게 됩니다.

하지만 val2버튼은 computedVal 객체에는 아무런 영향을 주지 않습니다. React가 Component의 변화를 감지하고 화면을 전체 re-reder하기 때문에 생기는 문제 입니다.

아래 코드처럼 변화를 줍시다.

    /*
    const computedVal = val1 * val1
    console.log('computedValue', computedVal)
    */

	// useMemo는 hook이므로 함수형 컴포넌트에서만 사용기 가능하기 때문에 아래와 같이 변화를 줍니다.
    const computedVal = useMemo(() =>
    {
        console.log('computedValue', val1 * val1);
        return val1 * val1;
    },[val1]);

다음과 같이 val2를 눌러도 console은 반응을 안하는것을 알 수 있습니다. val1의 값에 변화가 생긴게 아니라 computeVal의 랜더를 진행하지 않은 것입니다.

React.memo

React.memo라고 함수형뿐만 아니라 전체 컴포넌트를 memo할때사용하는 기능입니다.
아래 예시를 보시죠.

  1. OptimizerChild.js 라는 파일을 새로 만듭니다.
const OptimizerChild = () => {
    const [val3, setVal3] = useState(0)
    const [val4, setVal4] = useState(0)

    const handleAdd1 = () => {
        setVal3(prev => prev + 1)
    }

    const handleAdd2 = () => {
        setVal4(prev => prev + 1)
    }

    const computedVal2 = val3 * val3
    console.log('Child', computedVal2)

    // const computedVal = useMemo(() =>
    // {
    //     console.log('computedValue', val1 * val1);
    //     return val1 * val1;
    // },[val1]);

    return (
        <>
            <div>ChildVal1: {val3}</div>
            <div>ChildVal2: {val4}</div>
            <div>ChildVal3: {computedVal2}</div>
            <br />
            <button type="button" onClick={handleAdd1}>
                Add val1
            </button>
            <button type="button" onClick={handleAdd2}>
                Add val2
            </button>
        </>
    );
}

export default OptimizerChild;
  1. 위에서 만들어봤던 useMemo() 예제 JSX return 부분에 아래와 같이 Compoent를 추가합니다.
    return (
        <>
            <div>val1: {val1}</div>
            <div>val2: {val2}</div>
            <div>val3: {computedVal}</div>
            <br />
            <button type="button" onClick={handleAdd1}>
                Add val1
            </button>
            <button type="button" onClick={handleAdd2}>
                Add val2
            </button>
            <OptimizerChild /> // 요기 추가하세요.
        </>
    )

그럼 결과는 다음과 같습니다.

빨간색쪽이 자식이니 부모쪽에는 반응이 없습니다만, 부모쪽 버튼은 자식까지 반응해 버리네요. 결과는 4로 변함없는데 말이죠. 이럴때 React.memo를 사용하시면 됩니다.

	export default React.memo(OptimizerChild);

그럼 다음과 같이 부모버튼을 눌러도 자식의 컴포넌트에 변화가 없다면 re-render하지 않습니다.

UseCallback

위에서 설명하였던 memo기능들은 의존성을 참조하는 객체의 동일성을 갖고있다는 가정에서 기능이 작동합니다. 단, 배열이나 java Map, javascript object를 사용해보신 분들은 아시겠지만, 그 안에 값이 동일하더라도 동일성을 갖지는 않습니다.

let A = {obj : 1};
let B = {obj : 1};
A === B //false

그래서 useCallback을 이용해 함수를 특정 조건이 변경되지 않는 이상 재생성하지 못하게 제한하여 함수 동등성을 보장할 수 있습니다.

function Profile({ id }) {
  const [data, setData] = useState(null)

  const fetchData = () =>
    fetch(`https://test-api.com/data/${id}`)
      .then(response => response.json())
      .then(({ data }) => data)

  useEffect(() => {
    fetchData().then(data => setData(data))
  }, [fetchData])

언뜻 보면 페이지가 마운트 되었을 때 데이터 가져오는 fetchData 함수를 호출해 데이터를 잘 가져오는 듯 보이지만, fetchData는 함수이기 때문에 id 값에 관계없이 컴포넌트가 렌더링 될 때마다 새로운 참조값으로 변경이 됩니다. 함수를 참조하는 useEffect가 실행되어 re-render되고 무한루프에 진입하게 됩니다.

이 문제를 해결하기 위해서 다음고 같이 useCallback을 사용하여 id를 참조하게 만들면 됩니다.

  const fetchData = useCallback(
    () =>
      fetch(`https://test-api.com/data/${id}`)
        .then(response => response.json())
        .then(({ data }) => data),
    [id],
  )

만약 callback을 참조하는 객체가 setter 되는 객체와 동일할 경우 배열을 비워놓고 함수형식으로 setter를 제작하셔도 알아서 작동합니다.

	//해당 컴포넌트는...
    const onCreate = useCallback((author, content, emotion) => {
        const create_dt = new Date().getTime();
        const newItem = {
            author,
            content,
            emotion,
            create_dt,
            id: dataId.current
        };
        dataId.current += 1;
        setData([newItem, ...data]);
    },[data]);

	// 아래와 같이 수정가능합니다.
    const onCreate = useCallback((author, content, emotion) => {
        const create_dt = new Date().getTime();
        const newItem = {
            author,
            content,
            emotion,
            create_dt,
            id: dataId.current
        };
        dataId.current += 1;
        setData((data) => {[newItem, ...data]}); // 변경된 지점
    },[]);

useCallback 사용시 setter에 함수식을 넣어야함

연습을 해보다가 발견했습니다. 다음과 같이 작동이 안됩니다.

	//작동 안됨
    const newDiaryList = data.filter( (it) => it.id !== targetId );
    setData(newDiaryList);
    
    //작동 됨
    setData(data.filter( (it) => it.id !== targetId ));

번외, useEffect 와 useMemo

어찌보면 useEffect로 값을 참조하여 useMemo와 같은 기능을 구사하는게 가능합니다.
단, useEffect는 값의 변화를 보고 함수를 실행한다는 것, useMemo는 값이 바뀐 유무를 보고 실행하지 않는다는 것을 명심하며, useMemo 는 랜더링 과정 중에, useEffect 는 랜더링이 끝나고 나서 발동된다고 합니다.

이점을 이해하시고 더 효과적인 코드를 작성하시면 될 겁니다.



참고자료들___
(참고) Web: 최적화와 React.memo, useMemo 알아보기
(참고) useMemo와 useCallback는 왜, 언제 사용할까?
(참고) React의 컴포넌트(Component) - 함수형, 클래스형 컴포넌트
(참고) React.memo와 useMemo 차이점
(참고) useMemo 와 useEffect 비교

profile
하이

0개의 댓글