React

[한입리액트] 최적화 : useMemo, React.memo, useCallback, useReducer, context API

hyriver(강화영) 2023. 10. 18. 19:05

useMemo

함수 컴포넌트에서 발생하는 연산을 최적화한다. 렌더링할 때 특정 이 바뀌었을 때만 연산을 실행하고, 그 값이 바뀌지 않는다면 이전에 연산했던 결과를 그대로 사용.

 

1. 연산 최적화 하고자하는 함수에 useMemo를 붙여 콜백함수로 만들어준다.

  const getDiaryAnalysis = useMemo(
    () => {
      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] //data.length가 바뀔때만 앞의 콜백함수를 수행하고, 그렇지 않으면 수행했던 리턴값을 계속 사용
  );

useMemo는 useEffect와 유사하다.

차이점은 useEffect는 html이 다 실행되고나서 실행되는 것이고, useMemo는 렌더링될 때 실행된다는 것이다.

실행시점의 차이가 있다.

 

2. 함수사용

함수를 사용할 때는 ()를 붙이지 않는다. useMemo를 사용하면 콜백함수의 값이 getDiaryAnalysis에 저장된다. 그래서

getDiaryAnalysis는 으로 사용한다.

const { goodCount, badCount, goodRatio } = getDiaryAnalysis;

 

React.memo

부모 컴포넌트가 리렌더링 되면 하위 컴포넌트는 자동으로 리렌더링 된다. 하지만 그 자식 컴포넌트에 변동사항이 없다면 연산낭비이다. 그래서 컴포넌트의 props가 바뀌지 않는다면 리렌더링 하지 않도록 한다.

const TextView = React.memo(({ text }) => {	//props인 text가 변할때만 TextView 컴포넌트가 렌더링 된다.
  useEffect(() => {
    console.log(`update :: Text : ${text}`);
  });
  return <div>{text}</div>;
});

 

* 이 props가 객체라면?

자바스크립트에서는 객체를 얕은비교(주소비교)를 하기 때문에 객체인 값이 변하지 않더라도 다른 값으로 인식한다.

이것을 막기 위해 areEqual이라는 함수를 사용하는데, areEqual은 React.memo의 비교함수로 작용하게 된다.

//객체를 props로 가지고 있는 컴포넌트 CounterB
const CounterB = ({ obj }) => {
  return <div>{obj.count}</div>;
};

//areEqual함수를 사용하여 이전과 이후의 obj.count를 비교하여 같다면 true, 같지않다면 false 반환
const areEqual = (preProps, nextProps) => {
  return preProps.obj.count === nextProps.obj.count;
};

//CounterB는 areEqual의 판단에 따라 리렌더링을 할지 말지 결정하는 메모화된 컴포넌트가 된다.
const MemoizedCounterB = React.memo(CounterB, areEqual);

const OptimizeTest = () => {

  const [obj, setobj] = useState({
    count: 1,
  });
  
return (
 <MemoizedCounterB obj={obj} />
 )
 }

 

useCallBack

컴포넌트가 리렌더링 될 때마다 함수도 같이 재생성된다. useCallback을 사용하면  제일 처음 마운트될 때 함수를 한번 생성하고 이것을 재사용하도록 할 수 있다.

const onCreate = useCallback((author, content, emotion) => {
    setData((data) => [newItem, ...data]);
  }, []);		//[] : 마운트될 때만 함수 생성되도록

setData안에 최신 데이터 인자를 갖는 콜백함수를 넣어줘야 최신의 데이터를 인자를 통해 참조할 수 있다.

 

useReducer

useReducer는 기존에 컴포넌트 내에서 useState로 상태관리를 하던 것을 컴포넌트 밖으로 분리하여 사용할 수 있게 한다. 이렇게 상태변화로직(상태와 상태변화 처리함수)을 분리하여 컴포넌트를 가볍게 사용할 수 있다.

 

1. reduer 함수 만들기

