[250707] TIL

Today I Learned (2025-07-07)

렌더링 동작에 대한 완벽한 가이드 읽기

상태 변화 발생
↓
Render Phase
↓
Commit Phase

1. React가 실제 DOM 변경 (e.g., element.style.width = '200px')

2. **useLayoutEffect 실행** (DOM은 변경되었지만, 브라우저가 화면에 그리기 전 동기적으로 실행)
    ↓
    브라우저 렌더링 파이프라인 (Layout → Paint → Composite)
    ↓
    useEffect 실행 (브라우저가 화면 그리기를 완료한 후 비동기적으로 실행)

  1. 리액트의 기본 동작은 상위 컴포넌트가 렌더링 될 때 리액트가 해당 컴포넌트 내부의 모든 하위 컴포넌트를 순환하며 렌더링함
  2. 파이버 : 컴포넌트 하나하나의 상태, 위치, 작업 정보를 들고 있는 객체(노드), 컴포넌트마다 하나씩 존재
    • 전체 컴포넌트 트리 상태 추적
    • 파이버를 따라가면서 해당 컴포넌트의 함수를 호출해서 새로운 React 앨리먼트(가상돔)을 만듬
    • 가상돔은 함수 컴포넌트가 실행된 결과물이고, 파이버는 생애주기, 상태, 연결정보, 렌더링 이력을 들고 있음
    • 실제 props와 상태값을 사용할 때, 리액트는 사실 파이버 객체에 저장된 값에 대해 접근함
  3. React가 같은 타입의 여러 컴포넌트를 구별하는 데 사용하는 “고유 식별자”
    • 같은 키면 같은 컴포넌트로 간주하고 다른 키면 새로 만듬.
    • 그럼 react.memo랑 key랑 비슷한 역할을 하나 ?
  4. useState는 한번에 묶어서 일괄처리, 이벤트 핸들러, 프로미스, setTimeout 등 모든 비동기 콜백 안에 같은 이벤트 루프 틱 안에서 여러번 발생하면 한 번에 렌더링.
  5. 컴포넌트가 처음 렌더 될 때 현재 파이퍼에 훅 연결 리스트에 state값과 업데이트 큐를 저장함

콜스택이 비어야 태스크 큐(마이크로태스크, 매크로태스크)에서 함수가 실행되는데,

리액트가 리렌더링 할 시점, 상태 변경을 어떻게 감지하고 실행 흐름을 이어가냐?

  • setState 함수 실행 자체는 JS 콜스택에 있다가 곧바로 사라짐
  • 콜스택이 비면, 큐에 모아둔 업데이트를 한 번에 실행할 수 있게 자체적으로 콜백 함수를 태스크 큐에 등록
  • Promise.then(fn), queueMicrotask(fn) 이런 자바스크립트 함수들이 콜스택이 비면 실행 시켜주는 코드임
  • “리렌더링 실행을 예약해야겠다!”고 판단하고, queueMicrotask(flushUpdates)를 호출하여 마이크로태스크 큐에 리렌더링 작업을 등록합니다. (만약 이미 예약되어 있다면 중복 등록은 하지 않습니다.)
  • 리액트는 상태 감시 함수를 계속 돌리지 않는다. (이벤트 루프가 계속 감시함)
  • setState/useState의 setter 함수가 실행될 때 직접 “업데이트 예약” 신호를 React에 보냄
  • useEffect 콜백의 상태 업데이트는 큐에 저장되어 useEffect 콜백이 모두 완료되면 Passive Effects 단계의 마지막에 한 번에 플러시됨

인터럽트

  • 이벤트 처리 시스템은 자바스크립트에서가 아닌 브라우저의 이벤트 시스템이다.
  • 운영 체제와 긴밀하게 협력함 (OS로부터 신호를 받아 동작함)
  • 시스템이 이벤트를 감지하면 JS 콜백함수를 태스크 큐에 넣어주는 역할을 함

그럼 콜백 함수는 어디에다가 저장해둘까?

  • 브라우저가 관리하는 해당 DOM 요소의 내부적인 이벤트 리스너 목록에 저장
  • 브라우저의 C++로 구현된 내부 세상에 저장되는 데이터
  • 이벤트 리스너 목록을 가지고 있음

