학습 자료

렌더링과 리렌더링


Article

지금까지 "state가 바뀌면 React가 다시 그린다"고 했다. 편의상 그렇게 말했을 뿐 정확하지 않다.

렌더링은 화면을 그리는 일이 아니다. 컴포넌트 함수를 실행하는 일이다.

세 단계로 나뉜다

React가 화면을 갱신하는 과정은 셋으로 나뉜다.

트리거 → 렌더 → 커밋
  1. 트리거다시 그릴 이유가 생긴다
  2. 렌더컴포넌트 함수를 실행한다
  3. 커밋실제 DOM 을 고친다
단계무슨 일이누가
트리거setState 호출 또는 부모의 리렌더우리 코드
렌더함수를 실행해 JSX 를 얻는다React
커밋이전 결과와 비교해 바뀐 DOM 만 고친다React

렌더 단계에서는 DOM을 건드리지 않는다. 그냥 함수를 부르고 반환된 JSX 객체를 받아둔다. 3강에서 JSX가 그냥 객체라고 했던 게 여기서 쓰인다.

실제 화면이 바뀌는 건 커밋 단계다. 그리고 바뀐 게 없으면 커밋에서 아무것도 안 한다.

언제 다시 실행되나

이유는 두 가지뿐이다.

리렌더링의 두 원인

자기 state 가 바뀜

setState 호출

  • 값이 실제로 달라져야 한다
  • Object.is 로 비교한다
  • 같은 값을 넣으면 건너뛴다

부모가 리렌더링됨

props 와 무관하게

  • props 가 안 바뀌어도 다시 실행된다
  • 자식의 자식까지 전부
  • React.memo 로 막을 수 있다

두 번째가 초보자를 놀라게 한다. 부모가 다시 실행되면 자식도 전부 다시 실행된다. props가 그대로여도 그렇다.

function Parent() {
  const [count, setCount] = useState(0);

  return (
    <div>
      <button onClick={() => setCount(c => c + 1)}>{count}</button>
      <Child name="지원" />
    </div>
  );
}

function Child({ name }) {
  console.log("Child 실행");
  return <p>{name}</p>;
}

버튼을 누를 때마다 콘솔에 "Child 실행"이 찍힌다. name은 한 번도 안 바뀌었는데도.

리렌더링은 아래로 퍼진다
  • Parentstate 가 바뀌었다
    • Childprops 그대로여도 다시 실행
      • GrandChild여기도 다시 실행

상태는 위치로 기억된다

7강과 11강에서 미뤄둔 이야기다. React는 컴포넌트의 상태를 어디에 매달아 둘까.

화면 트리에서의 위치다.

function App() {
  const [showFirst, setShowFirst] = useState(true);

  return (
    <div>
      {showFirst ? <Counter /> : <Counter />}
      <button onClick={() => setShowFirst(s => !s)}>전환</button>
    </div>
  );
}

버튼을 눌러 다른 <Counter />로 바꿔도 카운트가 유지된다. 같은 위치에 같은 타입의 컴포넌트가 있으니 React는 같은 것으로 본다.

반대로 위치가 달라지면 상태가 날아간다.

{showFirst ? <Counter /> : <div><Counter /></div>}

이제는 초기화된다. div가 하나 끼어들어 위치가 달라졌기 때문이다.

React 가 같은 컴포넌트로 보는 조건
  1. 01

    같은 위치에 있는가

    부모 기준으로 몇 번째 자식인지가 같아야 한다.

  2. 02

    같은 타입인가

    Counter 였다가 Timer 가 되면 다른 것이다. 상태를 버린다.

  3. 03

    key 가 같은가

    key 가 있으면 위치보다 key 를 먼저 본다. 11강에서 상태를 초기화하던 그 방법이다.

    <Counter key={userId} />

7강에서 "조건부 렌더링으로 컴포넌트를 없애면 상태가 사라진다"고 했던 이유가 이것이다. 화면에서 빠지면 그 위치가 사라지고, React는 매달아둔 상태를 버린다.

렌더 함수는 순수해야 한다

렌더 단계에서 지켜야 할 규칙이 있다. 같은 입력이면 같은 출력을 내야 하고, 바깥 세상을 건드리면 안 된다.

let count = 0;

function Bad() {
  count++;
  return <p>{count}</p>;
}

컴포넌트 밖의 변수를 고쳤다. 실행할 때마다 결과가 달라진다. React가 언제 몇 번 실행할지는 우리가 정하지 않으므로, 이런 코드는 예측 불가능해진다.

function AlsoBad({ items }) {
  items.push("새 항목");
  return <ul></ul>;
}

props를 고쳤다. 5강과 12강에서 금지한 그것이다.

렌더 중에 하면 안 되는 일대신
바깥 변수 수정state 로 관리한다
props · state 직접 수정새 객체를 만든다
setState 호출이벤트 핸들러나 useEffect 에서
DOM 직접 조작useEffect 나 ref 에서
서버 요청useEffect 에서 (15강)
Math.random() · Date.now()렌더 밖에서 계산해 넘긴다

렌더 중에 setState를 부르면 무한 루프가 된다.

function Infinite() {
  const [count, setCount] = useState(0);
  setCount(count + 1);
  return <p>{count}</p>;
}
콘솔
  • Error: Too many re-renders. React limits the number of renders to prevent an infinite loop.

렌더 → setState → 트리거 → 렌더 → … 끝이 없다. React가 알아서 멈추고 에러를 던진다.

StrictMode 가 두 번 실행하는 이유

개발 중에 console.log가 두 번 찍혀 당황한 적이 있을 것이다.

<StrictMode>
  <App />
</StrictMode>

StrictMode개발 환경에서만 컴포넌트 함수를 일부러 두 번 실행한다.

순수하지 않은 컴포넌트를 찾아내려는 것이다. 위의 count++ 같은 코드가 있으면 두 번 실행했을 때 결과가 달라지므로 즉시 티가 난다.

정리하면

  • 렌더링은 세 단계다. 트리거 → 렌더(함수 실행) → 커밋(DOM 수정).
  • 렌더 단계는 DOM을 건드리지 않는다. 리렌더링이 일어나도 화면이 안 바뀔 수 있다.
  • 리렌더링의 원인은 둘뿐이다. 자기 state가 바뀌거나, 부모가 다시 실행되거나.
  • 부모가 다시 실행되면 props가 그대로여도 자식이 전부 다시 실행된다.
  • React는 상태를 화면 트리의 위치로 기억한다. 위치가 바뀌면 상태를 버린다.
  • 렌더 함수는 순수해야 한다. 바깥을 건드리지 않고, 같은 입력에 같은 출력을 낸다.
  • StrictMode의 이중 실행은 버그가 아니라 순수성 검사다.

다음 강의에서는 커밋 단계에서 React가 무엇을 어떻게 비교하는지 본다. 가상 DOM이다.

렌더링과 리렌더링 — 디코드랩(DCODELAB)