Render와 State 좀 더 알아보기

sucream·2023년 2월 2일
0

react

목록 보기
4/9
post-thumbnail

리액트의 렌더링

리액트는 많은 기능 중 우리가 만든 컴포넌트를 화면에 렌더링 하는 역할도 맡고 있다. 리액트가 컴포넌트를 렌더링하는 이유는 크게 두가지로 나눌 수 있는데, 컴포넌트의 초기 렌더링컴포넌트의 state 변화이다. 아래에서 좀 더 자세히 알아보자.

1. Initial render

당연히 맨처음 컴포넌트를 화면에 보여주기 위해서는 렌더링이 필요할 것이다. 잘 생각해 보면 초기 렌더링은 우리가 보통 신경쓰지 않게 되는 것 같다. 아래 코드를 확인해 보자.

// Image 컴포넌트
export default function Image() {
  return (
    <img
      src="https://i.imgur.com/ZF6s192.jpg"
      alt="'Floralis Genérica' by Eduardo Catalano: a gigantic metallic flower sculpture with reflective petals"
    />
  );
}
// index.js
import Image from "./Image.js";
import { createRoot } from "react-dom/client";

const root = createRoot(document.getElementById("root"));
root.render(<Image />); // 초기 렌더링하는 부분

위 코드에서 index.jsroot.render(<Image />); 부분을 보자. 이 부분을 통해 리액트는 루트 컴포넌트를 최초 렌더링하게 된다. 실제로 해당 render 함수를 호출하는 부분을 주석처리하면 컴포넌트가 렌더링되지 않는다.

※컴포넌트 렌더링은 재귀적이다.
우리는 지금까지 수많은 컴포넌트를 만들었고 컴포넌트 내부에는 다수의 자식 컴포넌트가 존재할 수 있음을 알고 있다. 만약 초기 렌더링 과정에 루트 컴포넌트가 자식 컴포넌트를 가진다면, 리액트는 루트 컴포넌트를 렌더링하기 위해 해당 자식 컴포넌트를 렌더링해야 할 것이다. 자식 컴포넌트 역시 또다른 자식을 가질 수 있으며, 최종 자식이 더이상 추가 자식 컴포넌트를 가지지 않을 때까지 재귀적으로 컴포넌트를 렌더링한다.
이러한 점은 장점이자 단점인데, 아래에서 단점에 대해 알아보자.

2. Re-renders when state updates

컴포넌트는 props 및 state를 가질 수 있다. 만약 컴포넌트의 state 혹은 부모/조상의 state가 setState 함수로인해 변경되면 재렌더링을 위한 트리거가 발동하고, 리액트는 해당 state를 참조하는 컴포넌트를 재렌더링한다. 여기서 문제가 발생한다. 아래 코드를 보자.

// Clock 자식 컴포넌트
export default function Clock({ time }) {
  return (
    <div>
      <h1>{time.toLocaleTimeString()}</h1>
      <div>
        <input />
      </div>
    </div>
  );
}
// 아무런 역할을 하지 않는 Other 자식 컴포넌트
const Other = () => {
  return <div>아무런 작업을 하지 않는 자식 컴포넌트</div>;
};

export default Other;
//부모 컴포넌트
import { useState } from "react";
import Clock from "./Clock";
import Other from "./Other";

export default function App() {
  const [time, setTime] = useState(new Date());

  const timeHandler = () => {
    setTime(new Date());
  };

  return (
    <div>
      <Clock time={time} />
      <button onClick={timeHandler}>갱신</button>
      <Other />
    </div>
  );
}

위 코드는 부모 컴포넌트에서 state를 만들고 해당 state를 자식 컴포넌트에 prop으로 넘긴다. 이후 자식 컴포넌트는 부모로부터 받은 prop을 화면에 렌더링한다.
이때 부모 컴포넌트의 갱신 버튼을 클릭하면 setTime 함수에 의해 time state가 변경되며 재렌더링이 발생한다.

리액트 크롬 확장을 이용해 재렌더링이 발생하는 영역을 하이라이트해 보면, 자식 컴포넌트와 부모 컴포넌트가 같이 재렌더링되는 것을 확인할 수 있다. 여기서 부모 컴포넌트, Clock 컴포넌트, Other 컴포넌트는 서로 다른 이유로 재렌더링됐다.

부모 컴포넌트
부모 컴포넌트는 자신이 가지고 있는 state가 갱신되었기 때문에 갱신되었다. 부모 컴포넌트가 렌더링되기 위해서는 자식 컴포넌트가 렌더링되어야 한다는 점을 기억하자.

Clock 자식 컴포넌트
Clock 컴포넌트는 prop으로 time을 받는다. 이 prop은 부모 컴포넌트에 의해 갱신되어 Clock 컴포넌트도 영향을 받아 다시 렌더링된다. 또한 부모 컴포넌트가 재렌더링되기 때문에 Clock 컴포넌트가 재렌더링된다고 볼 수도 있다.

