[ React ] 컴포넌트 재 렌더링 조건

exceed_96·2024년 3월 1일
0

React

목록 보기
11/18
post-thumbnail

React 프로젝트를 진행하다보면 원치 않은 컴포넌트가 재 렌더링 일어나거나 심할 경우 상상하기도 싫은 무한 재 렌더링이 발생할 수 있다. 재 랜더링이 발생하는 조건들이 다양하게 있는데 이번 포스팅에서는 React의 컴포넌트가 재 렌더링 일어날 조건들과 이에 대한 해결방법을 알아보자.



1. React의 재 렌더링 조건

React에는 대표적으로 4가지 조건에 의해서 재 렌더링이 발생한다.

컴포넌트의 상태(State)변경이 발생할 때

상위 컴포넌트에서 하위컴포넌트로 새로운 props가 전달 될 때

기존에 가지고 있던 props가 업데이트 될 때

부모 컴포넌트가 재 렌더링 될 때


컴포넌트의 상태(State)변경이 발생할 때

컴포넌트의 상태(state)가 변경된다면 해당 컴포넌트는 재 렌더링이 발생한다.

이는 전역상태 관리 라이브러리인 zustand, recoil, redux와 같은 라이브러리를 이용해도 동일하게 동작한다.


상위 컴포넌트에서 하위 컴포넌트로 새로운 props가 전달 될 때

상위 컴포넌트에서 하위 컴포넌트로 새로운 props가 전달된다면 해당 props를 받은 하위 컴포넌트는 재 렌더링이 발생한다.


기존에 가지고 있던 props가 업데이트 될 때

하위 컴포넌트에서 가지고 있던 props가 상위 컴포넌트에 의해서 업데이트가 일어난다면 해당 props를 가지고 있는 하위 컴포넌트는 재 렌더링이 발생한다.


부모 컴포넌트가 재 렌더링 될 때

부모 컴포넌트가 위 조건들에 의해서 재 렌더링이 발생한다면 부모 컴포넌트와 연결된 자식 컴포넌트들은 싹 다 재 렌더링이 발생한다.


위 4가지 조건들은 프로젝트가 커지면 커질수록 컴포넌트의 복잡성도 증가하기 때문에 해당 조건들이 의도치 않게 트리거 될 확률이 매우 높아진다.

재 렌더링이 과도하게 발생하는 경우 성능 저하가 발생하게 된다.

재 렌더링이 심하면 흔히 말하는 버벅이는 현상이 발생할수도 있고 useEffect훅으로 사이드 이펙트(특히 서버통신)를 정의한 컴포넌트의 무한 재 렌더링이 일어나는 경우.... 어떤일이 일어날지는 상상에 맡기겠다...

따라서 이를 위해 React에서는 의도치 않은 재 렌더링을 막아주기 위한 훅과 메서드가 존재한다.



2. 재 렌더링을 막기 위한 방법

2.1 memo

memo메서드는 함수형 컴포넌트에만 사용할 수 있는 메서드이다.


memo는 인자로 들어간 컴포넌트에 어떤 props가 입력되었는지 확인하고 입력되는 모든 props의 신규값을 확인한 뒤 이를 기존의 props의 값과 비교하도록 React에게 전달한다.

그리고 props가 변경된 경우에만 컴포넌트를 재 평가한다.

App

import { useState } from "react";
import "./App.css";
import Output from "./Components/Output";
import { Reset } from "styled-reset";

function App() {
  const [show, setShow] = useState<boolean>(false);

  const showDataHandler = () => {
    setShow((prevState) => !prevState);
  };

  console.log("App Component Rendering!!!!");
  return (
    <div className="App">
      <Reset />
      <Output outputDataFlag={true} />
      <button onClick={showDataHandler}>App Button</button>
    </div>
  );
}

export default App;

Output Component

import React from "react";
import styled from "styled-components";

interface OutputProps {
  outputDataFlag: boolean;
}

const Title = styled.p`
  width: 100px;
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
`;

const Output = (props: OutputProps): JSX.Element => {
  console.log("Output Component Rendering....");

  return <>{props.outputDataFlag && <Title>Hello World!</Title>}</>;
};

export default React.memo(Output);

위와같이 부모 컴포넌트인 "App"에서 자식 컴포넌트 "Output"에게 props를 고정값으로 전달해주니 "Output"컴포넌트의 재 렌더링이 발생하지 않는걸 확인할 수 있다.

그럼 props의 값을 고정 값이 아닌 변화하는 값으로 넣어주면 어떻게 될까?

이를 위해 "App"컴포넌트의 "outputDataFlag"값을 "show"로 변경해보자.


위와같이 props의 변화가 생길때마다 자식 컴포넌트인 "output"컴포넌트가 재 렌더링 하는걸 확인할 수 있다.


