React 부모-자식 컴포넌트간 리렌더링 최적화

Minseok Kim·2023년 2월 19일
0

회사 내부 어드민 툴을 만들다 보면 새로운 데이터를 생성하거나 수정할 일이 많다. 특히 domain이 복잡하고 클 수록 하나의 Form에서 관리해야 하는 데이터의 양이 엄청나게 많을 수 있다. 데이터를 그룹핑하여 자식 컴포넌트를 생성 함으로써 코드의 가독성을 높일 순 있지만, 데이터를 생성, 수정할 때에는 결국 하나로 합쳐져야 하기 때문에 최종적으로는 부모 컴포넌트에서 관리가 될 수 밖에 없는 구조다.

위 구조에서는 렌더링 최적화 이슈가 발생할 가능성이 매우 높다. 자식 컴포넌트에서 부모 컴포넌트에 정의된 state를 업데이트 할 경우, 부모 컴포넌트에 있는 모든 자식들의 업데이트가 일어난다. 즉, 하나의 Form 데이터를 A,B,C,D 그룹으로 나눠 자식 컴포넌트로 생성했다고 하더라도, A의 데이터를 수정할 때 A만 리렌더링이 발생하는 것이 아니라 B,C,D도 리렌더링이 발생하는 이슈가 생긴다.

위와 같은 구조로 코드가 작성된 경우, 관리해야 하는 데이터가 많아질 수록 리렌더링이 발생할 가능성도 높아지고 리렌더링의 대상도 많아져서 input 데이터를 변경할 때 매우 느리게 수정되는 현상이 발생하게 된다. 이와 같은 상황을 해결할 수 있는 방법은 어떤 것들이 있을까?

  1. State의 위치를 부모에서 자식 컴포넌트로 이동시키기

    리렌더링은 여러 가지 상황에서 발생할 수 있는데 기본적으로는 아래의 상황에서 리렌더링이 발생한다.

    • 선언되어있는 state의 값이 변경되는 경우
    • 전달받는 props의 값이 변경되는 경우
    • 부모 컴포넌트가 리렌더링 되는 경우

    부모 컴포넌트가 리렌더링 되는 경우 자식 컴포넌트들이 전부 리렌더링이 되기 때문에 최대한 부모 컴포넌트가 리렌더링 되는 상황을 만들지 말아야 한다. 이를 위해 꼭 부모가 관리할 필요가 없는 state가 있다면 child쪽으로 이동시키는게 필요하다. 하지만 앞서 이야기했던 대로 어드민에서는 모든 데이터를 한 곳에서 모아 생성 혹은 수정하는 api를 요청해야 하기 때문에 자식 컴포넌트로 이동을 시키는 것에 제한이 있을 수 밖에 없다.

  2. React Memo의 활용

    React Memo는 HOC의 한 종류로써, 렌더링 결과를 Memoizing해 불필요한 리렌더링을 줄일 수 있도록 도와준다.

import React, { useState } from 'react';

const Home = () => {
  const [child1Value, setChild1Value] = useState(0);
  const [child2Value, setChild2Value] = useState(0);


  return (
    <>
      <Child1 value={child1Value} setValue={setChild1Value} />
      <Child2 value={child2Value} setValue={setChild2Value} />
    </>
  );
};

export default Home;


const Child1 = ({value, setValue}) => {
  const handleClick = () => {
    setValue(value + 1);
  }

  console.log('child1 rendered');

  return (
    <div>
      Child1 / {value}
      <div onClick={handleClick}>Number Button</div>
    </div>
  )
}

const Child2 = React.memo(({value, setValue}) => {
  const handleClick = () => {
    setValue(value + 1);
  }

  console.log('child2 rendered');

  return (
    <div>
      Child2 / {value}
      <div onClick={handleClick}>Number Button</div>
    </div>
  )
})

위 상황에서 child2value를 업데이트 할 경우 부모 컴포넌트인 Home이 리렌더링 되기 때문에 Chil1 컴포넌트는 리렌더링이 발생한다. 반대로 child1value를 업데이트 할 경우, 전달 받는 child2value와 setChild2Value는 변하지 않았기 때문에 Child2 컴포넌트는 리렌더링이 발생하지 않는다.

만약 state로 관리되는 데이터가 primitive 값이 아닌 reference 타입의 데이터라면 어떻게 될까?

import React, { useState } from 'react';

const Home = () => {
  const [obj, setObj] = useState({
    child3: 0,
    child4: 0,
  });

  const handlePress = (key, value) => {
    const _obj = {...obj};
    _obj[key] = value;
    setObj(_obj)
  }

  return (
    <>
      <Child3 objValue={obj.child3} setObjValue={handlePress} />
      <Child4 objValue={obj.child4} setObjValue={handlePress} />
    </>
  );
};

const Child3 = ({objValue, setObjValue}) => {
  const handleObjClick = () => {
    setObjValue('child3', objValue + 1)
  }

  return (
    <div>
      Child3 / {objValue}
      <div onClick={handleObjClick}>Obj Button</div>
    </div>
  )
}

const Child4 = React.memo(({objValue, setObjValue}) => {
  const handleObjClick = () => {
    setObjValue('child4', objValue + 1)
  }

  return (
    <div>
      Child4 / {objValue}
      <div onClick={handleObjClick}>Obj Button</div>
    </div>
  )
}, checkEqualValue)

function checkEqualValue(prev, next) {
  return (
    prev.objValue === next.objValue
  );
}

obj안에 child3의 값을 변경할 경우, Child4가 전달받는 objValue(obj.child4), setObjValue(handlePress) 모두 변하지 않았기 때문에 Child4는 리렌더링이 발생하지 않고 Child3만 업데이트 된다.

하지만 문제는 obj안에 child4의 값을 변경할 때다. obj.child4의 값을 변경할 경우 부모 컴포넌트가 업데이트 되기 때문에 Child3도 같이 리렌더링이 발생하게 되는데, Child4 컴포넌트에서 갖고 있는 handlePress의 경우 최초 정의되어있는 obj의 상태값을 들고 있기 때문에 obj.child3의 최신 값이 아니라 초기값인 0으로 업데이트가 이루어지게 된다. 그렇기 때문에 한 obj로 관리되고 있는 데이터들은 자식 컴포넌트를 분리하는 것보다 하나의 컴포넌트로 관리되는 것이 좋다고 생각되는데, 혹시 다른 방법이 있는지 추후 더 리서치를 해볼 예정이다.

profile
Developer @CHLNGERS

0개의 댓글