Other 자식 컴포넌트
Other 컴포넌트는 부모 컴포넌트로부터 prop을 받지 않지만, 부모 컴포넌트가 재렌더링되며 자식컴포넌트들을 재렌더링하는 과정에 포함되어 같이 렌더링되었다. 즉 Other 컴포넌트의 재렌더링은 불필요한 렌더링이라는 뜻이다. 이러한 부분을 개선하는 다양한 방법이 있는 것 같다. 나중에 알아보도록 하자.

리액트는 실제로 변한 부분만 재렌더링한다.

위에서 부모 컴포넌트 및 자식 컴포넌트들이 재렌더링되는 것을 보았다. 그렇다면 만약 부모 컴포넌트에 있는 input에 엘리먼트에 입력을 하고 갱신 버튼을 클릭하면 input 값이 사라지지 않을까? 확인해 보자.

아니다. 사라지지 않고 남아있다. 왜일까?
그 이유는 컴포넌트가 재렌더링될 때 리액트가 해당 컴포넌트 내에서 렌더링 과정 사이에 자신이 관리하는 값들의 변화에만 관심이 있기 때문이다. 따라서 input은 리액트 입장에서 변화가 발생한 부분이 아니기 때문에 실제 DOM에 해당 내용이 반영되지 않는다. 자세한 내용은 더 공부가 필요할 것 같다.

State는 Snapshot으로 생각해 보기

우리가 만든 컴포넌트는 JSX를 반환한다. 그리고 특정 조건(state 갱신 등)에 의해 우리 컴포넌트는 재렌더링된다. 재렌더링되는 과정은 다음과 같이 생각해 볼 수 있다.

1. 리액트가 우리가 만든 함수(JSX를 반환하는 컴포넌트)를 호출
2. 우리 함수는 해당 순간의 스냅샷을 만들어내고 이를 리턴
3. 반환된 스냅샷을 기반으로 리액트가 렌더링

실제로 우리의 컴포넌트가 리액트에 의해 다시 호출될 떄, 리액트는 새로운 props 및 최신 state를 전달하여 컴포넌트가 최신 상태를 유지할 수 있도록 한다.
아래에서 재밌는 예제를 살펴보자.

import { useState } from 'react';

export default function Counter() {
  const [number, setNumber] = useState(0);
  
  const numberHandler = () => {
  	setNumber(number + 1);
    console.log(number);
  };

  return (
    <>
      <h1>{number}</h1>
      <button onClick={() => {
        numberHandler();
        numberHandler();
        numberHandler();
      }}>+3</button>
    </>
  )
}

위 컴포넌트는 number라는 state를 만들고 button의 onClicknumberHandler 함수를 3번 호출하고 있다. 버튼을 클릭할 때마다 3씩 증가하는가?
아니다. 1씩만 증가한다. 왜그럴까?

스냅샷
위 코드에서 numberHandler 함수 내부의 console.log 결과를 보면 3번 실행된 것을 확인할 수 있다. 이를 통해 일단 한번만 실행된게 아닌 것은 알았다. 그렇다면 무엇이 문제일까? 위에서 state를 스냅샷으로 이해해 보자고 했다. 리액트에 의해 컴포넌트가 한번 렌더링되면 그 순간의 스냅샷을 가지고 있을 것이다. 그래서 우리가 호출하는 setNumber(number + 1); 함수의 number는 0으로 시작한 상태면 3번 호출되어도 계속 0일 것이다. 즉, 3번의 setNumber는 현재 스냅샷의 number를 참조하여 계산했다는 것이다. 아주 주요한 부분이다.
결과적으로 우리는 다음과 같이 호출했다.

setNumber(0 + 1);
setNumber(0 + 1);
setNumber(0 + 1);

이전 state에 의존하여 state 갱신하기

그럼 어떻게 해야 우리가 원하는 대로 3씩 증가하게 만들 수 있을까? 방법은 생각보다 간단하다.

setState에 이전 값에 의존하는 함수를 전달하여 실행되도록 한다.

코드를 통해 확인해 보자.

import { useState } from 'react';

export default function Counter() {
  const [number, setNumber] = useState(0);

  return (
    <>
      <h1>{number}</h1>
      <button onClick={() => {
        setNumber(n => n + 1);
        setNumber(n => n + 1);
        setNumber(n => n + 1);
      }}>+3</button>
    </>
  )
}

차이점이 보이는가?
그렇다. setNumber()를 호출할 때 단순 값이 아닌 함수를 전달했다. 이 함수는 가장 최신 state값을 인자로 받아 반환하는 함수이다. 우리가 함수의 형태로 setState를 사용하면 리액트가 최신 state를 넘겨줄 것이다.

네이밍 컨벤션
일반적으로 함수의 첫대문자 글자들로 파라미터 명을 지정한다.

setEnabled(e => !e);
setLastName(ln => ln.reverse());
setFriendCount(fc => fc * 2);

좀 더 명확한 것을 선호한다면 다음과 같이 state명을 사용해도 된다.

setEnabled(enabled => !enabled);
setEnabled(prevEnabled => !prevEnabled);

Reference

profile
작은 오븐의 작은 빵

0개의 댓글