그럼 여기서 하나의 질문이 생긴다.

memo로 최적화가 가능하다면 모든 컴포넌트에 최적화를 하는게 좋지 않을까?

정답은 NO!!이다.

최적화는 비용이 따르기 때문에 모든 컴포넌트에 최적화를 해주는건 좋지 않은 선택이다.

따라서 정말 필요한 부분에만 최적화를 해주는게 좋다.

예를 들면 하위컴포넌트가 많은 상위컴포넌트일 경우 실행비용이 더 많이 들기 때문에 상위 컴포넌트에 memo로 최적화를 해주는게 좋다.

다만 memo도 만능은 아니다.

import { useState } from "react";
import "./App.css";
import Output from "./Components/Output";
import { Reset } from "styled-reset";

function App() {
  const [show, setShow] = useState<boolean>(false);

  const showDataHandler = () => {
    setShow((prevState) => !prevState);
  };

  console.log("App Component Rendering!!!!");
  return (
    <div className="App">
      <Reset />
      <Output outputDataFlag={true} buttonHandler={showDataHandler} />
      <button onClick={showDataHandler}>show ChildComponent</button>
    </div>
  );
}

export default App;

위 부모 컴포넌트에서 "showDataHandler"함수를 "Output"컴포넌트의 props로 전달하면 어떻게 될까?

지금까지 배운 지식으로는 하위 컴포넌트인 "Output"을 memo를 사용하였으니 재 렌더링이 발생하지 않을거로 예상할 수 있다.

하지만 결과는 전혀 다르다.

위와같이 props로 전달된 함수는 전혀 변화가 없지만 하위 컴포넌트인 "Output"은 재 렌더링이 발생하는걸 확인할 수 있다.

왜 이런 결과가 나온걸까?

React는 렌더링 되는 모든 함수나 변수를 재사용하는게 아닌 같은 기능을 하는 새로운 함수를 만드는 것이다.

즉, 우리 눈에는 함수가 사라지지 않고 계속 존재하고 값만 바뀌는거 같지만 React에서는 렌더링 하거나 값에 변화가 있을때마다 매번 로직을 다시 빠르게 그려내는 것이다.

JavaScript에서 함수는 하나의 객체에 불가하다.

즉, 이전 상태와 현재 상태가 같은 내용을 가지고 있다고 해도 JavaScript에서 이 둘을 비교하면 결코 동일하지 않다.

그렇기 때문에 "App"컴포넌트에서 전달해준 "showDataHandler" props가 우리 눈에는 같아보여도 JavaScript입장에서는 다른 거라고 인식해서 컴포넌트가 재평가 되는 것이다.

이를 위해 React에서는 객체에 대한 재평가를 방지할 수 있는 훅이 하나 존재한다.



2.2 useCallback

useCallback훅은 React내부 공간(메모리)에 함수를 저장하여 매번 랜더링 때마다 이 함수를 재 생성할 필요가 없다고 알려주는 훅이다.

이는 메모리의 동일한 위치에 저장되므로 이를 통해 비교작업을 할 수 있는 것이다.


먼저 useCallback훅을 import해준 후 React의 메모리에 저장하려는 함수를 useCallback으로 감싸줘서 첫번째 인수로 지정해야 한다.

useCallback함수는 저장된 함수를 반환해준다.

그리고 "App"컴포넌트가 다시 실행된다면 useCallback이 React가 메모리에 저장한 함수를 찾아서 같은 함수 객체를 재사용한다.

두번째 인수에는 배열이 들어간다.

useEffec와 마찬가지로 의존성배열이다.

의존성 배열에는 state, props 등을 지정할 수 있다.


App Component

import { useState, useCallback } from "react";
import "./App.css";
import Output from "./Components/Output";
import { Reset } from "styled-reset";

function App() {
  const [show, setShow] = useState<boolean>(false);

  const showDataHandler = useCallback(() => {
    setShow((prevState) => !prevState);
  }, []);

  console.log("App Component Rendering!!!!");
  return (
    <div className="App">
      <Reset />
      <Output outputDataFlag={true} buttonHandler={showDataHandler} />
      <button onClick={showDataHandler}>App Button</button>
    </div>
  );
}

export default App;

Output Component

import React from "react";
import styled from "styled-components";

interface OutputProps {
  outputDataFlag: boolean;
  buttonHandler: () => void;
}

const Title = styled.p`
  width: 100px;
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
`;

const Output = (props: OutputProps): JSX.Element => {
  console.log("Output Component Rendering....");

  return (
    <>
      {props.outputDataFlag && <Title>Hello World!</Title>}
      <button onClick={props.buttonHandler}>Output Button</button>
    </>
  );
};

export default React.memo(Output);