이 함수는 현재상태와 액션 객체를 파라미터로 받는다.

reducer에서 반환하는 값이 현재 상태값이 된다.

* 액션객체는 상태를 업데이트하는데 필요한 정보를 가지고 있으며 type 속성을 가지는 객체이다.

const reducer = (state, action) => {
  switch (action.type) {		//action의 type속성에 따라 리턴을 다르게
    case "INIT": {
      return action.data;
    }
    case "CREATE": {	//기존 컴포넌트 내부의 상태변화 함수에서 작성했던 것을 reducer함수 내부에서 수행
      const created_date = new Date().getTime();
      const newItem = {
        ...action.data,
        created_date,
      };
      return [newItem, ...state];
    }
    default:
      return state;
  }
};

 

2. useReducer 사용하기

이 useReducer에서 data는 우리가 사용할 상태를 선언하는 것이고, dispatch는 액션을 발생시키는 함수이다.

useReducer의 첫번째 파라미터는 앞에서 만들었던 reducer 함수이고, 두번째 파라미터는 초기 상태이다.

* dispatch가 계속 이해가 안되었는데, 그냥 상태값을 바꾸고 싶을 때는 dispatch라는 함수를 사용하고 그 파라미터를 액션으로 사용한다는 의미이다.

  const [data, dispatch] = useReducer(reducer, []);
  //초기값이 빈 배열인 data 상태

 

3. dispatch 사용하기

기존에 사용했던 상태변경함수 대신 dispatch를 사용하여 상태를 변경한다.

type 속성은 reducer 함수에서 수행할 타입을 지정해주는 것이고, data 속성에는 그 수행에 필요한 정보들을 넣는다.

* dispatch를 호출하면 알아서 현재의 state를 자동으로 reducer 함수가 참조한다.

  const getData = async () => {
    //setData(initData);	기존 상태 변화 함수에서 dispatch로 변경
    dispatch({ type: "INIT", data: initData });
  };
const onCreate = useCallback((author, content, emotion) => {
//    const created_data = new Date().getTime();	기존 함수에서 사용했던 것을 reducer에서 작성
//    const newItem = {
//      author,
//      content,
//      emotion,
//      created_data,
//      id: dataId.current, //0
//    };
//    dataId.current += 1;
//    setData((data) => [newItem, ...data]);
    dispatch({
      type: "CREATE",
      data: { author, content, emotion, id: dataId.current },	//data에는 필요한 인자들만 넘겨줌
    });
    dataId.current += 1;
  }, []);

 

 

context API

context API를 사용하여 상태를 전역으로 사용하여 불필요한 propsdrilling을 제거할 수 있다.

 

1. context 만들기

최상위 컴포넌트에서 context를 만든다.

export const DiaryStateContext = React.createContext();		//export를 붙여줘야 외부에서 사용 가능
export const DiaryDispatchContext = React.createContext();

 

2. context 사용하기

context provider를 사용하여 전역으로 데이터를 보내주는데 value에는 전달할 데이터를 넣는다.

* 상태와 함수는 context를 다르게 사용해야한다. Provider도 컴포넌트 이므로 data의 값이 변경되면 리렌더링 되므로 최적화했던것이 풀어진다 -> context 중첩으로 사용하기

    <DiaryStateContext.Provider value={data}>
      <DiaryDispatchContext.Provider value={memoizedDispatches}>
        <div className="App">
	// 내용작성 
        </div>
      </DiaryDispatchContext.Provider>
    </DiaryStateContext.Provider>

 

* 함수를 context로 관리하여 Provider로 보낼 때는 함수들이 불필요하게 재생성되지 않도록 useMemo를 사용하여 함수들을 묶어서 보낸다.

  const memoizedDispatches = useMemo(() => {
    return { onCreate, onRemove, onEdit };
  }, []);

 

3. context에서 상태 사용하기

상태가 필요한 컴포넌트에서 context를 사용하여 상태를 꺼내온다.

const diaryList = useContext(DiaryStateContext);