티스토리 뷰

728x90
반응형
React 최적화

React 최적화

React 최적화

1. UseMemo

Memoization

이미 계산 해본 연산 결과를 기억 해 두었다가 동일한 연산을 시키면, 다시 연산하지 않고, 기억해 두었던 데이터를 반환 시키게 하는 방법



useMemo

React 에서 컴포넌트가 rendering 될 때 마다 함수가 실행되면 불필요한 연산이 매번 발생하게 된다.

예를 들어 다음과 같은 코드가 있다고 할때,

export default function App() {
  const data = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];

  const getAnalysis = () => {
    console.log("분석 시작");
    const highCount = data.filter((it) => it >= 7).length;
    const lowCount = data.length - highCount;
    const highRatio = (highCount / data.length) * 100;

    return { highCount, lowCount, highRatio };
  };

  const { highCount, lowCount, highRatio } = getAnalysis();

  return (
    //
    <div className="App">
      <div>전체 데이터 개수 : {data.length}</div>
      <div>높은 점수 개수 : {highCount}</div>
      <div>높은 점수 개수 : {lowCount}</div>
      <div>높은 점수 개수 : {highRatio}</div>
    </div>
  );
}

getAnalysis 함수는 data 의 값이 별경 될 때 마다 호출 되기 때문에 비효율적이다.

그래서 useMemo 를 사용해서 data 의 길이가 변경될 때만 getAnalysis 함수를 호출 하도록 useMemo 라는 함수를 사용한다.

import { useMemo } from "react";

export default function App() {
  const data = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];

  const getAnalysis = useMemo(() => {
    console.log("분석 시작");
    const highCount = data.filter((it) => it >= 7).length;
    const lowCount = data.length - highCount;
    const highRatio = (highCount / data.length) * 100;

    return { highCount, lowCount, highRatio };
  }, [data.length]);

  const { highCount, lowCount, highRatio } = getAnalysis;

  return (
    //
    <div className="App">
      <div>전체 데이터 개수 : {data.length}</div>
      <div>높은 점수 개수 : {highCount}</div>
      <div>높은 점수 개수 : {lowCount}</div>
      <div>높은 점수 개수 : {highRatio}</div>
    </div>
  );
}

최적화 하고 싶은 함수를 useMemo 함수로 감싸게 되면, 해당 함수는 두번 째 매개변수로 들어간 dependency array 가 변경이 되었을 때만 호출하게 되고 그 함수의 반환 값을 반환 해준다.

이처럼, useMemo 를 사용하게 되면 re-rendering 이 발생할 경우, 특정 변수가 변할 때에만 useMemo 에 등록한 함수가 실행되도록 처리하면 함수를 값처럼 사용해서 연산 최적화를 할 수 있다.

2. React.memo

비효율적인 Component Rendering


setCount(10);

setText("Hello");

App 컴포넌트 의 count state를 setCount로 변경하게 되면, App 컴포넌트가 랜더링 되고 자식 컴포넌트 들인 CountView 컴포넌트와 TextView 컴포넌트가 rendering된다.

count state가 바뀔 때 TextView 컴포넌트가 쓸데 없이 rendering 되는 낭비가 발생하고

,text state가 바뀔 때, CountView 컴포넌트가 rendering 되는 낭비가 발생한다.

비효율적인 rendering을 막기 위해 count state만 변경됬을 때는 CountView 컴포넌트만 , text state만 변경됬을 때는 TextView 컴포넌트만 rendering되어야 한다.

이러한 문제를 해결하기 위해서는 함수형 컴포넌트 에선 컴포넌트에게 업데이트 조건을 걸어 원하는 컴포넌트만 rendering이 되도록 하는 React.memo 을 사용 한다 !!

Rendering 문제 예시

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

const CountView = ({ count }) => {
  useEffect(() => {
    console.log(`CountView is update count: ${count}`);
  });
  return <div>{count}</div>;
};
const TextView = ({ text }) => {
  useEffect(() => {
    console.log(`TextView is update text: ${text}`);
  });
  return <div>{text}</div>;
};

const OptimizeText = () => {
  const [count, setCount] = useState(1);
  const [text, setText] = useState("");
  ///
  return (
    <div style={{ padding: 50 }}>
      <div>
        <h2>count</h2>
        <CountView count={count} />
        <button
          onClick={() => {
            setCount(count + 1);
          }}
        >
          +
        </button>
      </div>
      <div>
        <h2>text</h2>
        <TextView text={text} />
        <input
          value={text}
          onChange={(e) => {
            setText(e.target.value);
          }}
          text={text}
        />
      </div>
    </div>
  );
};

export default OptimizeText;

