DEV/WebProgramming

[React] 최적화

9thxg 2022. 4. 25. 15:34

인프런, 한입 크기로 잘라 먹는 리액트(React.js) : 기초부터 실전까지의 내용 중 최적화 부분에 대해서 수강하고 내용을 정리한다.

 

목차
  1. useMemo( ) - 연산 재사용하기
  2. React.memo( ) - 컴포넌트 재사용하기
  3. useCallback( ) - 함수 재상용하기

 


useMemo( ) - 연산 재사용하기

 

컴포넌트들을 작성하다 보면 한 번 연산한 이후 계산할 필요가 없을 때가 있다. 이때 연산 결과를 재사용하므로써 연산을 줄일 수 있다. 이를 Memoization이라 부르고 이는 프로그래밍 기법에 가깝다.

즉, Memoization은 계산 해둔 연산의 결과 값을 기억해 두었다가 동일한 계산을 시키면, 새로 연산하지 않고 결과 값을 반환시켜 연산을 줄이는 것이다.

 

simple-dairy 프로젝트에서는 전체 일기 데이터에 대해 분석을 하도록 만들었다. 이때 일기의 데이터 수정시 기분에 관한 값은 변하지않고 고정된다. 따라서 처음 일기 리스트를 불러왔을 때의 결과 값을 기억해 두었다가 일기의 추가나 삭제를 하지 않으면 동일한 결과값을 가지기 때문에 계속해서 같은 값을 반환해주기만 하면 된다.

 

이때 React에서 사용할 수 있는 hook은 useMemo()이다. useMemo() 인자로 callback함수와 의존성 배열이 들어가게 된다. 의존성 배열 내부의 데이터가 변경되지 않는다면 계속해서 callback함수의 결과값을 기억해 두었다가 반환하게 된다.

 

useMemo()를 사용하게 되면 반환되는 값이 변수 값이기 때문에 함수형으로 사용하고 있던 내용들을 변수를 사용하는 것 처럼 변경해주어야 한다.

  const getDiaryAnalysis = useMemo(
        () => {
        console.log("일기 분석 시작");

        const goodCount = data.filter((it)=> it.emotion >=3 ).length;
        const badCount = data.length - goodCount;
        const goodRatio = (goodCount/ data.length) * 100;
        return {goodCount, badCount, goodRatio};
      }, [data.length] //의존성 배열
  );

 

React.memo( ) - 컴포넌트 재사용하기

 

React에서 부모 컴포넌트가 리렌더 된다면 모든 자식 컴포넌트는 자동으로 리렌더 되게 된다. 부모 컴포넌트의 state의 변화에 모든 자식 컴포넌트가 반응한다면 연산 낭비가 많이 생길 것이다. 이를 자식 컴포넌트에 업데이트 조건을 걸어준다면 많은 연산 낭비를 줄일 수 있을 것이다.

 

React.memo를 통해 컴포넌트에 업데이트 조건을 걸어 줄 수 있다. 부모 컴포넌트에서 동일한 props를 받아 동일한 결과를 렌더링한다면, React.memo가 기억해둔 결과값을 반환하게 된다. 단, 부모 컴포넌트의 props에 대해 Memoization한 것이기 때문에 자식 컴포넌트 본인의 state가 변경되어 리렌더 되는 경우 등은 리렌더가 정상적으로 작동한다.

 

React.memo를 사용할 때 props가 객체라면 문제가 발생할 수 있다. 이는 React가 객체를 비교할 때 얕은 비교를 하기 때문이다. 얕은 비교는 객체의 값을 비교하는 것이 아닌 객체가 저장된 저장 주소를 기준으로 비교하게 된다. 따라서 같은 값을 가지고 있는 객체일지라도 주소가 다르기 때문에 다른 값으로 판단하게 된다. 이때는 React.memo의 두 번째 인자로 들어가는 areEqual 함수를 정의하여 입력하면 해결이 가능하다.

 

