본문 바로가기
코딩 정보/React

[React] 랜더링의 동작과 관련 개념을 자세히 알아보자

by 꽁이꽁설꽁돌 2024. 7. 17.
728x90
반응형

목차

     

    계속 공부를 하면서 여러 오류들을 직면하는데 그 중 하나가 랜더링에 관련한 것이었다.

    그래서 이번에 랜더링에 대해 정리해보고자 한다.

     

     

    랜더링이란?

    렌더링이란 React가 컴포넌트에게 현재 Props와 State에 기반하여 UI에서 어떻게 보여지고 싶은지 알려달라고 요청하는 과정입니다.

     

     

    랜더와 커밋 단계

    • 렌더 단계 : 컴포넌트를 렌더링하고 변경 사항을 계산하는 모든 과정이 이루어지는 단계
    • 커밋 단계 : 변경 사항을 실제 DOM에 적용하는 단계

    커밋 단계를 거쳐서 DOM을 업데이트하고 나면, React는 요청된 DOM 노드와 컴포넌트 인스턴스를 가리키도록 모든 참조사항들을 업데이트합니다. 그리고나서 componentDidMount  componentDidUpdate 클래스 생명주기 메소드 또는 useLayoutEffect 훅을 동기적으로 실행합니다.

     

    그후 React는 짧은 타임 아웃을 세팅하고, 타임 아웃이 끝나면 모든 useEffect 훅을 실행합니다. 이 단계는 "수동적 효과 (Passive Effect)" 라고도 알려져 있습니다.

     

     

    가장 핵심적인 부분은 "렌더링"과 "DOM을 업데이트하는 것"은 같은 것이 아니며 컴포넌트는 어떠한 가시적인 변화가 없이도 렌더링 될 수 있다는 점입니다.

     

    표준적인 랜더의 동작

    React는 기본적으로 부모 컴포넌트가 렌더링되면, 그 안에 있는 모든 자식 컴포넌트를 재귀적으로 렌더링한다는 점입니다.

     

    여기서 또 중요한 부분은

    React는 "Props가 변경되었는지 여부"는 신경쓰지 않습니다. 그저 부모 컴포넌트가 렌더링되었기 때문에 자식 컴포넌트도 무조건 렌더링하는 것입니다.

     

     

    React 랜더링의 규칙

    바로 렌더링은 "순수"해야만 하며 사이드 이펙트를 만들어내서는 안된다는 것입니다.

     

    • 렌더링 로직은 다음과 같은 행위를 절대로 해서는 안됩니다.
      • 현재 존재하는 변수와 객체를 변경하는 행위
      • Math.random() 또는 Date.now() 등의 랜덤 값을 만들어내는 행위
      • 네트워크 요청을 만들어내는 행위
      • 상태 업데이트를 Queue에 넣는 행위
    • 다음과 같은 행위는 아마 괜찮을 수 있습니다.
      • 렌더링 도중에 새롭게 만들어진 객체를 변경하는 행위
      • 에러를 발생시키는 행위
      • 캐싱된 값처럼 아직 만들어지지 않은 데이터를 "Lazy 초기화 (Initialize)" 하는 행위

     

    React Fiber

    Fiber는 React v16에서 리액트의 핵심 알고리즘을 재구성한 새 재조정(Reconciliation) 엔진이다. React Fiber의 목표는 애니메이션, 레이아웃, 제스처, 중단 또는 재사용 기능과 같은 영역에 대한 적합성을 높이고 다양한 유형의 업데이트에 우선 순위를 지정하는 것이다.

     

    핵심 기능은 렌더링을 증분하는 것이다. 이는 렌더링 작업을 여러 덩어리로 나누어 여러 프레임에 분산하는 기능이다.

     

     

    Reconciliation

    React는 기존에 존재하는 컴포넌트 트리와 DOM 구조를 최대한 재활용해서, 최대한 효율적으로 리렌더링을 진행하려고 합니다. Virtual DOM으로 널리 이해되고 있는 것의 뒤에 있는 알고리즘을 말합니다.

     

     

    Reconcilation의 요점은 다음과 같다.

    • 컴포넌트 type이 다르면, 실질적으로 다른 트리를 생성한다고 가정한다. 따라서 리액트는 이를 구분(diff)하려고 하지 않고, 대신에 이전의 트리를 완전히 교체한다.
    • 목록은 key를 통해 구분(diff)된다. 따라서 key는 "안정적이고, 예측 가능하고, 유니크해야 한다."

     

    첫번째를 간단히 설명하면

    렌더링이 일어나는 동안에는 절대로 새로운 컴포넌트 유형을 생성해서는 안되며 새로운 컴포넌트 유형을 만들면 이는 모두 다른 참조를 갖고, React는 계속해서 자식 컴포넌트 트리를 파괴하고 다시 만들 것입니다.

     

    아래 행위는 해서는 안됩니다.

    function ParentComponent() {
      // This creates a new `ChildComponent` reference every time!
      function ChildComponent() {}
      
      return <ChildComponent />
    }

     

    컴포넌트는 항상 분리되어야 합니다.

    // This only creates one component type reference
    function ChildComponent() {}
      
    function ParentComponent() {
    
      return <ChildComponent />
    }

     

     

    두번째를 간단히 설명하면

    10개의 <TodoListItem> 컴포넌트를 갖고 있는 배열을 렌더링하는데, 배열 인덱스를 key로 사용한다고 해봅시다.

    React는 0 ~ 9의 Key를 갖고 있는 10개의 요소를 보겠죠. 여기서 6번째와 요소를 지우고 끝에 새로운 요소 2개를 추가해봅시다. 그럼 이제 0 ~ 9의 Key를 갖고 있는 11개의 요소가 생겼죠. 결론적으로 10개에서 11개가 되었기 때문에 React 입장에서는 새로운 요소가 하나 더 추가된 것처럼 보일 것입니다. 따라서 React는 기존에 있는 DOM 노드와 컴포넌트 인스턴스를 재사용하려고 합니다. 

     

    이러한 불필요한 업데이트를 피하기 위해 배열의 요소 각각에 key={todo.id}를 사용한다면, React는 정확하게 2개의 요소가 삭제되고 3개의 요소가 추가되었음을 알 수 있습니다. 따라서 정확히 2개의 컴포넌트 인스턴스와 관련된 DOM 요소를 삭제하고 3개의 새로운 컴포넌트 인스턴스와 관련된 DOM 요소를 생성할 것입니다.

     

    간략히 말하면

    React 컴포넌트에 key를 추가하여 식별자를 만들 수 있고, 이 key를 변경하면 React는 기존의 컴포넌트 인스턴스를 제거하고 새로 생성할 것입니다.

     

     

     

    Batching을 이용한 랜더링 최적화

    아래 코드의 console.log에는 count1count2 count3이 찍히지 않고 count3만이 찍힌다.

     

    이것은 3번의 상태변화가 3번의 렌더링을 발생시키지 않고, 한 번의 렌더링을 발생시켰다는 것을 의미한다.

    즉, 배칭이란 여러개의 state 업데이트를 하나의 리렌더링으로 묶는 것을 의미한다.

    function App() {
      const [count, setCount] = useState(0);
      const handleClick = () => {
        setCount((count) => count + 1);
        setCount((count) => count + 1);
        setCount((count) => count + 1);
      };
    
      useEffect(() => {
        console.log("count", count);
      }, [count]);
    
      return <button onClick={handleClick}>+</button>;
    }

     

    setState가 비동기로 동작하는 이유가 바로 여기에 있다.

    일정 기간이 끝날 때까지 setState를 실행하지 않고 모아둔다.

     

     

    불변성과 리랜더링

     

    불변성이란?

    불변성이 이야기하는 상태의 변경이라는 것은 단순한 변수의 재할당을 이야기하는 것이 아니다. 정확히 말하면 메모리에 저장된 값을 변경하는 모든 행위를 의미하며, 여기에 변수의 재할당과 같은 행위도 포함되는 것이다.

     

    불변성을 지키면 좋은 점

    1. 무분별한 상태의 변경을 막는다.

    그래서 개발자들은 상태를 변경하는 행위에 특정한 규칙과 제약을 정해서 무분별한 상태 변화를 최대한 피하고, 이런 변화를 추적할 수 있는 상황을 선호할 수 밖에 없다.

     

     

    2. 상태의 변경을 추적하기 쉽다.

    예를 들면 설명하면 다음과 같다.

     

    이 함수는 순수 함수가 아닌데, 그 이유는 함수가 참조에 의한 호출 방식을 사용하는 객체의 프로퍼티를 직접 변경하면 함수 외부에 있는 원본 객체의 상태도 변경되기 때문이다.

    const evan = { name: 'Evan' };
    const john = convertToJohn(evan);
    
    console.log(evan);
    console.log(john);

     

    { name: 'John' } // ?
    { name: 'John' }

     

    이런 상황에서 개발자는 “의도하지 않은 객체의 상태 변화”와 “상태의 변화를 추적할 수 없다”는 고약한 문제를 떠안게 된다. 그렇다면 이 문제를 어떻게 해결할 수 있을까?

     

     

    바로 name을 John으로 가지는 객체를 그냥 새로 생성해버리면 된다.

    function convertToJohn (person) {
      const newPerson = Object.assign({}, person);
      newPerson.name = 'John';
    
      return newPerson;
    }
    
    const evan = { name: 'Evan' };
    const john = convertToJohn(evan);
    
    console.log(evan);
    console.log(john);
    
    console.log(evan === john); // false

     

     

    객체의 상태를 변화시킬때, “상태가 변화된 객체”를 새로 생성한다면 우리는 이전 상태를 가진 객체와 다음 상태를 가진 객체를 비교하며 false가 나온다는 사실을 이용하며 객체의 상태가 변화되었음을 알 수 있는 것이다.

    이러한 불변성의 특징들은 참조에 의한 호출을 사용하는 자료형들의 상태 변화를 쉽게 감지할 수 있도록 만들어주기 때문에 개발자가 예상하지 못하는 방향으로 버그가 발생하는 것을 어느 정도 막을 수 있다.

     

    리액트의 랜더링 과정 요약

    React 마운트 과정

    1. 함수 컴포넌트 호출

    2. 구현부 실행

      - props 취득, hook 실행, 내부 변수 및 함수 생성

      - 단, hook 에 등록해둔 상태값, 부수함수 효과 등은 별도 메모리에 저장되어 관리된다.

    3. return 실행

      - 렌더링 시작

    4. 렌더 단계(Render Phase)

      - 가상DOM을 생성한다.

    5. 커밋 단계(Commit Phase)

      - 실제 DOM에 반영한다.

    6. useLayoutEffect

      - 브라우저가 화면에 Paint 하기 전에, useLayoutEffect에 등록해둔 effect(부수효과함수)가 '동기'로 실행된다.

      - 이 때, state, redux store 등의 변경이 있다면 한번 더 재렌더링 된다.

    7. Paint

      - 브라우저가 실제 DOM을 화면에 그린다. didMount가 완료된다.

    8. useEffect

      - Mount되어 화면이 그려진 직후, useEffect에 등록해둔 effect(부수효과함수)가 '비동기'로 실행된다.

     

     

    React 재렌더링 과정

    1. 함수 컴포넌트 재호출

    2. 구현부 실행

      - props 취득, hook 실행, 내부 변수 및 함수 재생성

      - 단, 각 hook의 특성에 따라 기존에 메모리에 저장한 내용을 적절히 활용한다.

    3. return 실행 

      - 렌더링 시작

    4. 렌더 단계(Render Phase)

      - 새로운 가상DOM 생성 후, 이전 가상 DOM과 비교하여, 달라진 부분을 탐색하고, 실제 DOM에 반영할 부분을 결정한다.

    5. 커밋 단계(Commit Phase)

      - 달라진 부분만 실제 DOM에 반영한다.

    6. useLayoutEffect

      - 브라우저가 화면에 Paint 하기 전에, useLayoutEffect에 등록해둔 effect(부수효과함수)가 '동기'로 실행된다.

      - 이 때, state, redux store 등의 변경이 있다면 한번 더 재렌더링 된다.

    7. Paint

      - 브라우저가 실제 DOM을 화면에 그린다. didUpdate가 완료된다.

    8. useEffect

      - update되어 화면이 그려진 직후, useEffect에 등록해둔 effect(부수효과함수)가 '비동기'로 실행된다.

      - effect에 return부분이 있다면, 구현부보다 먼저 실행된다.

     

     

    참고

    https://happysisyphe.tistory.com/41

     

    [React] Batching을 활용한 렌더링 최적화

    React 18(22.3.29)에서 Batching이 더욱 강화되었다. Batching은 React에서 굉장히 중요한 개념이며, 최적화에 크게 기여하고 있다. 하지만 생각보다 Batching은 초보 리액트 개발자에게 널리 알려지지는 않은

    happysisyphe.tistory.com

     

    https://velog.io/@arthur/%EB%B2%88%EC%97%AD-%EB%A6%AC%EC%95%A1%ED%8A%B8-%EB%A0%8C%EB%8D%94%EB%A7%81-%EB%8F%99%EC%9E%91%EC%9D%98-%EA%B1%B0%EC%9D%98-%EC%99%84%EB%B2%BD%ED%95%9C-%EA%B0%80%EC%9D%B4%EB%93%9C-A-Mostly-Complete-Guide-to-React-Rendering-Behavior

     

    [번역] 리액트 렌더링 동작의 (거의) 완벽한 가이드 [A (Mostly) Complete Guide to React Rendering Behavior]

    [번역] 리액트 렌더링 동작의 (거의) 완벽한 가이드

    velog.io

    https://evan-moon.github.io/2020/01/05/what-is-immutable/

     

    변하지 않는 상태를 유지하는 방법, 불변성(Immutable)

    이번 포스팅에서는 순수 함수에 이어 함수형 프로그래밍에서 중요하게 여기는 개념인 에 대한 이야기를 해보려고 한다. 사실 순수 함수를 설명하다보면 불변성에 대한 이야기가 꼭 한번은 나오

    evan-moon.github.io

    https://curryyou.tistory.com/486

     

    [React] 컴포넌트 렌더링 과정 정리(useLayoutEffect vs. useEffect)

    # React 컴포넌트 재렌더링 케이스 - React 컴포넌트는 아래의 2가지 상황에서 재렌더링(Re-rendering) 된다. 1. 내부 상태값(state)이나 중앙 상태값(redux store 등)이 변경되는 경우 2. 부모 컴포넌트가 재렌

    curryyou.tistory.com

     

    반응형