Web Front-end/React

[React/Optimize/Memo] Component를 분리하고, 마지막으로 React의 Memorization을 활용해보자!

Component의 재사용성과 확장성을 갖기위해서 , 컴포넌트의 "합성"를 이용해서 , 좀 더 추상적인 컴포넌트를 만들고 혹은 재사용되는 컴포넌트를 만들고 구체화하는 방식으로 구현했습니다. 하지만, 그에 따른 부작용으로 컴포넌트간의 관계가 복잡해지고, 리렌더링 Issue가 생기기 시작했습니다.

 

현실적으로 FE 개발자에게 "생산성"은 빠질 수 없는 요소이기 때문에, 사실 이러한 이슈에 대해서 깊게 파고드는 것은 당장 필요한 사항은 아닐 수 있습니다. 세부적인 동작은 모두 React에 일임하고 , 우리는 그것을 사용하면 되니까요. 하지만,한번 깊이감있게 가져가보겠습니다. 재밌기도 하거든요!

Memo를 사용하기 전에, 독립적으로 나눌 수 있는 Component가 없는지 확인해보자 . (By 공식문서)

저보다 똑똑한 분들이 만든 React는 이미 훌륭한 라이브러리이고, 그 React만의 흐름을 잘 타서 활용하는것이 좋은 방법이지 , 이 React 만의 흐름을 깨는것은 바람직하지 못합니다. (강아지가 뒤로가고 싶은데 제가 앞에서 목줄을 당긴다면, 힘이 많이 들겠죠!)

흐름을 거스르지 맙시다.

Memo가 "최적화"방법이라고 하지만, 최적화를 할 수 있는 상황에서 "최적화"이지 , 최적화를 하지 않아도 되는 상황에서 Memo를 사용하면 오히려, 얕은 비교를 실행하여 불필요한 추가연산이 필요해집니다. 생각해보면, Memo "동작이 모든 환경에서의 최적화 동작"을 했다면, 그냥 렌더링의 모든 과정을 memo로 구현하면 되지, 왜 그러지 않았을까요!

하지만, 저희는 컴퓨터공학도! 입증하려면, React의 코드를 직접 까봐야합니다. 직접 까보는것도 정말 좋겠지만, 우선 저의 시간을 아껴주실 선생님을 모시도록 하겠습니다.

https://blog.isquaredsoftware.com/2022/10/presentations-react-rendering-behavior/

 

React Advanced 2022 - A (Brief) Guide to React Rendering Behavior

A shorter look at how React rendering actually works

blog.isquaredsoftware.com

렌더링 Issue

React가 렌더링을 다시하는 경우는 4가지로 정해져 있습니다. "React"는 컴포넌트에 대한 전권을 갖고있습니다. 즉, React가 모든 상황을 통제합니다. React에서는 정확하게는 "렌더링에서 제외되는 조건"이 정해져있지만 , "렌더링이 제외되는 조건"보다는 렌더링이 되는 조건이 조금 더 직관적인것 같습니다.

 

1. State가 변경이 되었을 때 

  • 즉, setState가 실행되면 해당 State가 존재하는 컴포넌트는 리렌더링 되어집니다.

2."새로운" Props가 들어올 때 

  • 부모 컴포넌트로부터, 새 props가 들어오면, 자식 컴포넌트는 재렌더링 됩니다. 

3. 기존 Props가 업데이트 되었을 때

  • 부모 컴포넌트로 받은, props가 변경되면 props 값을 받은 자식 컴포넌트도 재렌더링 됩니다.

4. 부모 컴포넌트가 리렌더링 될 때

  • 부모 컴포넌트가 재렌더링되면, 자식 컴포넌트도 모두 재렌더링 됩니다.

흔한 Counter 예시를 들어보겠습니다.

Counter .jsx

import React ,{useState} from "react"

function Inner(){
	console.log("Inner is Rendered!");
	return(
    	<div>
        	This is Inner!
        </div>
    )
}


function Counter(){
	const [count,setCount] = useState(0)
    console.log("Counter is Render"!)
    const handleCount = () =>{
    	setCount((prev)=>prev +1)
    }
	return(
    	<>
        	<button onClick={handleCount}></button>
        	<div>{count}</div>
            <Inner></Inner>
        </>
    )
}

버튼을 클릭하면 , 어떻게 될까요 ? 네 , 당연히 Counter가 리렌더링 되면서  , Inner가 리렌더링 됩니다.

왜냐하면, Counter 아래에 <Inner>이 있기 때문이죠! 그럼 , 이걸 어떻게 해결할 수 있을까요?  렌더링 되는 원인은 Inner가 Counter 속에 있기 때문입니다.  하지만, Rendering 위치는 변하면 안됩니다. Inner는 정확하게 {count} 아래에 있어야합니다.

 

우선, 가장 잘 알려진 Memo기능을 사용할 수 있겠네요! 

 

