무지성 React - 리렌더링 막고 싶어요...

E4ger·2021년 7월 2일
7

무지성으로 습득해서 나름대로 뇌피셜을 적는 공간

굉장히 비 학습적인 게시글이며 무지하기 때문에 여기 있는 정보를 정보라고 생각하면 안됩니다. 재미로 보면서 이런 허접도 있구나 라고 웃으면서 보는 게 좋겠네요. 아래 내용들은
Only 뇌피셜입니다.

나는 React도 무지성으로 공부

지금 내가 express에서 Spring boot로 전환하고 있던 동아리 사이트의 프론트엔드는 React로 제작하였다. 제작할 당시 무지성 React의 지식 상태는 인프런의 클론 코딩 무료 강의들을 무지성으로 따라했었을 뿐이였다. 물론 강의를 들을 때의 지식 상태 즉 자바스크립트와 ES6 문법은 알지도 못했다. 처음에 무작정 콜백 함수, 화살표 함수를 따라치는데 대체 이게 뭔지 몰라서 화살표 함수만 무지성으로 이해하는데 오래걸렸다..

useState로 인한 리렌더링

쨋든 동아리 사이트의 프론트를 만들면서 여러가지 문제를 겪었다. 그런데 문제는 오류의 문제라기 보단 그냥 뭔가 이건 아닌듯한 사이트의 작동이였다.

예를 들어 이렇게 카드 아이템들이 나열되어있는 카드 리스트 컴포넌트가 있다고 한다.
이 카드를 누르면

이렇게 모달창이 나오게 했다. 그런데 이 모달창을 띄우기 위해 먼저 여러가지 State를 사용했다.

1. 카드를 누르면 모달창을 띄우기 위한 boolean값 True면 모달창 ON False면 모달창 OFF
2. 모달창 안에 들어갈 데이터들

그런데 문제가 있다 지금은 카드가 6개 7개 뿐이라 딱히 문제가 없어보이지만 내가 저 페이지를 무한스크롤로 구현하고자 해서 카드를 한 900개 정도 넣어두고 테스트를 했다.

아~ 무한 스크롤 잘되네! 하면서 모달창 한번 클릭하는 순간 모달창 열고 닫는데 버벅이더니 거의
0.5초가 소요되는 것 이였다...

그래서 나는 리액트 개발 도구를 켜서 관찰했더니 모달창을 열고 닫을 때와 모달창에 띄울 데이터를 담고 있는 State를 변경 할 때마다 페이지의 내용들이 리렌더링 되는 것이였다.
즉 모달창을 열고 닫을 때마다 다른 말로는 State값이 변경 될 때마다 카드 아이템 900개가계속 리렌더링 되는 것이였다.

사실 처음에는 납득이 가지 않았다. 이거 왜이래 버그야??

무지성으로 useState 관찰하기

사실 납득이 가지 않아서 무지성 구글 조사 결과 useState는 값이 변경 될 때마다 컴포넌트를
리렌더링 해준다는 점이였다. 근데 뭐 사실 useState를 처음 배울때 아 그렇구나~ 하고 넘어가긴 했는데 리렌더링의 양날의 검을 겪어보기 전까진 문제를 삼지 않았던게 엑시던트였던 것 같다.

import React, {useState} from "react";


function App(props) {
  console.log("렌더링 함");
  const [a, setA] = useState(true);

  const ChangeA = () => setA((prev) => !prev);

  return (
    <>
      <button onClick={ChangeA}>as</button>
    </>
  );
}

export default App;

간단하게 테스트 해보았다. as버튼을 누르면 a의 State 값이 반대로 바뀌도록 하였다
true면 false로 false면 true로


일단 만든 페이지에 가면 처음 App함수가 실행되어서 html을 반환한다 그러니 console.log가 찍히는게 당연하다. 이제 버튼을 눌러본다

