React Children 파헤치기

taehyung·2024년 7월 24일

React.js

목록 보기
22/24
post-thumbnail

Children 이란?

React에서 사용하는 JSX 문법의 일종으로 컴포넌트를 부모-자식 관계로 표현하는 것입니다.
부모-자식으로 설정된 컴포넌트 관계에서 부모 컴포넌트는 props 객체에 children 속성으로 접근할 수 있습니다.

function Parent(props) {
  const [count, setCount] = useState(0);
  console.log("부모 렌더링");
  console.log(props.children);
  return (
    <div>
      <p>{count}</p>
      <p onClick={() => setCount((prev) => prev + 1)}>Parent</p>
    </div>
  );
}

function Child() {
  console.log("자식 렌더링");
  return (
    <div>
      <div>children</div>
    </div>
  );
}

function App() {
  return (
    <>
      <Parent number={0}>
        <Child bool={true}></Child>
      </Parent>
    </>
  );
}

⭐️ children은 반드시 JSX만 전달해야 하는것은 아닙니다. 어떠한 데이터도 전달 할 수 있습니다.

예시

function App() {
  return (
    <>
      <Parent number={0}>
        <Child bool={true}></Child>
        <>반드시 JSX가 아니어도 됩니다.</>
        {{ a: "객체도", b: "가능합니다." }}
      </Parent>
    </>
  );
}

Children은 뭐가 다를까?

부모 자식관계는 React를 공부했다면 children이 아니라도 부모 컴포넌트에서 자식 컴포넌트를 직접 import 하여 사용해도 props로 데이터 전달이 가능한데 왜 쓰는걸까요?

가독성 측면에 있어서 그다지 유리해보이지는 않습니다. 그럼에도 만들어놓은 이유가 뭘까요?


일반적인 import의 자식컴포넌트의 리렌더링

function Parent() {
  const [count, setCount] = useState(0);
  console.log("부모 렌더링");
  return (
    <div>
      <p>{count}</p>
      <button onClick={() => setCount((prev) => prev + 1)}>
        부모 리렌더링
      </button>
      <Child />
    </div>
  );
}