onClick에 전달된 콜백 함수는 즉시 실행되거나 태스크 큐에 들어가지 않고,
브라우저가 C++ 레벨에서 관리하는 해당 DOM 요소의 내부 “이벤트 리스너 목록”에 함수 참조 형태로 저장되어,
실제 이벤트가 발생할 때까지 안전하게 대기합니다.

이벤트가 발생하면 브라우저는 이 저장소에서 해당 함수를 꺼내 태스크 큐에 넣어주며, 비로소 JavaScript의 이벤트 루프가 처리할 수 있는 영역으로 넘어오게 됩니다.

컨텍스트와 렌더링 동작

  • 컨텍스트는 상태관리 도구가 아니다.
  • 컨텍스트에 전달되는 값을 직접 관리해야함 -> 이 말은 Context는 그 값을 props 없이 하위 트리에 흘려주는 역할만 한다

요약

  • 리액트는 항상 기본적으로 컴포넌트를 순회하며 렌더링하므로, 상위 컴포넌트가 렌더링될 때 하위 컴포넌트들도 렌더링됩니다.
  • 렌더링 자체는 괜찮습니다. 리액트가 DOM 변경이 필요한지 아는 방법입니다.
  • 그러나 렌더링에는 시간이 걸리고 UI 출력이 변경되지 않는 “낭비된 렌더링”이 추가될 수 있습니다.
  • 대부분의 경우 콜백 함수 및 객체와 같은 새로운 참조를 내려주는 것은 문제되지 않습니다.
  • React.memo()와 같은 API는 props가 변경되지 않은 경우 불필요한 렌더링을 건너뛸 수 있습니다.
  • 그러나 props로 항상 새로운 참조를 전달하면, React.memo()는 렌더링을 절대 건너뛸 수 없으므로 이러한 값들은 메모이제이션해야 할 수 있습니다.
  • 컨텍스트는 깊이 중첩된 컴포넌트가 관련된 값에 접근할 수 있도록 해줍니다.
  • 컨텍스트 공급자는 참조를 비교해 값이 변경되었는지 확인합니다.
  • 새 컨텍스트 값은 중첩된 모든 소비자가 리렌더링되도록 강제합니다.
  • 그러나 많은 경우, 일반적인 상위 컴포넌트 렌더링 캐스케이드 프로세스로 인해 하위 컴포넌트는 어쨌든 리렌더링 됐을 것입니다.
  • 따라서 컨텍스트 값을 업데이트할 때 전체 트리가 항상 렌더링되지 않도록 컨텍스트 공급자의 하위 컴포넌트를 React.memo()로 감싸거나 {props.children}를 사용할 수 있습니다.
  • 하위 컴포넌트가 새 컨텍스트 값에 기반해 렌더링되면 리액트는 해당 지점에서 다시 내려가며 렌더링합니다.
  • React-Redux는 컨텍스트로 저장소 상태 값을 전달하는 대신 Redux 저장소에 대한 구독을 통해 업데이트를 확인합니다.
  • 이러한 구독은 모든 Redux 저장소 업데이트에서 실행되므로 가능한 한 빨라야 합니다.
  • React-Redux는 데이터가 변경된 컴포넌트만 리렌더링 하기 위해 많은 작업을 수행합니다.
  • connect는 React.memo()와 같은 역할을 하므로 연결된 컴포넌트가 많으면 한 번에 렌더링하는 컴포넌트의 수를 최소화할 수 있습니다.
  • useSelector는 훅이므로 상위 컴포넌트로 인해 발생하는 렌더링을 중단할 수 없습니다. useSelector만 사용하는 앱이 있다면 React.memo()를 일부 컴포넌트에 적용해 렌더링이 항상 캐스케이드 되는 것을 방지해야 합니다.
  • “React Forget” 자동 메모이제이션 컴파일러가 출시되면 이 모든 것을 획기적으로 간소화시킬 수 있습니다.

Categories:

Updated:

Leave a comment