as버튼 10번 눌렀더니 console.log가 10번 찍혔다. console.log 10번이 찍혔다는 건
App 함수가 10번 다시 실행되었다는 뜻이다. 이런 엑시던트다
이것이 useState가 변경 된 값을 리렌더링 해주는 양날의 검이였다. 만약 과장해서 App 함수가 1분걸려서 걸려서 렌더링 되는 함수라면 10번이면 10분이다. 참고로 예전에 봤던 조사결과가 있는데 페이지 로딩이 1초이상 걸리면 사용자들은 불편해한다고 한다ㅋㅋ

참 무지성 학습법의 문제가 여기서 나온 것이다. useState의 작동 원리도 모르고 무지성으로 했더니 이걸 깨닫는데 좀 오래걸렸다... 그런데 사실 이것만 보고 리렌더링 되는게 문제가 있으려나 ㅋ 했다.

다시 생각해보면 이 카드가 900개라면 900개의 카드 아이템들이 리렌더링 되는 것이다. 절대 안된다. 컴퓨터 과부하 올 것 같다.

무지성으로 분할 아닌 컴포넌트 분할

useState 양날의 검 리렌더링으로 인해 내가 해맸던 점이 있었다.

일단 나는 어디서 주워들은건 있어서 컴포넌트를 분할하면 좋다는 것을 들었었다. (재사용 가능한 컴포넌트 ㅇㅇ) 근데 나는 그냥 있어보여서 무작정 분할했다. 그런데 분할 아닌 분할을 했었다.

일단 예를 들어서 유튜브 퍼가기를 하면 얻을 수 있는 iframe을 분할 해서 렌더링 해주겠다.

컴포넌트 안에 컴포넌트?

사실 이게 분할이라고 하기엔 애매한 느낌이다.

import React, { useState } from "react";

function App(props) {
  console.log("렌더링 함");
  const [a, setA] = useState(true);
  const ChangeA = () => setA((prev) => !prev);

  const RenderYoutube = () => (
    <iframe
      width="560"
      height="315"
      src="https://www.youtube.com/embed/x1miJONn-VM"
      title="YouTube video player"
      frameborder="0"
      allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
      allowfullscreen
    ></iframe>
  );

  return (
    <>
      <button onClick={ChangeA}>as</button>
      <RenderYoutube />
    </>
  );
}

export default App;

RenderYoutube라는 함수형 컴포넌트에서 유튜브 퍼가기를 렌더링 해준다.

그런데 잘보면 App 함수안에 넣어놨다. 사실 아직까지도 이걸 컴포넌트 분할이라고 할 수 있는가에 대한 내 생각은 결과적으로 분할이 아닌 것 같긴하다.
쨋든 나는 지금까지 이렇게 했다. 왜냐면 저 RenderYoutube 함수를 App함수 밖으로 빼서 함수를 정의하면 그 함수안에서는 App함수의 State를 변경하는게 가능할까 싶어서다.

쨋든 이 구조대로 코딩하고 이제 버튼을 클릭해서 state를 변경해보겠다.

이런... useState의 리렌더링 양날의 검을 제대로 느끼게 되었다. App 함수가 다시 실행되면서 RenderYoutube의 함수형 컴포넌트까지 다시 실행되는 것이다. 이건 굉장히 좋지 않다.
근데 내가 말했듯이 내가 컴포넌트 분할이라고 하면서 App 함수안에 또 다른 컴포넌트 함수를 넣는 행위는 아무리 생각해도 분할이 아니다. 그냥 RenderYoutube함수를 App 함수 밖으로 빼보겠다.

import React, { useState } from "react";

const RenderYoutube = () => (
  <iframe
    width="560"
    height="315"
    src="https://www.youtube.com/embed/x1miJONn-VM"
    title="YouTube video player"
    frameborder="0"
    allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
    allowfullscreen
  ></iframe>
);

function App(props) {
  console.log("렌더링 함");
  const [a, setA] = useState(true);
  const ChangeA = () => setA((prev) => !prev);
  return (
    <>
      <button onClick={ChangeA}>as</button>
      <RenderYoutube />
    </>
  );
}

export default App;

이렇게 RenderYoutube함수를 밖에서 정의하였다. 이 상태에서 한번 다시 State를 바꿔보자

오~ 버튼을 눌러도 깜빡거리는 게 없다! 리렌더링 현상이 사라진 것일까?

