개발/Javascript & Typescript

[React] 연산 최적화 하기

devhooney 2023. 2. 2. 21:32
728x90

1. useMemo()

리액트 Hook 에서 useEffect()와 비슷하게 불필요한 동작을 막기 위해 useMemo()라는 것을 사용한다.

 

예를 들면, Lifecycle이 시작될 때 배열을 서버로부터 받아와서 useState()로 데이터를 상태를 유지한다고 하면,

const [data, setData] = useState([]);

const getData = async () => {
    const response = await fetch("url")
            	.then((res) => res.json());
    setData(response);
}

useEffect(() => {
    getData();
}, [])

이런 코드를 작성할 수 있다.

 

여기서 Data 안에 있는 내용을 화면에 보여준다고 하면,

const getDataDetail = () => {
    const oddCount = data.filter((d) => d.id % 2 !== 0).length;
    const evenCount = data.length - dataOddCount;
    const oddRatio = (oddCount / data.length) * 100;
    
    return {oddCount, evenCount, oddRatio};
}

const {oddCount, evenCount, oddRatio} = getDataDetail();

return (
    <div>
    	<div>Data Count : {data.length}</div>
        <div>Odd Count: {oddCount}</div>
        <div>Even Count: {evenCount}</div>
        <div>Odd Ratio: {oddRatio}</div>
    </div>
)

이런 코드를 작성할 수 있는데,

이 상황에서 data의 내용을 변경을 한다고 가정하면,

아무 관련없는 getDataDetail() 함수가 두 번 실행 될 것이다.

이를 방지하고자 useMemo()를 사용한다.

 

useMemo()를 사용한 코드로 수정하면,

const getDataDetail = useMemo(() => {
    const oddCount = data.filter((d) => d.id % 2 !== 0).length;
    const evenCount = data.length - dataOddCount;
    const oddRatio = (oddCount / data.length) * 100;
    
    return {oddCount, evenCount, oddRatio};
}, [data.length]);

const {oddCount, evenCount, oddRatio} = getDataDetail;

return (
    <div>
    	<div>Data Count : {data.length}</div>
        <div>Odd Count: {oddCount}</div>
        <div>Even Count: {evenCount}</div>
        <div>Odd Ratio: {oddRatio}</div>
    </div>
)

이렇게 수정할 수 있다.

useMemo()를 이용하여 구조 분해 할당했을 경우 더이상 함수가 아닌 값이 전달되기 때문에 getDataDetail()이 아닌 getDataDetail를 사용해야 한다.

 

이렇게 하면 Data의 내용을 바꿔도 렌더링이 일어나지 않으며, [data.length] 때문에 데이터를 추가하거나 삭제했을 경우에만 렌더링이 진행된다.

 

 

2. memo

useMemo와 비슷하게 불필요한 리렌더링을 막아준다.

const Main = () => {
    const [count, setcount] = useState(1);  
    const [text, setText] = useState("");

    return (
        <div>
          <div>
            <CountView count={count} />
            <button onClick={() => setcount(count + 1)}>+</button>
          </div>
          <div>
            <TextView text={text} />
            <input value={text} onChange={(e) => setText(e.target.value)} />
          </div>
        </div>
    );
    
}

const CountView = ({count}) => {
    useEffect(() => {
    	console.log(`Count Update :: ${count}`);
    });
}

const TextView = ({text}) => {
    useEffect(() => {
    	console.log(`Text Update :: ${text}`);
    });
}

 

이렇게 두 개의 상태가 있다고 가정하면,

count를 클릭할 경우 text도 같이 리렌더링이 진행되고, 반대로 text input에 값을 입력해도 count도 같이 리렌더링이 된다.

이를 방지하려면,

 

const Main = () => {
    const [count, setcount] = useState(1);  
    const [text, setText] = useState("");

    return (
        <div>
          <div>
            <CountView count={count} />
            <button onClick={() => setcount(count + 1)}>+</button>
          </div>
          <div>
            <TextView text={text} />
            <input value={text} onChange={(e) => setText(e.target.value)} />
          </div>
        </div>
    );
    
}

const CountView = memo(({count}) => {
    useEffect(() => {
    	console.log(`Count Update :: ${count}`);
    });
});

const TextView = memo(({text}) => {
    useEffect(() => {
    	console.log(`Text Update :: ${text}`);
    });
})

 

이렇게 memo로 감싸주면 된다.

또 다른 방법으로는 useEffect()에 count와 text를 각각 넣어주어도 불필요한 리렌더링은 막을 수 있다.

 

const Main = () => {
    const [count, setcount] = useState(1);  
    const [text, setText] = useState("");

    return (
        <div>
          <div>
            <CountView count={count} />
            <button onClick={() => setcount(count + 1)}>+</button>
          </div>
          <div>
            <TextView text={text} />
            <input value={text} onChange={(e) => setText(e.target.value)} />
          </div>
        </div>
    );
    
}

const CountView = ({count}) => {
    useEffect(() => {
    	console.log(`Count Update :: ${count}`);
    }, [count]);
};

const TextView = ({text}) => {
    useEffect(() => {
    	console.log(`Text Update :: ${text}`);
    }, [text]);
}

 

memo보다는 useEffect를 사용할 일이 많으니 아래 방법으로 자주 활용할 것 같다.

 

memo의 다른 사용 예를 보기 전에