위와같이 props로 전달하는 함수를 useCallback훅으로 감싸줘서 메모리에 캐싱하여 재 생성을 막아주니 "Output"컴포넌트의 재 렌더링이 발생하지 않는걸 확인할 수 있다.

그럼 useCallback훅을 제거해주면 어떻게 될까?

위와같이 함수가 렌더링 할 때마다 재 생성하므로 이에 따른 자식컴포넌트도 재 렌더링이 발생하는걸 확인할 수 있다.



2.3 useMemo

useMemo훅은 컴포넌트의 재 렌더링 보다는 성능의 최적화를 향상 시켜줄 수 있는 훅이다.

useCallback은 부모 컴포넌트의 함수를 자식 컴포넌트에게 전달할 때 해당 함수를 메모리에 캐싱하여 재생성되는걸 방지해 재 렌더링을 방지해주는 것이지만 useMemo는 부모 컴포넌트한테 자식 컴포넌트가 받은 props의 값의 변화 유무에 따라서 특정 로직을 실행시키거나 혹은 실행시키지 않을 수 있다.

const value = useMemo(() => {}, [])

useMemo훅은 첫번째 인수로 콜백함수를 취하고 두번째 인자로 의존성 배열이 정의된다.

의존성 배열에 있는 값의 변화가 생긴다면 콜백함수를 실행시키고 해당 콜백함수의 반환값을 useMemo의 반환값으로 정의한다.


App Component

import { useState, useCallback } from "react";
import "./App.css";
import Output from "./Components/Output";
import { Reset } from "styled-reset";

function App() {
  const [show, setShow] = useState<boolean>(false);

  const showDataHandler = useCallback(() => {
    setShow((prevState) => !prevState);
  }, []);

  console.log("App Component Rendering!!!!");
  return (
    <div className="App">
      <Reset />
      <Output outputDataFlag={true} buttonHandler={showDataHandler} />
      <button onClick={showDataHandler}>App Button</button>
    </div>
  );
}

export default App;

Output Component

import React, { useMemo } from "react";
import styled from "styled-components";

interface OutputProps {
  outputDataFlag: boolean;
  buttonHandler: () => void;
}

const Title = styled.p`
  width: 100px;
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
`;

const Output = (props: OutputProps): JSX.Element => {
  const result = useMemo(() => {
    console.log("Output Component Rendering....");
    return "useMemo callback Function active";
  }, [props.outputDataFlag]);

  console.log(result);

  return (
    <>
      {props.outputDataFlag && <Title>Hello World!</Title>}
      <button onClick={props.buttonHandler}>Output Button</button>
    </>
  );
};

export default React.memo(Output);

위 예시에서 "Output"컴포넌트의 console.log출력 함수를 useMemo훅으로 감싸주고 의존성 배열에 "props.outputDataFlag"를 정의하면 "props.outputDataFlag"값의 변화가 있을 때만 useMemo훅의 콜백함수가 동작하고 해당 콜백함수가 반환하는 값은 "result"변수에 초기화가 될 것이다.

위와같이 "outputDataFlag"값을 "true"로 고정할시에는 useMemo훅으로 감싼 로직이 동작하지 않는걸 확인할 수 있다. 그럼 "outputDataFlag"값을 고정값이 아닌 변화하는 값으로 주면 어떻게 동작할까?

이를 위해 "outputDataFlag"값을 "true"가 아닌 "show"로 바꿔보자.

위와같이 "outputDataFlag"의 값은 고정 값이 아닌 변화 값이므로 useMemo훅 내부의 콜백함수가 동작하고 반환한 값은 "result"변수에 정의된 걸 확인할 수 있다.


이처럼 useMemo훅은 애플리케이션을 만들 때 성능이 너무 안좋거나 느리면 불필요한 연산을 줄여주는 역할을 하여 최적화 부분을 신경써줄 수 있는 훅이다.



3. 마치며

이번 포스팅에서는 React가 재 렌더링을 발생시키는 조건과 이에 따른 최적화 방법에 대해서 알아봤다.

React로 프로젝트를 하다보면 재 렌더링 관련한 문제는 빠질 수 없는 문제라고 생각한다.

그만큼 신경써줘야 하는 부분도 광범위 하고 프로젝트가 커질수록 어떤 트리거에 의해서 재 렌더링이 일어나는지 찾는일도 쉽지 않다.

위 3가지 방법이 모든걸 해결해 줄 순 없고 남용해서는 안 될 훅과 메서드이긴 하지만 프로젝트를 진행시에 필요하다고 생각하는 부분은 꼭 사용하여서 재 렌더링의 지옥에서 조금은 벗어나길 바란다.



4. Reference

https://www.udemy.com/course/best-react/learn/lecture/36543902#overview

profile
개발진행형

0개의 댓글

관련 채용 정보