React에서 리렌더링이 어떻게 보이는 것일까?

사실 나는 저 유튜브 관련 리렌더링 때문에 많은 의문이 있었다. 과연 깜박거리지 않았으니 리렌더링이 되지 않은것이 아닐까?

사실 리렌더링이란게 아직까지도 잘 모르겠다. 사실 결론은 깜빡거리지 않았지만 리렌더링이 된 것이라고 본다. RenderYoutube 함수안에 console.log를 찍자...

const RenderYoutube = () => {
  console.log("RenderYoutube 실행");
  return (
    <iframe
      width="560"
      height="315"
      src="https://www.youtube.com/embed/x1miJONn-VM"
      title="YouTube video player"
      frameborder="0"
      allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
      allowfullscreen
    ></iframe>
  );
};

iframe을 리턴해주기 전에 console.log만 추가해보고 실행해보겠다.

ㅋㅋㅋㅋ 리렌더링 되고 있었다. 이제부터 이에 대한 내 뇌피셜을 말해보겠다.

사실 리렌더링이 필요한 이유는 변화하는 값을 다시 그려주기 위해서라고 생각할 수 있다.
그래서 사실 우리가 눈에 보이는 것은 바뀌는 값만 바뀌어서 보이는 것이다.
그런데 RenderYoutube함수가 반환해주는 html 요소는

<iframe
      width="560"
      height="315"
      src="https://www.youtube.com/embed/x1miJONn-VM"
      title="YouTube video player"
      frameborder="0"
      allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
      allowfullscreen>
</iframe>

이렇게 고정적인 값이다. 즉 RenderYoutube 함수가 아무리 100번 1000번 10000번 실행되도 같은 결과를 준다. 따라서 몇번을 실행해도 바뀌는 것이 없다. 따라서 App함수 안에서
State가 변경되어 RenderYoutube 함수를 다시 실행해 iframe을 반환받아 온다 한들
바뀌는 값이 없기 때문에 우리는 뭐가 작동되고 있는지 느낄 수가 없다. 하지만
RenderYoutube함수를 계속 호출하면서 iframe을 받아 오고 있다는 점은 알아둬야 할 것이다.

그렇다 일단 여기까지 이해했다고 치자. 그럼 App함수안에서 RenderYoutube함수를 실행했을 때는 왜 그렇게 깜빡이였을까?라는 의문을 가지게 되었다.

import React, { useState } from "react";

function App(props) {
  console.log("렌더링 함");
  const [a, setA] = useState(true);
  const ChangeA = () => setA((prev) => !prev);
  const RenderYoutube = () => {
    console.log("RenderYoutube 실행");
    return (
      <iframe
        width="560"
        height="315"
        src="https://www.youtube.com/embed/x1miJONn-VM"
        title="YouTube video player"
        frameborder="0"
        allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
        allowfullscreen
      ></iframe>
    );
  };
  return (
    <>
      <button onClick={ChangeA}>as</button>
      <RenderYoutube />
    </>
  );
}

export default App;

일단 아까 보았듯이 App함수 안에 State가 변경되면 App함수는 다시 실행된다고 했다.
그런데 대체 왜 App함수 안에 있는 RenderYoutube함수는 똑같은 iframe을 반환해주는데
왜 why? 깜빡이면서 나 리렌더링 되고 있어요~ 라는 것을 보여주는 것일까?

일단 다시 뇌피셜을 말해보겠다.