Child Component는 매번 바뀌지 않기 때문에, Memo 해놓고, 렌더링 시켜주면 됩니다.

import React, { useState } from "react";

function _Child() {
  console.log("Child is Render!");
  return (
    <>
      <div>Hello!</div>
    </>
  );
}
const Child = React.memo(_Child);

function Parent() {
  const [count, setCount] = useState(0);
  const handleClick = () => {
    setCount((prev) => prev + 1);
  };
  console.log("Parent is Render!");
  return (
    <>
      <button onClick={handleClick}>+1</button>
      <div>{count}</div>
      <Child />
    </>
  );
}

export default Parent;

 

네 잘되네요!

하지만, 리엑트는 이렇게 권장하고 있습니다.

상위 컴포넌트에서 컴포넌트를 분리하고, Props로써 던져주라고요! 저도, 이게 더 좋은것 같습니다. 더 "리엑트"스럽네요!

Index.js

function Inner() {
  console.log("Inner is Rendered!");
  return <div>This is Inner!</div>;
}

root.render(
  <StrictMode>
    {/*<Parent lastChild={<Child />} />*/}
    <Counter props ={<Inner/>}/>
  </StrictMode>
);

의도했던대로, 하위 컴포넌트가 리렌더링 되지않습니다. 이건, 새로운 Props가 전달되지 않죠! 알아둡시다.  컴포넌트는 매번 새롭게 할당되지 않습니다. 

컴포넌트 안에 컴포넌트를 생성하면 어떻게 될까요

function Parent(){
	function Child(){
    	return <div>Im Child!</div>
    }
    return <Child/>
}

Child는 Parent가 "렌더링"될 때, 새로운 컴포넌트가 생성이 되어지고, return ,즉  Render하는 부분에서 새로운 Component가 생성되어 넘어갑니다. 다른 말로하면 매번 새로운 Reference값이 넘어가는거죠. 이렇게되면, 아래에 있는 모든 DOM노드들은 새롭게 다시 그려지는 비효율이 발생합니다. HOC와 다릅니다 . HOC는 Component 객체를 넘기는거지, <Child>즉, 렌더를 하는게 아닙니다.

 

절대 Never , Ever 컴포넌트 안에 컴포넌트를 생성하는 일이 없어야 한다고 Mark아저씨는 이야기 합니다.

아래와 같이, 분리를 하는게 좋습니다.

function Child(){
	return<div>Hi</div>
}

function Parent(){
	return <Child/>
}

 

한가지 예시는 아쉬우니까,  TodoList에 뭔가를 추가하게 될 때 최적화를 생각해볼까요! (댓글이라고 생각해도 재밌겠네요.)

TodoList가 간단하게 있다고 하고, 2초마다 지속적으로 TodoList가 하나씩 추가되는 Interval을 주었다고 해봅시다.

 

TodoList.jsx

import React from "react";
import TodoItem from "./Todoitem";

const Todos = [
  { id: 1, doing: "wash" },
  { id: 2, doing: "Coffee" },
  { id: 3, doing: "Yes" },
  { id: 4, doing: "No" },
  { id: 5, doing: "Swimming" }
];

function TodoList() {
  const [Todo, setTodo] = React.useState(Todos);
  
  React.useEffect(() => {
    const IntervalId = setInterval(() => {
      setTodo((prev) => [
        ...prev,
        { id: `${prev.length + 1}`, doing: "newJob" }
      ]);
    }, 2000);
    return () => clearInterval(IntervalId);
  }, []);
  
  return (
    <div>
      {Todo.map((items) => (
        <TodoItem key={items.id} props={items.doing} />
      ))}
    </div>
  );
}

export default TodoList;

TodoItem .jsx

function TodoItem({ props }) {
  console.log("TodoItem is Render!");
  return (
    <>
      <div>{props}</div>
    </>
  );
}

export default TodoItem;

TodoItem은 상위 TodoItem.jsx의 state가 바뀌기 때문에, 리렌더링이 되고, 그에 따라서 자식인 TodoItem 아래의 모든 자식들이 ReRendering 됩니다. 렌더링 되는 횟수를 보시면 어마무시합니다.

어떻게 최적화 시킬 수 있을까요 ?  이럴 때도, HOC를 설계해서 분리하고 해서 최적화를 해줄 수 있습니다만, 생각보다 쉬운듯 어렵습니다. 저는 솔직히 아이디어가 떠오르지 않습니다.  그래서, 저는 Memo를 써주겠습니다.

이때, props로써 , 함수가 오는 상황이 있을 수 있습니다.

import React from "react";
function TodoItem({ props }) {
  console.log("TodoItem is Render!");
  return (
    <>
      <div>{props}</div>
    </>
  );
}

export default React.memo(TodoItem);

함수는 매번 렌더링 될 때마다, 새로운 ref를 생성하므로, Memo가 올바르게 동작하지 않습니다. 따라서, useCallback이라는 또 다른 Hook을 사용해서, 최적화 시키면 됩니다!