OptimizeText Component 에서 count 또는, text State 가 변경 될 때마다 CountView Component 와 TextView Component 둘 다 rendering 이된다.

React.memo

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

const CountView = React.memo(({ count }) => {
  useEffect(() => {
    console.log(`CountView is update count: ${count}`);
  });
  return <div>{count}</div>;
});
const TextView = React.memo(({ text }) => {
  useEffect(() => {
    console.log(`TextView is update text: ${text}`);
  });
  return <div>{text}</div>;
});

const OptimizeText = () => {
  const [count, setCount] = useState(1);
  const [text, setText] = useState("");
  ///
  return (
    <div style={{ padding: 50 }}>
      <div>
        <h2>count</h2>
        <CountView count={count} />
        <button
          onClick={() => {
            setCount(count + 1);
          }}
        >
          +
        </button>
      </div>
      <div>
        <h2>text</h2>
        <TextView text={text} />
        <input
          value={text}
          onChange={(e) => {
            setText(e.target.value);
          }}
          text={text}
        />
      </div>
    </div>
  );
};

export default OptimizeText;

Component 를 React.memo 로 감싸주게되면, 해당 Component 는 prop 의 값이 변경 될 때만 Rendering 이 된다.

React.memo 에서 areEqual 사용

prop 의 값이 Promitive Type 일 경우 에는 해당 prop 의 값이 변경이 되지 않으면 rendering 이 되지 않지만, Object 와 같은 Reference Type 일 경우 에는 주소 값을 저장하기 때문에 prop 의 값이 변경 되지 않아도 다른 값으로 인식하기 때문에 rendering 이 된다. 이때, 다음과 같이 두번째 매개변수로 areEqual 함수를 넘겨주어 rendering이 되도록 수정할 수 있다.

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

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

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

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

const MemoizedCounterB = React.memo(CounterB, areEqual);

const 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: 1 })}>B Button</button>
      </div>
    </div>
  );
};

export default OptimizeTest;

3. useCallback

useCallback

useCallbakc 은 함수를 메모이제이션 하기 위해서 사용하는 React Hooks 으로, useMemo 와 비슷하다.

useMemo 는 함수를 값처럼 사용해서 연산 최적화를 위해서 사용한다면, useCallback 은 특정 함수를 재정의(새로 만들지 않고) 하지 않고 재사용 하기 위해 사용한다.

useCallback(() => {}, []);

useCallback 이 필요한 이유

React.memo 는 전달된 prop 을 비교할 때 얕은 비교를 하기 때문에 prop 이 ‘값’ (Promitive Type) 이 아니라 ‘객체’ 나 ‘함수’ (Reference Type) 일 경우, prop 에 변화가 있는 것으로 판단하여 Component 를 re-rendering 한다. 즉, 이를 최적화 하기 위해서는 prop 에 변화가 있는 것으로 판단하지 않도록 해당 함수를 useCallback 으로 감싸줌으로써 재생성 하지못하도록 하여 이 문제를 해결할 수 있다.

useMemo 를 사용하면 안되는 이유 useMemo 는 함수 반환이 아닌 값을 반환하기 때문에 함수 자체를 반환하기 위해서 useCallback 을 사용한다.

import React, { useCallback, useEffect, useRef, useState } from "react";
import "./styles.css";

const ComponentA = ({ data, deleteData }) => {
  useEffect(() => {
    console.log("ComponentA is render");
  });

  return (
    <div>
      {data.map((it) => (
        <div>
          <div>{it.id}</div>
          <button
            onClick={() => {
              deleteData(it.id);
            }}
          >
            삭제
          </button>
        </div>
      ))}
    </div>
  );
};

//React.memo 전달된 prop이 변경될 때만 랜더링
const ComponentB = React.memo(({ createData }) => { 
  useEffect(() => {
    console.log("ComponentB is render");
  });

  return (
    <div>
      <button onClick={createData}>데이터 삽입</button>
    </div>
  );
});

export default function App() {
  const [data, setData] = useState([]);
  const num = useRef(0);

  const createData = () => {
    num.current++;
    setData([...data, { id: num.current }]);
  };

  const deleteData = (id) => {
    setData(data.filter((it) => it.id != id));
  };
  return (
    <div className="App">
      <ComponentA data={data} deleteData={deleteData} />
      <ComponentB createData={createData} />
    </div>
  );
}

예를 들어, 위와 같은 상황에서 App Component 의 data state 는 Component B의 button (데이터 삽입) 을 누르게 되면 data state 가 변경되기 때문에 App Component 가 re-rendering 하게 된다.

이 때, App Component 의 createData 함수를 재선언 하게 되는데, createData 함수는 reference type 이므로 Component B 는 prop 인 createData 함수가 변경되었다고 판단하고 Componet B 가 re-rendering 된다.