먼저 계속 말했듯이 App함수 안에 State가 변경될 때 마다 App함수가 다시 실행된다고 했다.
App 함수가 다시 실행되면서 RenderYoutube함수를 실행하여 유튜브 화면을 렌더링 해주는 것인데
이걸 약간 감히 운영체제 관점에서 보겠다. 참고로 운영체제 이거 잘 알지도 못한다. 하지만 전공과목에서 얇은 귀로 들었던 무지한 지식과 흘려들었던 리액트 지식을 인용하겠다.
나는 깜빡이는 이유 중 하나는 RenderYoutube함수가 다시 생성되기 때문이라고 생각한다.
App함수가 다시 실행될 때마다 RenderYoutube 함수가 새로운 메모리 공간에 생성될 것이라고 생각한다. 새로운 공간은 아니더라도 적어도 함수가 새로 생성되는 것은 맞을 것이다. 라고 믿고싶다. 근거는 RenderYoutube함수를 밖으로 빼면 적어도 App함수는 다시 실행되어 안에 있는 함수들이 다시 생성되지만 밖에 있는 RenderYoutube함수는 그냥 가만히 있을 것이기 때문이다.
그래서 App안에 있다면 함수를 새로 생성하는 순간? 찰나? 또는 그냥 근본적으로는 함수를 다시 생성하고 iframe을 반환받아야하기 때문에 깜빡이는 것이라고 생각한다.

useState로 인한 리렌더링 막아보기

그럼 위에서 내가 겪었던 리렌더링과 함수 재생성 문제를 한번 해결해보려고 무지성 구글 학습을 했다.

1. React.memo로 자식 컴포넌트 렌더링 막기

import React, { useState } from "react";
const RenderYoutube = () => {
  console.log("RenderYoutube 실행");
  return (
    <iframe
      width="560"
      height="315"
      src="https://www.youtube.com/embed/x1miJONn-VM"
      title="YouTube video player"
      frameborder="0"
      allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
      allowfullscreen
    ></iframe>
  );
};
function App(props) {
  console.log("렌더링 함");
  const [a, setA] = useState(true);
  const ChangeA = () => setA((prev) => !prev);
  return (
    <>
      <button onClick={ChangeA}>as</button>
      <RenderYoutube />
    </>
  );
}

export default App;

먼저 이 코드가 있다고 가정하자. RenderYoutube 함수는 App함수 밖에 있다.
그리고 App함수는 RenderYoutube 함수를 통해 유튜브 관련 html을 반환받아 렌더링 한다.
이 상태에서 버튼을 눌러 App함수 내부에 있는 State가 바뀐다면 아까처럼...

RenderYoutube함수가 실행된다. 결국 RenderYoutube 함수가 계속 호출되고 있다는 것인데
이 함수가 오래걸리는 함수라면? 그리고 애초에 나는 App함수의 State를 바꾸고 있었는데 App함수가 리렌더링 되어 직접적으로 관련없는 RenderYoutube함수가 계속 호출되고 있다는 점이다.
그럼 App함수의 State가 바뀌었을 때 RenderYoutube 함수가 호출되지 않도록 하려면

React.memo를 사용하면 된다고 한다. 간략히 말하자면 부모 컴포넌트로 부터 받는 props가 바뀌지 않는다면 함수 호출을 다시 하지 않는다.

const RenderYoutube = React.memo(function RenderYoutube(props) {
  console.log("RenderYoutube 실행");
  return (
    <iframe
      width="560"
      height="315"
      src="https://www.youtube.com/embed/x1miJONn-VM"
      title="YouTube video player"
      frameborder="0"
      allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
      allowfullscreen
    ></iframe>
  );
});

기존의 RenderYoutube함수를 React.memo로 감싸준다. 말 그대로 기억한다는 느낌이다.

일단 이렇게 바꾸고 실행해보자

오~ 이제 페이지 첫 로딩 시에만 App과 RenderYoutube 함수가 실행되고 버튼을 누르면 App만 리렌더링 되면서 관계없던 RenderYoutube함수는 실행되지 않는다.

자 그렇다 다시 뇌피셜을 정리해보자면 React.memo는 약간 자식 컴포넌트가 필요할 때만 리렌더링 되도록(필요할 때만 함수를 호출해서 html요소를 렌더링 시켜주도록) 관리해주는 함수 인 것 같다. 그럼 필요할 때만이라고 했는데 필요할 때만은 그 순간은 언제 캐치될 까 라고 하면 아까 말했듯이 부모가 주는 props의 변화를 감지하여 리렌더링 해준다.

import React, { useState } from "react";

const ShowNumber = React.memo(function ShowNumber(props) {
  console.log("ShowNumber 실행");
  return <>{props.bool}</>;
});