리액트 공식홈페이지에서 memo에 대한 설명을 보면

 

마지막 문단에 두 번째 인자로 별도의 비교함수를 제공하면 된다고 되어 있다.

위 코드와 비슷한 상황을 가정하고, 상태가 그대로일 경우에는 리렌더링이 변화하지 않는다.

하지만 그 상태가 객체일 경우 객체는 얕은 비교를 하기 때문에 상태가 같은 것 처럼 보이지만 주소로 비교를 하기 때문에 다르다고 인식한다.

 

const Main = () => {
	const [count, setCount] = useState(1);
  	const [obj, setObj] = useState({
    	count: 1,
	});
    
    return (
    	<div>
            <CounterA count={count} />
            <button onClick={() => setCount(count)}>A Button</button>
  
  			<CounterB obj={obj} />
            <button
              onClick={() =>
                setObj({
                  count: obj.count,
                })
              }
            >
              B Button
            </button>
        </div>
    );
}

const CounterA = memo(({ count }) => {
  useEffect(() => {
    console.log(`CounterA Update :: ${count}`);
  });
  return <div>{count}</div>;
});

const CounterB = memo({ obj }) => {
  useEffect(() => {
    console.log(`CounterB Update :: ${obj.count}`);
  });
  return <div>{obj.count}</div>;
};

 

이렇게 memo로 감싸져 있어도 CounterB의 렌더링은 상태가 바뀔 때 마다 계속 진행된다.

이를 막기 위해서

 

const areEqual = (prevProps, nextProps) => {
  return prevProps.obj.count === nextProps.obj.count;
};

const MemoizedCounterB = memo(CounterB, areEqual);

이런 코드를 추가하고 CounterB 대신 MemoizedCounterB를 사용하면

불필요한 렌더링을 막을 수 있다.

 

최종 코드

const Main = () => {
	const [count, setCount] = useState(1);
  	const [obj, setObj] = useState({
    	count: 1,
	});
    
    return (
    	<div>
            <CounterA count={count} />
            <button onClick={() => setCount(count)}>A Button</button>
  
  			<CounterB obj={obj} />
            <button
              onClick={() =>
                setObj({
                  count: obj.count,
                })
              }
            >
              B Button
            </button>
        </div>
    );
}

const CounterA = memo(({ count }) => {
  useEffect(() => {
    console.log(`CounterA Update :: ${count}`);
  });
  return <div>{count}</div>;
});

const CounterB = ({ obj }) => {
  useEffect(() => {
    console.log(`CounterB Update :: ${obj.count}`);
  });
  return <div>{obj.count}</div>;
};

const areEqual = (prevProps, nextProps) => {
  return prevProps.obj.count === nextProps.obj.count;
};

const MemoizedCounterB = memo(CounterB, areEqual);

 

 

3. useCallback()

useCallback()은 useMemo()가 값을 재사용하는 것과는 다르게 함수를 재사용할 때 사용한다,

 

const memoizedCallback = useCallback(
  () => {
    doSomething(a, b);
  },
  [a, b],
);

 

useEffect나 useMemo와 비슷한 형태인데, []안에 있는 값이 변할 때만 doSomething() 함수가 실행된다.

useEffect 처럼 []안을 비워두면 처음에만 실행되고, 더 이상 실행되지 않는데 문제가 발생할 수 있다.

 

  const [data, setData] = useState([]);

  const getData = async () => {
    const res = await fetch(
      "url"
    ).then((res) => res.json());

    setData(res);
  };

  useEffect(() => {
    getData();
  }, []);


  const onCreate = useCallback((author, content, emotion) => {
    const created_date = new Date().getTime();
    const newItem = {
      author,
      content,
      emotion,
      created_date,
      id: dataId.current,
    };
    dataId.current += 1;
    setData((data) => [newItem, ...data]);
  }, []);

 

예제 코드인데, 여기서 onCreate 함수가 실행되고, data에 배열에 요소를 추가해서 렌더링 해줘야하는데, 처음에 데이터가 없었기 때문에 빈 배열에 새로 추가한 요소만 추가된다. 이를 방지하기 위해 함수형 업데이트를 활용하면 된다.

 

  const onCreate = useCallback((author, content, emotion) => {
    const created_date = new Date().getTime();
    const newItem = {
      author,
      content,
      emotion,
      created_date,
      id: dataId.current,
    };
    dataId.current += 1;
    setData((data) => [newItem, ...data]);
  }, []);

 

setData에 콜백 함수를 전달해줘서 항상 최신의 데이터를 유지할 수 있게 된다.

[]은 비워야 하고, 화살표 함수의 인자를 통해 데이터를 참고하고, 이전과 같은 문제를 방지할 수 있다.

 

 

 

- 참고

https://www.inflearn.com/course/%ED%95%9C%EC%9E%85-%EB%A6%AC%EC%95%A1%ED%8A%B8/unit/103532?tab=curriculum

728x90

'개발 > Javascript & Typescript' 카테고리의 다른 글

[RN] React Native CLI vs Expo CLI  (81) 2023.11.05
[React] 상태 변화 로직 분리하기  (0) 2023.02.04
[React] Lifecycle 제어하기  (0) 2023.02.01
[React] DOM 선택하기  (1) 2023.01.31
[React] state 변경 공통화 하기  (0) 2023.01.30