따라서, useCallback Hook 을 사용 하여 App Component 가 rendering 될 때, createData 함수가 재선언 되지 않도록 하여 이 문제를 해결 할 수 있다.

useCallback 사용

함수가 재선언 되지 않게, 최적하를 하기 위해 함수를 useCallback 으로 감싸준다.

import React, { useCallback, useEffect, useRef, useState } from "react";
import "./styles.css";

const ComponentA = ({ data, deleteData }) => {
  useEffect(() => {
    console.log("ComponentA is render");
  });

  return (
    <div>
      {data.map((it) => (
        <div>
          <div>{it.id}</div>
          <button
            onClick={() => {
              deleteData(it.id);
            }}
          >
            삭제
          </button>
        </div>
      ))}
    </div>
  );
};

//React.memo 전달된 prop이 변경될 때만 랜더링
const ComponentB = React.memo(({ createData }) => {
  useEffect(() => {
    console.log("ComponentB is render");
  });

  return (
    <div>
      <button onClick={createData}>데이터 삽입</button>
    </div>
  );
});

export default function App() {
  const [data, setData] = useState([]);
  const num = useRef(0);

  const createData = useCallback(() => {
    num.current++;
    setData((data) => [...data, { id: num.current }]);
  }, []);

  // const createData = () => {
  //   num.current++;
  //   setData([...data, { id: num.current }]);
  // };

  const deleteData = (id) => {
    setData(data.filter((it) => it.id != id));
  };
  return (
    <div className="App">
      <ComponentA data={data} deleteData={deleteData} />
      <ComponentB createData={createData} />
    </div>
  );
}

useCallback 의 defendency array 에 빈 배열로 전달하면, setData 함수의 data 는 App Component 가 Mount 되는 시점의 data 값(빈 배열) 을 전달하기 때문에 최신 state 값을 유지 할 수 없다.

그렇다고 defendency array 에 data 의 값을 넣어주면 data 가 삭제되거나 수정 될 때 마다 App Component 가 re-rendering 이 되기 때문에 딜레마에 빠지게 된다.

이 때, 함수형 업데이트를 사용하여 최신 state 를 참조할 수 있게 하여 이 문제를 해결 할 수 있다.

setData((data) => [newItem, ...data]);
함수형 업데이트 setState 함수에 함수를 전달하는 방식 state 값을 초기화 해줌으로써 defendency array 를 비워도 항상 최신의 state를 참조 할 수 있게 한다.

UseReducer

UseReducer

useReducer 는 useState 처럼 Component 의 상태 관리를 Component 안에서 사용하는 것과 달리, Component 의 상태관리를 Component 밖으로 분리하기 위해 사용하는 React Hook 이다.

Component 의 구조가 복잡해지는 경우 useState 를 대신하여 상태 변화 로직 들을 Component 밖에서 사용함으로써 Component 를 더욱 가볍게 작성 할 수 있도록 도와준다.

useReducer 가 필요한 이유

useState 를 이용하여 state 를 관리 하다보면, 코드가 길어지고 복잡해 질 수 있다.

만약 하나의 Component 의 여러개의 state 가 존재 하거나 Object state 를 가지게 된다면 해당 Component 가 무거워 질 수 있기 때문에 useState 를 사용한 상태관리는 되도록 지양하고 useReducer 를 사용하여 component 상태 변화 로직을 분리하는 것이 필요하다.

useReducer 사용

const Counter = () => {
    const [count, setCount] = useState(1);	//useState 로 state 관리 
	
    const add1 = () => {
    	setCount(count + 1);
    }
    
    const add10 = () => {
    	setCount(count + 10);
    }
    
    const add100 = () => {
    	setCount(count + 100);
    }
    
    const add1000 = () => {
    	setCount(count + 1000);
    }
    
    return ( 	
        <div>
            {count}
            <button onClick={add1}> add 1 </button>
            <button onClick={add10}> add 10 </button>
            <button onClick={add100}> add 100 </button>
            <button onClick={add1000}> add 1000 </button>
        </div>
    )
}

예를들어, 위처럼 useState 를 사용하여 count state 를 1, 10, 100, 1000 을 더하도록 상태관리를 할 수 있다. 이를 useReducer 를 사용하면 다음과 같다.

const reducer = {state, action} => {
	switch(action.type)
    	case 1:
        	return state + 1;
        case 10:
        	return state + 10;
        case 100:
        	return state + 100;
        case 100:
        	return state + 1000;
        default:
        	return state;
}