function App(props) {
  console.log("App 렌더링 함");
  const [a, setA] = useState(0);
  const [b, setB] = useState(0);
  const ChangeA = () => setA((prev) => prev + 1);
  const ChangeB = () => setB((prev) => prev + 1);
  return (
    <>
      <button onClick={ChangeA}>a값 변경</button>
      <button onClick={ChangeB}>b값 변경</button>
      <ShowNumber bool={b} />
    </>
  );
}

export default App;

간략하게 테스트 코드이다. App함수에서는 a와 b라는 state가 있다.
a값 변경이라는 버튼을 누르면 a의 값이 + 1, b값 변경이라는 버튼을 누르면 b의 값이 +1
그리고 ShowNumber는 부모 컴포넌트로 부터 값을 받아 그 값을 출력해준다.

우리는 a값을 변경해도 React.memo를 사용했기 때문에 관련 없는 ShowNumber라는 함수가 실행되지는 않을 것이다.

그런데 잘보면 App함수에서 ShowNumber라는 컴포넌트를 호출할 때 props로 bool이라는걸 넘겨준다.
그리고 그 넘겨주는 값은 App함수에서 사용되는 b state이다.
근데 아까 말했듯이 React.memo는 부모가 넘겨주는 props값이 변할 때만 리렌더링 된다고 했다.
따라서 b값 변경 버튼을 눌러 b의 state가 바뀐다면 ShowNumber 입장에서는 부모가 전달해준
props가 바뀌었기 때문에 리렌더링이 될 것이다. 한번 확인해보자.

a의 값을 변경할 때는 ShowNumber 함수가 호출되지 않지만 b의 값이 변경되면 ShowNumber 입장에서 props가 변경되었으므로 다시 호출되어 바뀐 값을 리렌더링 해준다.
이 React.memo는 변경된 State와 관련없는 컴포넌트들의 불필요한 리렌더링을 막을 때 사용하면 될 것 같다.

2. useCallback으로 함수 재사용?

import React, { useState } from "react";

function App(props) {
  console.log("렌더링 함");
  const [a, setA] = useState(true);
  const ChangeA = () => setA((prev) => !prev);
  const RenderYoutube = () => {
    console.log("RenderYoutube 실행");
    return (
      <iframe
        width="560"
        height="315"
        src="https://www.youtube.com/embed/x1miJONn-VM"
        title="YouTube video player"
        frameborder="0"
        allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
        allowfullscreen
      ></iframe>
    );
  };
  return (
    <>
      <button onClick={ChangeA}>as</button>
      <RenderYoutube />
    </>
  );
}

export default App;

위에서 나는 App함수안에 RenderYoutube함수를 분할아닌 분할이라며 넣어두었다.
그리고 App함수 안에서 State가 변경될 때 마다 App함수가 다시 실행되면서 RenderYoutube함수도 새롭게 만들어졌다. 이로인해 깜빡 거렸었다.

만약 State가 바뀔 때마다 새로 생성되지 않도록 RenderYoutube함수를 요리한다면 좋지 않을까? 라는 생각이 든다.

구글링을 통해 함수의 재사용 useCallback을 찾게 되었다.

import React, { useState, useCallback } from "react";

function App(props) {
  console.log("렌더링 함");
  const [a, setA] = useState(true);
  const ChangeA = () => setA((prev) => !prev);
  const RenderYoutube = () => {
    console.log("RenderYoutube 실행");
    return (
      <iframe
        width="560"
        height="315"
        src="https://www.youtube.com/embed/x1miJONn-VM"
        title="YouTube video player"
        frameborder="0"
        allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
        allowfullscreen
      ></iframe>
    );
  };
  const FixRenderYoutube = useCallback(() => RenderYoutube(), []);
  return (
    <>
      <button onClick={ChangeA}>as</button>
      <FixRenderYoutube />
    </>
  );
}

export default App;
const FixRenderYoutube = useCallback(() => RenderYoutube(), []);

<FixRenderYoutube />

