ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [Article] React 렌더링 동작에 대한 (거의) 완벽한 가이드(1편)
    프로그래밍/React 2023. 11. 21. 21:05

    개요

    • 랜더링이란?
    • React의 랜더링방식
    • 랜더링 성능 개선
    • Context와 랜더링 동작
    • Redux와 랜더링 동작
    • React의 향후 개선사항
    • 기타

    랜더링이란?

    현재 porps 및 상태를 기반으로 React가 컴포넌트에게 UI영역이 어떻게 보이길 원하는 지 설명을 요청하는 프로세스

     

    랜더링 프로세스)

    • 프로세스동안 React는 컴포넌트 트리의 루트에서 시작해서 업데이트가 필요한 모든 컴포넌트를 찾기 위해 아래로 순회한다.
    • 컴포넌트 랜더 출력은 일반적으로 JSX구문으로 작성되며 Javascript가 컴파일되고 React.createElemet()를 통해 createElement라는 DOM객체와 유사한 객체로 반환된다.
    • 이후 재조정을 통해서 업데이트 내용을 수집하여 DOM요소에 딱 한번 적용시켜 업데이트 내용을 반영한다.
    // JSX 구문입니다.
    return <MyComponent a={42} b="testing">Text here</MyComponent>
    
    // 아래와 같은 호출로 변환됩니다.
    return React.createElement(MyComponent, {a: 42, b: "testing"}, "Text Here")
    
    // 그리고 이것은 다음과 같은 요소 객체가 됩니다.
    {type: MyComponent, props: {a: 42, b: "testing"}, children: ["Text Here"]}
    
    // 그리고 내부적으로는 리액트가 실제 함수를 호출해 렌더링 합니다.
    let elements = MyComponent({...props, children})
    
    // HTML 처럼 보이는 "호스트 컴포넌트"의 경우
    return <button onClick={() => {}}>Click Me</button>
    // 아래와 같이 호출되어
    React.createElement("button", {onClick}, "Click Me")
    // 최종적으로 아래와 같이 됩니다.
    {type: "button", props: {onClick}, children: ["Click me"]}
    React에서 가상DOM 용어를 멀리하는 이유
    초창기(2013년도)에는 React가 모든 렌더에서 DOM노드를 생성한다고 생각하였기에 의미가 있던 가상DOM
    하지만 실제 React에서는 "값 UI"라는 용어를 사용 (UI는 문자열이나 배열과 마찬가지로 값이다)
    변수에 저장하고 전달하고, Javascript 제어 흐름에서 사용할 수 있다 (단순히 비교하는 개념 이상의 개념이다!)

     

    렌더와 커밋 단계)

    가상DOM을 통한 업데이트가 아닌 이 두 단계를 통해서 변경사항을 업데이트한다.

    • 렌더 단계 : 컴포넌트를 렌더링하고 변경사항을 계산
    • 커밋 단계 : 렌더 단계에서 계산된 변경사항을 DOM에 적용

    업데이트 이후 실행되는  useLayoutEffect   hook(라이프사이클도 있지만 class형 컴포넌트 거의 안쓰니깐 스킵)

    이후 짧은 시간제한을 설정하고 만료시   useEffect   hook실행(패시브 이펙트 단계)

    위와 같이 랜더링DOM업데이트는 같은 과정이 아니다. (컴포넌트가 랜더링될 때 어떠한 가시적 변화도 일어나지 않을 수 있다)

    • 컴포넌트가 지난번과 동일해서 렌더출력을 반환해 변경이 필요하지 않을 수 있다
    • 동시 렌더링 (React 18 useTransition)에서 React는 컴포넌트를 여러 번 렌더링할 수 있지만 다른 업데이트로 인해 현재 수행 중인 작업이 무효화되는 경우 렌더 출력을 버린다.

    React의 랜더링 방식

    일반적인 렌더링 동작)

    React의 기본동작은 상위 컴포넌트가 랜더링될 때 해당 컴포넌트 내부의 하위 컴포넌트를 순환하면서 랜더링한다.그렇기에 A > B > C > D 의 구조의 컴포넌트에서 B 내부의 state가 변화가 있을 때  React는 최상단에서 랜더 패스를 시작하여 A는 지나치고 B는 업데이트가 필요하니 랜더링하고 자연스럽게 C, D도 랜더링한다.

     

    React 랜더링 규칙)

    기본적으로 랜더링은 순수해야하며 어떠한 사이드 이펙트도 없어야 한다.

     

    파이버객체)

    React는 애플리케이션에 존재하는 모든 컴포넌트 인스턴스를 추적하는 내부데이터 구조를 따로 객체형태로 저장하는데 그 객체형태의 데이터가 파이버라는 것이고 이건 다양한 값을 가지고 있다

    • 컴포넌트 트리의 해당지점에서 렌더링되어야 할 컴포넌트 타입
    • 해당 컴포넌트와 관련된 props, state
    • 상위, 형제, 하위 컴포넌트 포인터
    • 기타 내부 메타데이터(랜더링 프로세스 추적에 필요함)
    export type Fiber = {
      // 파이버 타입을 식별하기 위한 태그입니다.
      tag: WorkTag;
    
      // 해당 요소의 고유 식별자 입니다.
      key: null | string;
    
      // 파이버와 관련된 것으로 확인된 함수/클래스 입니다.
      type: any;
    
      // 단일 연결 리스트 트리 구조입니다.
      child: Fiber | null;
      sibling: Fiber | null;
      index: number;
    
      // 파이버로 입력되는 데이터 입니다. (arguments/props)
      pendingProps: any;
      memoizedProps: any; // 출력을 만드는데 사용되는 props입니다.
    
      // 상태 업데이트 및 콜백 큐 입니다.
      updateQueue: Array<State | StateUpdaters>;
    
      // 출력을 만드는데 사용되는 상태입니다.
      memoizedState: any;
    
      // 파이버에 대한 종속성(컨텍스트, 이벤트)입니다.(존재하는 경우)
      dependencies: Dependencies | null;
    };

     

    컴포넌트 타입과 재조정)

     

    리스트의 key가 유일해야하는 이유)

    React의 특성상 기존의 DOM노드와 컴포넌트 인스턴스를 재사용할려는 경향이 있다. 이는 key를 index로 설정했을 때 발생할 수 있는 오류의 원인이 된다.key를 0~9까지 설정해놓은 <item />이 있는데 여기서 3개를 삭제하고 4개를 추가하게 된다면 결론은 key가 0~10까지 가진 <item />이 된다. 이는 React의 특성상 기존의 것을 활용하려는 특성떄문에 인스턴스는 유지된 상태에서 다른 데이터 객체를 가져오기에 예기치 못한 오류가 발생할 수 있다.

     

    랜더링 일괄처리와 타이밍)

    React 18에서 도입된 것으로 기본적으로 setState() 호출은 React가 새 렌더 패스를 시작하고 동기적으로 실행하고 반환하도록 한다. 그러나 React에서는 최적화의 일종으로 일괄처리하는 형태로 적용하기도 하는데 기존에 17이전에 버전에서는 아래의 코드가 3번 리랜더링이 되었다(일괄로 await전에꺼 몰아서 1번 이후 개별적으로 렌더패스로 각각 setCounter 1번씩해서 총 3번) 근데 18에서는 await이후도 일괄로 몰아서 총 2번으로 바뀌었다는 내용

    const [counter, setCounter] = useState(0);
    
    const onClick = async () => {
      setCounter(0);
      setCounter(1);
    
      const data = await fetchSomeData();
    
      setCounter(2);
      setCounter(3);
    };

     

    비동기 랜더링, 클로저, 상태스냅샷)

    function MyComponent() {
      const [counter, setCounter] = useState(0);
    
      const handleClick = () => {
          setCounter(counter + 1);
          // ❌ 이것은 의도한대로 동작하지 않습니다.
          console.log(counter);
          // 원래 값이 기록됩니다. - 왜 아직 업데이트되지 않았을까요??????
        };
    }

     

    위의 코드가 의도대로 동작하지 않는 이유는 가장 간단하게 말하자면 React 상태 업데이트는 비동기이기 때문이다라고 하면 되지만 정확히 말하자면 handleClick함수가 클로저이기에 함수가 정의되었을 때 존재했던 변수의 값만을 알 수 있다 하지만 setCounter가 호출되면서 렌더 패스가 큐에 추가되어 미래의 렌더링에서 새로운 값을 갖는 새로운 변수와 handleClick함수가 되기에 문제가 생긴다.

     

    랜더링 동작 엣지 케이스)

    useLayoutEffect의 경우 랜더링 후 브라우저가 페인팅하기 전에 추가적인 로직을 수행할 수 있도록 하기 위해 존재하는 hook으로 커밋단계 생명주기 메서드라고 불린다.

    이러한 메서드를 사용하면 부분->최종으로 되는 진화형태의 업데이트가 아닌 최종의 내용만 화면에 표시하듯이 나탄낼 수 있다.

     

    랜더링 중 상태 설정)

    useEffect에서 setState() 호출이 가능하냐를 말하는 부분인데 가능한데 무한루프를 조심하면서 사용해야한다.잘 사용한다면 리랜더링 없이 props의 변경에 따라 상태값을 즉시 강제 업데이트 하는데 사용 될 수 있다.

     

     

     

     

     

     

    Article :  원문, React 랜더링...

    '프로그래밍 > React' 카테고리의 다른 글

    [React] Suspense  (2) 2023.11.22
    [Hooks] useEffect  (1) 2023.11.22
    [SWR] Data의 reavalidate, mutate 활용  (0) 2023.11.03
    [전역상태관리] Recoil  (1) 2023.11.02
    [React] react-router-dom의 사용  (1) 2023.10.04

    댓글

Designed by Tistory.