const Counter = () => {
    const [count, dispatch] = useReducer(reducer, 1);
    
    return (
    	<div>
            {count}
            <button onClick={() => dispatch({ type : 1 })}> add 1 </button>
            <button onClick={() => dispatch({ type : 10 })}> add 10 </button>
            <button onClick={() => dispatch({ type : 100 })}> add 100 </button>
            <button onClick={() => dispatch({ type : 1000 })}> add 1000 </button>
        </div>
   }
}
  • const [count, dispatch] = useReducer(reducer, 1);
    • 비구조화 할당을 통해 state(count) 와 dispatch 함수를 할당 받는다.
    • 상태변화를 처리하는 reducer 라는 함수를 useReducer의 첫번째 인자로 전달한다.
    • state의 초기값을 두번째 인자로 전달한다.
  • dispatch({type:1})
    • 액션 객체를 type 이라는 property 를 이용하여 dispatch 함수로 전달한다.
  • const reducer = (state, action) ⇒ {}
    • dispatch 함수로 전달 받은 액션 객체는 reducer 함수로 전달되어 상태 변화를 처리한다.
    • 현재 상태인 state 를 첫번째 인자로 전달한다.
    • dispatch 함수로 전달 받은 액션 객체를 두번 째 인자로 전달한다.

Context

Context

Context 는 React Component 트리 안에서 전역 데이터를 공유 하고 관리하여 Component 의 단계 마다 일일이 props 를 넘겨주지 않고도 Component 트리 전체에 데이터를 제공할 수 있다.



Context 가 필요한 이유

프로젝트의 Component 트리 구조가 아래와 같을 때, 가장 하위에 있는 DiaryItem Component 에 prop을 전달하기 위해서는 최상단 Component 인 App Component 에서 DiaryList Component 에 prop 을 넘겨주고 이를 다시 DiaryItem Component 에 전달하는 방식을 따르게 된다. 이러한 props 드릴링은 너무 번거롭고 규모가 커질 수록 복잡해 지기 때문에 이를 해결하기 위해서 Context 라는 개념이 생겼다.



Context 사용

  1. Context 생성

    React 패키지에서 제공하는 createContext 함수를 사용하여 Context 를 생성한다.

    그리고 외부 컴포넌트에서 사용할 수 있도록 export 를 사용해서 내보낸다.

    import { createContext } from "react";
    export const DiaryStateContext = React.createContext();
  1. Context 저장

    Context.Provider 로 감싸주어 하위에 모든 컴포넌트들이 value 에 저장되어있는 전역데이터에 접근할 수 있게 된다.

    return (
        <DiaryStateContext.Provider value={data}>
            <div className="App">
              <DiaryEditor />
              <DiaryList />
            </div>
        </DiaryStateContext.Provider>
      );
  1. Context 접근

    useContext 를 이용하여 Context 에 저장된 전역 데이터에 접근할 수 있다.

    const { data } = useContext(DiaryStateContext);

중첩된 Context 사용

하나의 Context 의 value prop 에 변수 뿐만 아니라 함수도 포함하여 전달할 경우 Provider 또한 Component 이기 때문에 prop이 변경 될 경우 Provider 가 재생성 되면서 하위 Component 들도 재생성 되게 된다. 예를들어 아래에서 data 가 변경 될 때 마다 DiaryStateContext.Provider 하위에 있는 모든 Component 들이 re-rendering 되기 때문에 DiaryStateContext 에 상태변경 함수를 넣어주게 되면 함수 또한 재성성이 되므로 최적화가 풀려 버릴 수 있다. 이러한 경우 중첩 Context 를 사용한다.

return (
    <DiaryStateContext.Provider value={data, onCreate, onEdit, onRemove}>
        <div className="App">
          <DiaryEditor />
          <DiaryList />
        </div>
    </DiaryStateContext.Provider>
  );

다음과 같이 DiaryStateContext 하위에 별도의 Context 를 생성해서 data 가 변경되어도 DiaryDispatchContext 에 전달한 상태변경 함수는 변동이 없기 때문에 최적화를 유지할 수 있다.

export const DiaryDispatchContext = React.createContext();
 
 const memoizedDispatch = useMemo(() => {
    return { onCreate, onRemove, onEdit };
  }, []);

return (
    <DiaryStateContext.Provider value={data}>
      <DiaryDispatchContext.Provider value={memoizedDispatch}>
        <div className="App">
          <DiaryEditor />
          <DiaryList />
        </div>
      </DiaryDispatchContext.Provider>
    </DiaryStateContext.Provider>
  );
728x90
댓글
반응형
250x250
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
TAG
more
«   2026/01   »
1 2 3
4 5 6 7 8 9 10
11 12 13 14 15 16 17
18 19 20 21 22 23 24
25 26 27 28 29 30 31
글 보관함