function Child() {
  console.log("children 렌더링");

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

function App() {
  return (
    <>
      <Parent />
    </>
  );
}

부모 컴포넌트에서 자식을 직접 import해서 사용하고있는데요. ( 컴포넌트가 분리되어있어도 똑같습니다. )

부모 컴포넌트가 리렌더링되면 당연히 리렌더링 트리거 중 하나인 "부모 컴포넌트가 리렌더링 되었을 때" 에 해당하기 때문에 자식또한 리렌더링 됩니다.

예상했던 결과와 일치합니다.


children Props의 자식컴포넌트의 리렌더링

function Parent({ children }: { children: ReactNode }) {
  const [count, setCount] = useState(0);
  console.log("부모 렌더링");
  return (
    <div>
      <p>{count}</p>
      <p onClick={() => setCount((prev) => prev + 1)}>Parent</p>
      {children}
    </div>
  );
}

function Child() {
  console.log("children 렌더링");

  return (
    <div>
      <div>children</div>
    </div>
  );
}
function App() {
  return (
    <>
      <Parent>
        <Child></Child>
      </Parent>
    </>
  );
}

이번엔 children props를 이용하여 자식 컴포넌트를 부모 컴포넌트에서 렌더링 했습니다.

이 경우에는 어떨까요?

❗️엇, 예상했던 결과와는 다른데요?
부모컴포넌트가 리렌더링 되었음에도 불구하고 자식컴포넌트는 리렌더링 되지 않았습니다!


렌더링 최적화에 사용하기

children props는 부모 컴포넌트가 리렌더링 되어도 아무 이유없이 리렌더링 되지않네요?

  1. 부모 컴포넌트가 렌더링 되었을 때
  2. state가 변동되었을 때
  3. props가 변동되었을 때
  4. key props가 변동되었을 때

위 네가지 렌더링 트리거에서 1번에 영향을 받지 않는다는것을 알 수 있습니다.

1번의 경우 자식컴포넌트는 아무런 변화가 없는데 리렌더링 된다는것이 특정 상황을 제외하고는 매우 비효율적으로 느껴집니다.

그래서 우리는 종종 React.memo를 통해 컴포넌트를 메모이제이션하고 리렌더링을 방지하는 코드를 짜왔습니다.

children props를 사용하면 개발자에 의도로 메모이제이션 하지 않아도 메모이제이션이 되는걸까요?

렌더링 최적화에 도움이 된다는 것은 알았지만 그냥 외운다기보다 더 잘 알고싶기에 구글링을 통해 좋은 레퍼런스를 찾았습니다.

https://velog.io/@2ast/React-children-prop에-대한-고찰feat.-렌더링-최적화

React.memo()

본격적인 설명에 앞서 컴포넌트 메모이제이션 함수인 React.memo()에 대해 알아보겠습니다.

  1. 부모 컴포넌트가 렌더링 되었을 때

주로 이 상황을 막기위해 사용하며 자식 컴포넌트를 메모이제이션 합니다.

이렇게하면 2,3,4 번에 해당하는 상황에만 리렌더링됩니다.

const MemoComponents = () => {
  console.log("자식 컴포넌트 리렌더링");
  return <div>memo</div>;
};

function Memo() {
  const [count, setCount] = useState(0);
  return (
    <div>
      <>
        <MemoComponents></MemoComponents>
      </>
      <div>{count}</div>
      <button
        onClick={() => {
          setCount(count + 1);
        }}
      >
        리렌더링
      </button>
    </div>
  );
}

예상했던 결과와 일치합니다.

이제 메모이제이션 한 결과를 보겠습니다.

const MemoComponents = React.memo(() => {
  console.log("자식 컴포넌트 리렌더링");
  return <div>memo</div>;
});

코드를 이렇게 변경하고 메모이제이션 했습니다. 그랬더니 짠!

부모 컴포넌트가 렌더링되는 상황에서 리렌더링 되지 않습니다. 어디서 많이 본 결과네요. children 과 매우 흡사합니다.


children과 memo의 이상현상 ( 메모이제이션 무효화 )

여러분 가족은 나와 내 부모만 있는것이 아닙니다. 부모의 부모도 있고 그 부모의 부모도 있듯이 리액트 컴포넌트는 단연 부모-자식 1뎁스에서 끝나는 것이 아닙니다.

이렇게 A, B, C 컴포넌트가 있습니다. 아래는 코드입니다.

const ComponentA = () => {
  const [toggle, setToggle] = useState(false);

  console.log("A 렌더링");
  return (
    <>
      <div
        onClick={() => {
          setToggle(!toggle);
        }}
      >
        A
      </div>
      <div>
        <ComponentB>
          <ComponentC></ComponentC>
        </ComponentB>
      </div>
    </>
  );
};
const ComponentB = ({ children }: { children: React.ReactNode }) => {
  const [toggle, setToggle] = useState(false);

  console.log("B 렌더링");
  return (
    <>
      <div
        onClick={() => {
          setToggle(!toggle);
        }}
      >
        B
      </div>
      <div>{children}</div>
    </>
  );
};
const ComponentC = () => {
  const [toggle, setToggle] = useState(false);

  console.log("C 렌더링");
  return (
    <div
      onClick={() => {
        setToggle(!toggle);
      }}
    >
      C
    </div>
  );
};

function Components() {
  return <ComponentA></ComponentA>;
}
  • A컴포넌트는 B컴포넌트를 Import 했습니다.
  • B컴포넌트는 C컴포넌트를 Children Props 로 받습니다.

문제1. A 컴포넌트가 리렌더링 되면 어떤 컴포넌트가 리렌더링 될까요?
문제2. B컴포넌트가 리렌더링 되면 어떤 컴포넌트가 리렌더링 될까요?

1.정답 : A,B,C 리렌더링
2.정답 : B 리렌더링


자, 그렇다면 여기서 A컴포넌트의 리렌더링에 B컴포넌트가 반응하지 않게하려면 어떻게 해야할까요?

  1. B컴포넌트를 A컴포넌트에게 children으로 제공한다.
  2. React.memo를 사용한다.

1 번은 많이 해봤으니 2번을 사용해보겠습니다.

const ComponentB = React.memo(({ children }: { children: React.ReactNode }) => {
  const [toggle, setToggle] = useState(false);

  console.log("B 렌더링");
  return (
    <>
      <div
        onClick={() => {
          setToggle(!toggle);
        }}
      >
        B
      </div>
      <div>{children}</div>
    </>
  );
});

메모이제이션 해주면 ~

어라? 결과가 바뀌지 않습니다.. B컴포넌트를 메모이제이션해도 계속 리렌더링이 발생하고있습니다.. 왜 그런걸까요??

분명 메모이제이션 했는데!

기본에 충실해야 답이 보인다.

구글링을하고 생각해본 결과 우선 각자의 역할에 대해 알아야 이 문제를 해결할 수 있습니다.

⭐️ 각각의 역할
1. React.memo : 부모 컴포넌트의 리렌더링에 반응하지 않는다. 즉, Props와 자신의 State에 의해서만 리렌더링이 트리거된다.
2. Children : 부모 컴포넌트의 리렌더링에 반응하지 않는다. 즉, Props와 자신의 State에 의해서만 리렌더링이 트리거된다.

추가적으로 Children은 Props를 통해 전달됩니다.

⭐️힌트
1. React.memo로 메모이제이션 한 컴포넌트 B는 C컴포넌트를 Props로 받습니다.
2. A컴포넌트의 상태를 변경하여 리렌더링 시켰을 때, B컴포넌트가 리렌더링되는 시나리오는 단 하나밖에 없습니다.

B컴포넌트의 Props가 변경된것이죠. 바로 children props가 변경되었다는 겁니다.

React 기본 JSX문법

function MyComponent() {
  return <div>Hello, world!</div>;
}

이런 JSX 문법이 있다고 했을 때 MyComponent가 return 하는건 뭘까요? HTML인가요? 아닙니다.

function MyComponent() {
   return React.createElement('div', null, 'Hello, world!');
}

이렇게 생긴 함수 실행문을 리턴하는겁니다. 그럼 React.createElement() 의 실행 결과값은 뭘까요?

이렇게 생긴 객체를 리턴합니다.

console.log(<MyComponent/> === <MyComponent/>); ///false

그리고 위 코드의 결과는 이렇습니다. 모든 컴포넌트는 새로 생성될때마다 다른 메모리 주소에 할당되는 것이죠. 같은 객체를 참조하는 것이 아니라는 겁니다.

❓만약 같은 객체를 참조한다면 무슨일이 생길까요?
모든 컴포넌트는 무슨짓을해도 똑같을 수 밖에 없겠죠? 그렇기 때문에 리액트는 하나의 컴포넌트를 많이 사용한다고해도 다 각자의 인스턴스처럼 행동하게 됩니다.


메모이제이션 무효화의 이유

그럼 다시 A 컴포넌트를 보도록 하겠습니다.

const ComponentA = () => {
  const [toggle, setToggle] = useState(false);

  console.log("A 렌더링");
  return (
    <>
      <div
        onClick={() => {
          setToggle(!toggle);
        }}
      >
        A
      </div>
      <div>
        <ComponentB>
          <ComponentC></ComponentC>
        </ComponentB>
      </div>
    </>
  );
};
  1. A컴포넌트가 리렌더링 되면 B와 C컴포넌트를 렌더링 합니다.
  2. B컴포넌트는 C컴포넌트를 Props로 받지만, C컴포넌트는 A컴포넌트가 렌더링될때마다 새로 생성됩니다.
  3. B컴포넌트의 Props인 C컴포넌트가 새로 생성되었기 때문에 B컴포넌트의 Props가 변경됩니다.
  4. B컴포넌트는 메모이제이션 했지만 Props가 변경되었으니 리렌더링 됩니다.

부모요소의 리렌더링에 children이 반응하지 않는 이유

지금까지 알아본 결과로 children 은 단순한 props와 같다고 했습니다.

function Components() {
  
  return <ComponentA>
    		<ComponentB/> //이건 단순히 리액트 엘리먼트로 판별됨
    	 </ComponentA>;
}
function Components() {
  return <ComponentA children={<ComponentB/>}>
}

위 두개의 예제는 완전히 동일한 결과를 내놓습니다. 그렇다면 props의 어떠한 특징때문에 children 요소는 부모 요소의 리렌더링에 반응하지 않을까요?

예제

const ComponentB = ({ random }: { num: number }) => {
  const [toggle, setToggle] = useState(false);
  console.log("B 리렌더링");

  return (
    <div>
      {random}
      <button
        onClick={() => {
          setToggle(!toggle);
        }}
      >
        리렌더링
      </button>
    </div>
  );
};

function ComponentA() {
  const randomNum = () => {
    return Math.random();
  };

  return <ComponentB random={randomNum()} />;
}

B컴포넌트의 리렌더링 버튼을 누르면 console 창에는 "B 리렌더링" 이라는 단어만 나오고 실제 DOM에 표시된 랜덤한 숫자는 변경되지 않습니다.

A컴포넌트가 리렌더링 되지 않는다면 저 랜덤은 영원히 변하지 않을겁니다. 즉, 컴포넌트가 리렌더링 되어도 Props는 변하지 않는다는겁니다.


결론

React.memo가 적용된 children을 포함한 컴포넌트가 메모이제이션을 무효화하는 이유

children을 포함한 컴포넌트가 리렌더링 되면 children 컴포넌트를 다시 그리기 때문에 객체의 값이 변경된다.
children을 포함한 컴포넌트의 props인 children이 다시 생성되었기 때문에 Props가 변경되어 리렌더링되기 때문에 memo가 먹통이된다.

children으로 제공된 컴포넌트가 부모의 렌더링에 반응하지 않는 이유

부모 컴포넌트가 리렌더링되어도 children Props로 받은 children 컴포넌트는 동일한 객체 주소를 참조하고있기 때문에 리액트가 다시 그리지 않는다.

profile
Front End

0개의 댓글