areEqual 함수는 인자로 prevProps, nextProps를 받고 이를 비교하여 결과 값을 true로 지정하면 같은 값이라 판단하여 리렌더를 하지 않게 되고 false로 지정한다면 리렌더하게 된다.

import React, { useState, useEffect } from "react"

const CounterA = React.memo(({count}) => {

    useEffect(() => {
        console.log(`CounterA Update - count: ${count}`)
    })

    return <div>{count}</div>
})

const CounterB = ({obj}) => {

    useEffect(() => {
        console.log(`CounterB Update - count: ${obj.count}`)
    })

    return <div>{obj.count}</div>
}

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

const MemoizedCounterB = React.memo(CounterB, areEqual)

export default function OptimizeTest() {

    const [count, setCount] = useState(1);
    const [obj, setObj] = useState({count: 1,})

     return(
     <div style={{ padding: 50 }}>
         <div>
            <h2>Counter A</h2>
            <CounterA count={count} />
            <button onClick={() => setCount(count)}>A button</button>
         </div>
         <div>
            <h2>Counter B</h2>
            <MemoizedCounterB obj={obj}/>
            <button onClick={() => setObj({count: obj.count})}>B button</button>
         </div>
     </div>);
}

 

useCallback( ) - 함수 재사용하기

 

자식 컴포넌트가 부모 컴포넌트의 함수를 props로 받고 있을 때 부모 컴포넌트에서 state 변경을 통해 리렌더하게 되면 자식 컴포넌트가 부모 컴포넌트의 함수를 props로 받고 있기 때문에 리렌더된다.

 

이때 React.memo를 통해 어느정도 해결 가능하지만 부모 컴포넌트에서 API를 호출을 통해 state값을 변경하게 되면 자식 컴포넌트는 총 2번의 렌더를 거치게 된다.

 

자식 컴포넌트를 한 번의 렌더를 하기 위해서는 useCallback() hook을 통해 부모 컴포넌트의 함수를 최적화 해주어야 한다. useCallback() 함수는 callback함수와 의존성배열을 입력받고 의존성 배열 내부의 데이터 값이 변경되지 않으면 callback함수를 계속 재사용한다.

 

simple-diary에서 useCallback()함수를 적용하면 일기를 등록하고나면 리스트가 없어져버리는 경우가 생긴다. 이는 부모 컴포넌트에서 Mount 될 때 렌더되고 이후 렌더되지 않아 API 호출한 state가 최신화 되지 않아 생기는 문제이다. callback함수 내부에 부모 컴포넌트의 state를 변경하는 setState함수 안에 값을 전개구문이나 변수를 사용하는 것이 아닌 함수식으로 보내어 해결이 가능하다. 함수식을 통해 setState함수를 최신화 하는 것을 함수형 업데이트라 한다.

 

리스트의 아이템을 나타낼 때도 사용이 가능한데 이때 아이템 컴포넌트에 React.memo()를 적용해주고 props로 받고있는 함수들을 부모 컴포넌트에서 useCallback() hook을 통해 최적화 해주고 이때도 state를 함수형으로 업데이트 해주면 된다.

  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]);
  }, []);

  const onRemove = useCallback((targetId) => {
    console.log(`${targetId}가 삭제되었습니다.`);
    setData(data => data.filter((it) => it.id !== targetId));
  }, []);

  const onEdit = useCallback((targetId, newContent) => {
    setData(
      data => data.map((it) => it.id === targetId ? {...it, content:newContent} : it)
    );
  }, [])

 

마무리

 

해당 내용을 수강한 것은 2일이 걸렸지만 학교의 과제들과 창의설계프로젝트 진행에 의해 정리하는 부분은 많이 늦어졌다. 아직까지 본인만의 프로젝트를 진행하고 있지않아서 크게 와닿지 않은 내용이었지만 이후 큰 프로젝트나 많은 데이터를 받게 될 때 충분히 고려해야하는 부분이라고 생각한다. 빠른 시일내에 개인 프로젝트를 진행하여 이번에 정리한 최적화 부분을 적용해볼 것이다.