[React] children, 자식 컴포넌트의 리렌더링에 관하여

강동욱·2024년 2월 14일
0

React Element란

그전에 children이란

children은 react Element의 prop이다.

element로 전달했을 때

const Parent = ({ children }) => {
  return <>{children}</>;
};

<Parent>
  <Child />
</Parent>;
const parent =  (props) => {
  return <>{props.children}</>
}

<Parent children={<Child/>}/>

render function으로 전달했을 때

// 함수로 전달
<Parent children={() => <Child />} />


<Parent>
  {() => <Child />}
</Parent>

// 실행
const Parent = ({ children }) => {
  return <>{children()}</>
}
<Parent children={Child} />;

const Parent = ({ children: Child }) => {
  return <>{<Child />}</>;
};

React Element

const child = <Child />

위와 같은 리액트의 문법적 설탕을 사용했을 때 어떤 상황이 일어나는지 정확히 알고 있어야 한다.
는 React.createElement 함수를 실행하고 객체를 리턴해주는 문법적 설탕에 불과하다.
즉 아래와 같은 코드와 같다.

const child = React.createElement(Child, null, null);
const Parent = () => {
  const child = <Child />;

  return <div>{child}</div>;
};

Child 컴포넌트는 오로지 return문에 있을때 그리고 Parent 컴포넌트가 렌더링이 될때만 렌더링이 된다.

React Element 업데이트

element은 불변 객체이다. 만약 element를 업데이트 하고 싶으면 객체 스스로가 재생성이 되면된다. 그렇게 되면 element는 리렌더링이 일어난다.

const Parent = () => {
  const child = <Child />;

  return <div>{child}</div>;
};

위와 같은 코드에서 Parent 컴포넌트가 리렌더링되면 child 변수는 재생성이 된다. 그렇게 되면 Child 컴포넌트는 리렌더링이 된다.

Child 컴포넌트의 리렌더링의 의문점

prop으로 전달된 컴포넌트는 왜 리렌더링이 되지 않을까?

const MovingComponent = ({ children }) => {
const [state, setState] = useState({ x: 100, y: 100 });

  return (
    <div onMouseMove={(e) => setState({ x: e.clientX - 20, y: e.clientY - 20 })} style={{ left: state.x, top: state.y }}>
      {children}
    </div>
  );
};
const SomeOutsideComponent = () => {
  return (
    <MovingComponent>
      <ChildComponent />
    </MovingComponent>
  );
};

children은 단지 SomeOutsideComponent에서 생성된 ChildComponent의 element이다. Parent 컴포넌트에서 리렌더링이 되어도 prop으로 전달된것은 ChildComponent의 React.createElement의 리턴값인 element이기 때문에 불변 객체이므로 재생성되지 않고 결론적으로는 ChildComponent는 리렌더링이 되지 않는다.

render function이 prop으로 전달될 때는 왜 리렌더링이 발생할까?

const MovingComponent = ({ children }) => {
  const [state, setState] = useState();
  return (
    <div 
    >
      {children()}
    </div>
  );
};

const SomeOutsideComponent = () => {
  return (
    <MovingComponent>
      {() => <ChildComponent />}
    </MovingComponent>
  )
}

MovingComponent에서 리렌더링이 발생했을 때 우리는 children 함수를 호출한다. 함수를 호출하면 리턴값인 ChildComponent 렌더링이 발생할 때 마다 재생성하게 된다. 그러므로 재생성이 이뤄지므로 ChildComponent에서도 리렌더링이 발생한다.

MovingComponent를 Reac.memo로 감싸도 SomeOutsideComponent에서 리렌더링이 발생될 때 ChildComponent는 왜 리렌더링이 될까?

const MovingComponent = ({ children }) => {
  const [state, setState] = useState();
  return (
    <div 
    >
      {children}
    </div>
  );
};

const MovingComponentMemo = React.memo(MovingComponent);

const SomeOutsideComponent = () => {
  const [state, setState] = useState();

  return (
    <MovingComponentMemo>
      <ChildComponent />
    </MovingComponentMemo>
  )
}

