FrontEnd/React

[React] 폼의 입력 필드 리렌더링 최적화하기 : React.memo 활용 (+useMemo 비교)

devOhzl 2024. 11. 28. 16:25

로그인 폼을 구현하던 중, 입력 필드의 rules를 확인하여 FormHelperText를 표시하는 과정에서 리렌더링 문제가 발생했습니다.

구체적으로, ID 입력 필드에서 에러 처리를 했을 때 콘솔에 ID와 PW 필드가 모두 리렌더링되는 상황을 겪었습니다.

이는 각 필드가 독립적으로 리렌더링되지 않고 모든 필드가 동시에 리렌더링되었기 때문입니다.

이러한 문제를 해결하기 위해 React.memo를 사용해 최적화를 시도했습니다.

 

useMemo 방식과 비교하여 각각의 방식이 어떻게 성능에 영향을 주는지 예시와 함께 작성했습니다.

React 리렌더링 방식

React는 부모 컴포넌트가 리렌더링될 때 자식 컴포넌트도 기본적으로 리렌더링을 시도합니다.

리렌더링 시도는 "컴포넌트를 다시 그린다"는 의미가 아니라, 변경 사항을 감지하기 위해 컴포넌트를 다시 실행하는 과정입니다.

리렌더링이 발생하는 주요 조건은 다음과 같습니다:

  1. 상태(State) 변경: 컴포넌트의 상태가 변경되면 해당 컴포넌트는 리렌더링됩니다.
  2. props 변경: 부모로부터 전달받은 props 값이 변경되면 자식 컴포넌트가 리렌더링됩니다.
  3. 부모 컴포넌트의 리렌더링: 부모가 리렌더링되면 자식도 리렌더링됩니다.

이렇게 기본적으로 부모가 리렌더링될 때 자식도 리렌더링되기 때문에, 리렌더링 범위를 줄이기 위해서는 최적화가 필요합니다.

useMemo

메모이제이션을 통해 비용이 많이 드는 연산을 다시 실행하지 않도록 최적화하는 훅입니다.

컴포넌트가 리렌더링될 때마다 동일한 계산이나 객체 생성이 반복적으로 이루어진다면, useMemo를 사용하여 이전 결과를 재사용함으로써 성능을 개선할 수 있습니다.

⭐ useMemo의 작동 방식

  1. useMemo는 첫 번째 인자로 전달된 함수를 실행하여 결과를 반환하고, 이를 메모이제이션합니다.
  2. 두 번째 인자로 전달된 의존성 배열을 기준으로, 값이 변경되지 않으면 기존 메모이제이션된 값을 반환합니다.
  3. 의존성 배열의 값이 변경된 경우에만 함수가 다시 실행되어 새로운 결과를 반환합니다.
const expensiveCalculation = (num) => {
  console.log("Expensive Calculation...");
  return num * 2;
};

const MyComponent = ({ value }) => {
  const computedValue = useMemo(() => expensiveCalculation(value), [value]);

  return <div>Computed Value: {computedValue}</div>;
};

위 코드에서 ParentComponent의 상태가 변경될 때마다 일반적으로 모든 자식 컴포넌트도 리렌더링됩니다.

하지만 React.memo로 ChildComponent를 감싸면, name props가 변경되지 않는 한 ChildComponent는 리렌더링되지 않습니다.

따라서, 버튼 클릭으로 count가 변경되더라도 ChildComponent는 불필요하게 다시 렌더링되지 않게 됩니다.

 

React.memo

고차 컴포넌트(Higher-Order Component)로, 전달된 props가 변경되지 않을 경우 해당 컴포넌트를 다시 렌더링하지 않도록 최적화하는 역할을 합니다.

이를 통해 부모 컴포넌트가 리렌더링되더라도 자식 컴포넌트는 props가 변경되지 않으면 리렌더링을 방지할 수 있습니다.

⭐ React.memo 작동 방식

  1. React.memo로 감싸진 컴포넌트는 props의 이전 값과 새 값을 비교합니다.
  2. props가 이전과 동일하다면 컴포넌트를 다시 렌더링하지 않고, 이전에 렌더링한 결과를 재사용합니다.
  3. props가 변경된 경우에만 컴포넌트를 다시 렌더링합니다.
const ChildComponent = React.memo(({ name }) => {
  console.log("ChildComponent rendered");
  return <div>{name}</div>;
});

const ParentComponent = () => {
  const [count, setCount] = useState(0);

  return (
    <>
      <button onClick={() => setCount(count + 1)}>Increase Count</button>
      <ChildComponent name="Hello" />
    </>
  );
};

 

위 코드에서 ParentComponent의 상태가 변경될 때마다 일반적으로 모든 자식 컴포넌트도 리렌더링됩니다.

하지만 React.memo로 ChildComponent를 감싸면, name props가 변경되지 않는 한 ChildComponent는 리렌더링되지 않습니다.

따라서, 버튼 클릭으로 count가 변경되더라도 ChildComponent는 불필요하게 다시 렌더링되지 않게 됩니다.

 

⭐ 폼에서 텍스트 필드 리렌더링 최적화하기

const TextFieldItem = React.memo(({ itemKey, title, requiredText, control, error }) => {
  // rules 객체는 매번 리렌더링될 때마다 생성되지만, 여기서는 단순한 객체이므로 useMemo 없이도 큰 문제가 되지 않습니다.
  const rules = { required: requiredText };
  
  return (
    <Box>
      <Typography fontWeight="bold" sx={{ mb: 1 }}>
        {title}
      </Typography>
      <Controller
        name={itemKey}
        control={control}
        defaultValue=""
        rules={rules}
        render={({ field }) => (
          <TextField
            {...field}
            fullWidth
            variant="outlined"
            placeholder={requiredText}
            error={!!error}
          />
        )}
      />
      <FormHelperText
        sx={{
          transition: 'all 0.5s ease-in-out',
          opacity: error ? 1 : 0,
          visibility: error ? 'visible' : 'hidden',
          color: theme.palette.error.main,
        }}
      >
        {error?.message}
      </FormHelperText>
    </Box>
  );
});

여기서 중요한 최적화 요소는 React.memo입니다.

TextFieldItem은 여러 입력 필드로 재사용될 수 있는 컴포넌트이기 때문에, 부모 컴포넌트가 리렌더링될 때마다 불필요하게 다시 렌더링되지 않도록 React.memo로 감쌌습니다.

이렇게 하면 props(itemKey, title, requiredText, control, error)가 변경되지 않는 한 컴포넌트를 다시 렌더링하지 않도록 합니다.

 

또한, rules 객체는 단순히 { required: requiredText } 형태로 생성되기 때문에, useMemo를 사용하지 않아도 큰 성능 문제가 없습니다. 만약 rules가 더 복잡한 연산을 필요로 하는 경우라면 useMemo를 사용하여 객체 생성을 최적화할 수 있습니다.

결론

React에서 성능 최적화를 위해 React.memo와 useMemo는 필수적인 도구입니다.

React.memo는 컴포넌트의 리렌더링을 방지하고, useMemo는 비용이 많이 드는 연산을 메모이제이션하여 재실행을 방지합니다.

TextFieldItem과 같은 컴포넌트에 React.memo를 사용함으로써, 부모의 리렌더링이 자식 컴포넌트로 불필요하게 전파되는 것을 막고, 성능을 크게 향상시킬 수 있습니다.