중간에 나는 useCallback을 사용하여 RenderYoutube함수를 FixRenderYoutube라는 변수에 고정시켰다. 대강 사용법을 보니 첫번째에는 고정 시킬 함수의 내용을 넣는 것 같았다. 나는 일단 간단히 단순 RenderYoutube가 다시 재생성되는 것을 막을 것이니 그냥 RenderYoutube()만 넣었다. 그리고 두번째로는 빈 객체 []가 있는데 useEffect랑 비슷하다. []가 아닌 변수가 들어간다면 그 변수가 바뀔 때 함수가 재생성 된다. 빈객체를 넣는다면 아무리 무슨짓을 해도 RenderYoutube함수의 작동이 바뀌지 않으므로 쭉 함수는 재생성 되지 않는다.

따라서 절대 재생성되지 않는 FixRenderYoutube로 컴포넌트로 사용하여 State를 바꿔보자

이제 App함수가 리렌더링 된다고 해도 RenderYoutube함수는 useCallback함수를 통해 FixRenderYoutube에 고정시켜놨으므로 깜빡거리지 않는다!!

하지만 함수 호출은 계속 된다. 그럼 그냥 React.memo를 포함해서 useCallback으로 저장하면 되지 않을까?

import React, { useState, useCallback } from "react";

function App(props) {
  console.log("렌더링 함");
  const [a, setA] = useState(true);
  const ChangeA = () => setA((prev) => !prev);
  const RenderYoutube = React.memo(function RenderYoutube() {
    console.log("RenderYoutube 실행");
    return (
      <iframe
        width="560"
        height="315"
        src="https://www.youtube.com/embed/x1miJONn-VM"
        title="YouTube video player"
        frameborder="0"
        allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
        allowfullscreen
      ></iframe>
    );
  });
  const FixRenderYoutube = useCallback(() => RenderYoutube(), []);
  return (
    <>
      <button onClick={ChangeA}>as</button>
      <FixRenderYoutube />
    </>
  );
}

export default App;

그렇다 지금 RenderYoutube가 함수로 인식이 안된다는데 React.memo는 생각해보니 컴포넌트 재사용이였고 "함수형" 컴포넌트지만 결국 함수가 아니라고 봐야하나보다. 이건 잘 모르겠다.

그럼 만약 나처럼 App함수 안에 특정 컴포넌트를 반환하는 함수가 있다면 그리고 이 컴포넌트를 필요할때만 리렌더링 시켜주려면 어떻게 해야할까...

import React, { useState, useCallback } from "react";

function App(props) {
  console.log("렌더링 함");
  const [a, setA] = useState(true);
  const ChangeA = () => setA((prev) => !prev);
  const RenderYoutube = function RenderYoutube() {
    console.log("RenderYoutube 실행");
    return (
      <iframe
        width="560"
        height="315"
        src="https://www.youtube.com/embed/x1miJONn-VM"
        title="YouTube video player"
        frameborder="0"
        allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
        allowfullscreen
      ></iframe>
    );
  };

  const FixRenderYoutube = useCallback(
    React.memo(() => RenderYoutube()),
    []
  );
  return (
    <>
      <button onClick={ChangeA}>as</button>
      <FixRenderYoutube />
    </>
  );
}

export default App;

useCallback함수로 고정시킬 함수 내용을 작성할 때 그냥 React.memo를 사용해서 반환해보았는데

;; 일단은 RenderYoutube함수가 실행되지 않았다. 그런데 이건 그냥 내 개인적인 궁금함 때문이지 사실 이런 컴포넌트 구조는 좋지 않은 것 같다. 일단 좀더 공부해보고 무지성 지식 습득을 좀더 해야겠다

3개의 댓글

comment-user-thumbnail
2021년 10월 20일

잘 배우고 갑니다 ^^7

답글 달기
comment-user-thumbnail
2022년 6월 12일

너무 웃기고 유익하네요 ㅋㅋㅋ

답글 달기
comment-user-thumbnail
2024년 2월 21일

Maintaining organization is key to minimizing errors, understanding their origins, and managing their impacts effectively. By implementing these practices, aspiring developers can emulate the proficiency of Naver developer Muji React, aiming to optimize performance by minimizing unnecessary re-rendering.
mysainsburys login portal

답글 달기