위와 같은 코드를 이해를 쉽게 하면 아래와 같이 나타낼 수 있다.

const MovingComponent = ({ children }) => {
  const [state, setState] = useState();
  return (
    <div 
    >
      {children}
    </div>
  );
};

const MovingComponentMemo = React.memo(MovingComponent);

const SomeOutsideComponent = () => {
  const [state, setState] = useState();

  return (
    <MovingComponentMemo chdilren={<ChildComponent />}/>
  )
}

ChildComponent는 SomeOutsideComponent가 리렌더링이 되면 매번 재생성이 된다. 메모아이즈된 MovingComponentMemo는 children prop의 변화를 감지한다. 그렇게되면 결국 MovingComponent도 리렌더링이 된다.

ChildComponent만 React.memo로 감싸면 Parent는 왜 감싸지 않아도 되는걸까?

const MovingComponent = ({ children }) => {
  const [state, setState] = useState();
  return (
    <div 
    >
      {children}
    </div>
  );
};

const ChildComponentMemo = React.memo(ChildComponent);

const SomeOutsideComponent = () => {
  const [state, setState] = useState();

  return (
    <MovingComponent>
      <ChildComponentMemo />
    </MovingComponent>
  )
}

ChildComponent가 메모아이즈 되있으므로 리렌더링에 방지된다. MovingComponent는 메모아이즈가 안되어있으므로 리렌더링이 일어날 것이지만 리액트가 children파트에 도달하면 ChildComponentMemo가 메모아이즈가 된걸 확인하고 그대로 넘어간다 그래서 리렌더링이 일어나지 않는다.

useCallBack을 이용해 메모아이징된 ChildComponent를 리턴하는 함수를 prop으로 전달했을 때 왜 함수가 메모아이징이 되지 않을까?

const MovingComponent = ({ children }) => {

  return (
    <div>
      {children()}
    </div>
  );
};

const SomeOutsideComponent = () => {
  const [state, setState] = useState();
  const child = useCallback(() => <ChildComponent />, []);

  return <MovingComponent children={child} />;
};

MovingComponent는 리렌더링이 일어난다. 리렌더링이 일어날때 children 실행시키고 리턴값은 메모아이징되지않은 ChildComponent이다. 그래서 매번 실행시키면 ChildComponent는 매번 재생성되고 리렌더링이 될 것 이다.


// MovingComponent.jsx
const MovingComponent = ({ children }) => {

  return (
    <div>
      {children()}
    </div>
  );
};

// SomeOutsideComponent.jsx
const MovingComponentMemo = React.memo(MovingComponent)
const SomeOutsideComponent = () => {
  const [state, setState] = useState();
  const child = useCallback(() => <ChildComponent />, []);

  return( 
  	<MovingComponentMemo>
      {child}
    <MovingComponentMemo/>
  )
};

만약에 ChildComponent를 메모아이즈 하고 싶으면 MovingComponent를 React.memo로 감싸주고 child함수를 위의 코드처럼 useCallback으로 감싸주면 될 것 이다. 이렇게 되면 MovingComponent가 리렌더링 되는 것을 방지해주고 이것은 곧 children 함수가 실행될 일이 없다는 뜻이다.


// MovingComponent.jsx
const MovingComponent = ({ children }) => {

  return (
    <div>
      {children}
    </div>
  );
};

// SomeOutsideComponent.jsx

const ChildComponentMemo = React.memo(ChildComponent)

const SomeOutsideComponent = () => {
  const [state, setState] = useState();

  return( 
  	<MovingComponent>
	  <ChildComponentMemo/>
    <MovingComponent/>
  )
};

또 하나의 방법은 위의 코드를 참고하면 함수 메모아이징을 삭제하고 ChildComponent를 React.memo로 감싸주면된다. MovingComponent가 리렌더링되면 children 함수의 반환값 ChildComponent는 메모아이징이 되므로 ChildComponent에 리렌더링은 일어나지 않는다.

출처

https://www.developerway.com/posts/react-elements-children-parents

profile
차근차근 개발자

0개의 댓글