React Context의 내부 동작 원리

우혁·2024년 10월 13일
32

React

목록 보기
9/10
post-thumbnail

context란?

컴포넌트의 트리 전체에 데이터를 효율적으로 전달할 수 있는 기능이다.
Context API를 사용하면 Props Drilling 없이 여러 컴포넌트에 데이터를 공유할 수 있다.

장점

  • Props Drilling 문제 해결
  • 컴포넌트 간 데이터 공유 용이
  • 전역 상태 관리 기능

주의사항

  • 전역 상태 관리에 유용하지만, 불필요한 리렌더링을 유발할 수 있어 적절히 사용해야 한다.
  • 컴포넌트 재사용성이 떨어지므로, 꼭 필요한 경우에만 사용하는 것이 좋다.

createContext

  • 컴포넌트 외부에서 createContext를 호출하여 컨텍스트를 생성한다.
import { createContext } from 'react';
const ThemeContext = createContext('light');

매개 변수

  • 초기 값: 컴포넌트가 컨텍스트를 읽을 때 상위에 일치하는 컨텍스트 Provider가 없는 경우 사용되는 값이다.의미 있는 기본 값이 없다면 null로 지정하는 것이 좋다.

Provider가 존재한다면 Provider의 value prop이 초기 값보다 우선순위가 높다.

반환 값

컨텍스트 객체 자체는 어떠한 정보도 가지고 있지 않고 다른 컴포넌트가 읽거나 제공하는 어떤 컨텍스트를 나타낸다.
↪ 컨텍스트 객체는 일종의 통로 또는 채널로 생각할 수 있다. 이 통로 자체는 비어있지만, 데이터가 이 통로를 통해 흐를 수 있다.

상위 컴포넌트에서 컨텍스트 값을 지정하기 위해 Context.Provider를 사용하고, 하위 컴포넌트에서 읽기 위해 useContext(Context)를 호출한다.

컨텍스트 객체의 속성

  • Context.Provider: 컴포넌트에 컨텍스트 값을 제공한다.
  • Context.Consumer: useContext 이전에 컨텍스트를 읽는 방법으로 드물게 사용된다.

Provider

  • 컴포넌트를 컨텍스트 Provider로 감싸서 컨텍스트의 값을 모든 내부 컴포넌트에 지정한다.
function App() {
  const [theme, setTheme] = useState('light');
  // ...
  return (
    <ThemeContext.Provider value={theme}>
      <Page />
    </ThemeContext.Provider>
  );
}

Props

  • value: Provider 내부의 컨텍스트를 읽는 모든 컴포넌트에 전달하려는 값이다.

Provider 내부에서 useConext()를 호출하는 컴포넌트는 그 위의 가장 가까운 컨텍스트의 value를 받게 된다.

Consumer

컨텍스트를 읽는 방법 중 하나로 레거시 방식이다. 아직 작동은 하지만 useContext를 사용하는 것을 더 권장한다.

function Button() {
  // 🟡 이전 방식 (권장하지 않음)
  return (
    <ThemeContext.Consumer>
      {theme => (
        <button className={theme} />
      )}
    </ThemeContext.Consumer>
  );
}

Props

  • children: 함수이며 React는 useContext와 동일한 알고리즘으로 결정된 현재 컨텍스트의 값을 전달하여 함수를 호출하고, 이 함수에서 반환하는 결과를 렌더링한다.

부모 컴포넌트에서 컨텍스트가 변경되면 React는 이 함수를 다시 실행하고 UI를 업데이트한다.


useContext

  • useContext 를 컴포넌트의 최상위 수준에서 호출하여 컨텍스트를 읽고 구독한다.
import { useContext } from 'react';

function MyComponent() {
  const theme = useContext(ThemeContext);
  // ...

매개변수

  • SomeContext: createContext로 생성한 컨텍스트이다. 컨텍스트 자체는 정보를 담고 있지 않으며, 컴포넌트에서 제공하거나 읽을 수 있는 정보의 종류를 나타낸다.

반환 값

호출하는 컴포넌트에 대한 컨텍스트 값을 반환한다. 이 값은 트리에서 호출하는 컴포넌트 위의 가장 가까운 Provider에 전달된 value로 결정된다.

Provider가 존재하지 않는다면 createContext에 매개 변수인 초기 값이 지정된다.

컨텍스트가 변경되면 React는 자동으로 해당 컨텍스트를 읽는 컴포넌트를 다시 렌더링한다.

주의사항

⛔️ 1. React는 다른 value를 받는 Provider로부터 시작해서 특정 컨텍스트를 사용하는 모든 자식들을 자동으로 리렌더링한다. React.memo를 사용하더라도 컨텍스트 값이 변경될 때 리렌더링된다.

React.memo는 props의 변화만을 체크하므로, 컨텍스트 값의 변화는 감지하지 못한다.

⛔️ 2. 빌드 과정에서 같은 모듈이 여러 번 포함되는 경우(심볼릭 링크에서 발생할 수 있음) 컨텍스트가 손상될 수 있다.

React에서 컨텍스트가 제대로 작동하려면, 컨텍스트를 제공하는 곳과 사용하는 곳에서 정확히 같은 컨텍스트 객체를 참조해야 한다.

컨텍스트 객체를 비교할 때 동등 비교(===)를 사용하여 두 객체가 메모리 상에서 정확히 같은 객체인지 확인한다.

중복 모듈로 인해 동일한 컨텍스트가 여러 번 생성되면, 제공자(Provider)와 소비자(Consumer)가 서로 다른 컨텍스트 객체를 참조할 수 있다.

💡 심볼릭 링크란?
다른 파일이나 디렉토리를 가리키는 특별한 파일로, 원본 파일의 경로 정보를 포함한다.
주로 파일 시스템 구조를 유연하게 관리하거나 여러 위치에서 같은 파일에 접근할 때 사용된다.

심볼릭 링크를 사용할 때 중복 모듈이 생기는 이유는 빌드 시스템이 심볼릭 링크를 따라가 실제 파일을 복사할 때 같은 파일을 여러 번 복사할 수 있기 때문이다.

특히 복잡한 프로젝트 구조에서 여러 심볼릭 링크가 같은 파일을 가리키고 있다면, 빌드 과정에서 각 링크를 독립적인 파일로 처리하여 중복 모듈이 생길 수 있다.


내부 동작 원리

createContext, 컨텍스트 객체 생성

export function createContext<T>(defaultValue: T): ReactContext<T> {
  const context: ReactContext<T> = {
    $$typeof: REACT_CONTEXT_TYPE,
    _currentValue: defaultValue,
    _currentValue2: defaultValue,
    Provider: (null: any),
    Consumer: (null: any),
  };

  if (enableRenderableContext) {
    context.Provider = context;
    context.Consumer = {
      $$typeof: REACT_CONSUMER_TYPE,
      _context: context,
    };
  } else { // 일반적인 React 애플리케이션 개발에서 사용
    (context: any).Provider = {
      $$typeof: REACT_PROVIDER_TYPE,
      _context: context,
    };

    (context: any).Consumer = context;
  }

  return context;
}

Provider

Provider 리액트 앨리먼트 ↔ Fiber 태그 매핑

createContext에서 반환된 context 객체의 리액트 앨리먼트 타입(REACT_PROVIDER_TYPE)은 아래와 같은 과정에서 Fiber 태그 ContextProvider에 매핑된다.

export function createFiberFromTypeAndProps(
  type: any, // React$ElementType
  key: null | string,
  pendingProps: any,
  owner: null | ReactComponentInfo | Fiber,
  mode: TypeOfMode,
  lanes: Lanes
): Fiber {
  let fiberTag = FunctionComponent;
  default: {
      if (typeof type === 'object' && type !== null) {
        switch (type.$$typeof) {
          case REACT_PROVIDER_TYPE:
            if (!enableRenderableContext) {
              fiberTag = ContextProvider; // Fiber 노드 타입 매핑!
              break getTag;
            }
          case REACT_CONTEXT_TYPE:
            if (enableRenderableContext) {
              fiberTag = ContextProvider;
              break getTag;
            } else {
              fiberTag = ContextConsumer;
              break getTag;
            }
          case REACT_CONSUMER_TYPE:
            if (enableRenderableContext) {
              fiberTag = ContextConsumer;
              break getTag;
            }
        }
      }
    }
  }

  const fiber = createFiber(fiberTag, pendingProps, key, mode);
  fiber.elementType = type;
  fiber.type = resolvedType;
  fiber.lanes = lanes;

  return fiber;
}

Provider의 유일한 목적은 Consumer가 사용하는 값을 설정하는 것이다.


updateContextProvider, 렌더링 중에 Provider 동작하는 방식

function beginWork(
  current: Fiber | null,
  workInProgress: Fiber,
  renderLanes: Lanes
): Fiber | null {
  if (current !== null) {
  switch (workInProgress.tag) {
    case ContextProvider: // updateContextProvider 리턴
      return updateContextProvider(current, workInProgress, renderLanes);
    case ContextConsumer:
      return updateContextConsumer(current, workInProgress, renderLanes);
  }
}

function updateContextProvider(
  current: Fiber | null,
  workInProgress: Fiber,
  renderLanes: Lanes
) {
  let context: ReactContext<any>;
  if (enableRenderableContext) {
    context = workInProgress.type;
  } else {
    context = workInProgress.type._context; // 내부 컨텍스트 객체 가져오기
  }
  
  
  const newProps = workInProgress.pendingProps; // 현재 업데이트 중인 Fiber 노드의 새로운 props
  const oldProps = workInProgress.memoizedProps; // 이전에 사용된 Props
  const newValue = newProps.value; // 새로운 컨텍스트 값
	
  // 새로운 컨텍스트 값을 Provider 스택에 푸시(현재 활성화된 컨텍스트 값 추적)
  pushProvider(workInProgress, context, newValue);

  if (oldProps !== null) {
    const oldValue = oldProps.value;
    if (is(oldValue, newValue)) { // 이전 값과 새로운 값 비교
      if (
        oldProps.children === newProps.children && // 자식이 변경되지 않고
        !hasLegacyContextChanged() // 레거시 컨텍스트가 변경되지 않았다면
      ) {
        return bailoutOnAlreadyFinishedWork( // bail out하여 이전 작업을 재사용
          current,
          workInProgress,
          renderLanes
        );
      }
    } else { // 이전 값과 새로운 값이 변경되었다면 컨텍스트 변경 전파
      propagateContextChange(workInProgress, context, renderLanes);
    }
  }

  const newChildren = newProps.children;
  // 자식 컴포넌트를 조정하고 업데이트
  reconcileChildren(current, workInProgress, newChildren, renderLanes);
  return workInProgress.child;
}

1. pushProvider를 호출하여 새 값을 업데이트한다.

2. 변동 사항이 없다면 bailout 실행한다.

3. 변동 사항이 있다면 propagateContextChange를 호출하여 Consumer들에게 변경을 알리고, 자식 컴포넌트를 조정하고 렌더링한다.


pushProvider

export function pushProvider<T>(
  providerFiber: Fiber, // 현재 Provider를 나타내는 Fiber 노드
  context: ReactContext<T>, // 업데이트 할 Context 객체
  nextValue: T, // 새로운 컨텍스트 값
): void {
  if (isPrimaryRenderer) { // 현재 렌더러 확인 true 브라우저 / false 서버 사이드 렌더링
    push(valueCursor, context._currentValue, providerFiber);
    context._currentValue = nextValue;
  } else {
    push(valueCursor, context._currentValue2, providerFiber);
    context._currentValue2 = nextValue;
  }
}

// Fiber Stack
export type StackCursor<T> = {current: T};
const valueStack: Array<any> = [];
let index = -1;

function push<T>(
  cursor: StackCursor<T>, // 스택의 현재 위치를 나타내는 객체
  value: T, // 스택에 푸시할 새로운 값
  fiber: Fiber // 현재 작업 중인 Fiber 노드
): void {
  index++; // 전역 변수 index를 증가시켜 스택의 다음 위치를 가리키도록 한다.
    
  /*
  현재 커서가 가리키고 있는 값을 valueStack 배열의 현재 인덱스에 저장
  이는 나중에 이 값을 복원할 수 있도록 하기 위한 것이다.
  */
  valueStack[index] = cursor.current; 
  cursor.current = value; // 커서 업데이트, 커서는 이제 새로운 값을 가리키게 된다.
}

isPrimaryRenderer는 현재 렌더러가 주 렌데러(주로 브라우저 환경의 React-DOM)인지, 보조 렌더러(서버 사이드 렌더링, React Native 등)인지 여부를 나타낸다.

  • 스택 관리: push 함수를 통해 이전 컨텍스트 값을 스택에 저장하여 필요할 때 복원할 수 있도록 한다.

동일한 컨텍스트에 대해 여러 Provider가 중첩되어 있을 때 스택을 사용하면 Provider의 값이 순서대로 쌓이기 때문에 가장 가까운 Provider의 값은 스택의 최상위에 위치하게 된다.

  • 컨텍스트 값 업데이트: 새로운 컨텍스트 값을 설정하여 이후 Consumer들이 이를 사용할 수 있게 한다.

  • 렌더러 환경 구분: 주 렌더러와 보조 렌더러 간의 차이를 두어 각각의 상황에 맞게 컨텍스트 값을 관리한다.

이렇게 렌더러 환경을 구분하는 이유는 서로 다른 환경에서 독립적으로 상태를 유지하기 위해서이다.
이를 통해 각 환경에서 Context의 값을 독립적으로 관리하여 서로 간섭하지 않고 각자의 상태를 유지할 수 있다.


popProvider

  • popProvidercompleteWork에서 호출된다.
function completeWork(
  current: Fiber | null,
  workInProgress: Fiber,
  renderLanes: Lanes,
): Fiber | null {
  const newProps = workInProgress.pendingProps;

  popTreeContext(workInProgress);
  switch (workInProgress.tag) {
    case ContextProvider:
      // Pop provider fiber
      let context: ReactContext<any>;
      context = workInProgress.type._context;
      popProvider(context, workInProgress); // popProvider 호출
      bubbleProperties(workInProgress);
      return null;
  }
}
export function popProvider(
  context: ReactContext<any>, // 업데이트 할 context 객체
  providerFiber: Fiber, // 현재 Provider를 나타내는 Fiber 노드
): void {
  const currentValue = valueCursor.current; // 현재 값 가져오기, 가장 최근에 푸쉬된 컨텍스트 값

  // 값 업데이트
  if (isPrimaryRenderer) {
    context._currentValue = currentValue;
  } else {
    context._currentValue2 = currentValue;
  }

  pop(valueCursor, providerFiber); // 스택에서 값 제거
}

function pop<T>(cursor: StackCursor<T>, fiber: Fiber): void {
  // 스택의 최상위 값을 커서의 현재 값으로 복원, 이전에 저장된 컨텍스트 값을 다시 설정
  cursor.current = valueStack[index]; 
  valueStack[index] = null;  // 스택에서 해당 위치의 값을 제거하여 메모리 정리
  index--; // 인덱스를 감소시켜 이전 위치로 이동
}
  • 상태 복원: 스택을 사용하여 이전에 저장된 상태로 돌아가는 기능을 제공한다.

  • 컨텍스트 일관성 유지: Provider의 범위를 벗어날 때, 이전 컨텍스트 값으로 돌아가도록 하여 일관성을 유지한다.

💡 컴포넌트 트리를 순회하면서 새로운 Context Provider를 만나면 pushProvider가 호출되고, Provider의 범위를 벗어나면 popProvider가 호출된다.


Consumer

propagateContextChange함수가 어떻게 동작하는지 이해하려면 먼저 Consumer가 어떻게 동작하는지 이해해야 한다.

위에서 살펴봤듯이 Consumer는 실제로 컨텍스트 자체이며, ContextConsumer Fiber 태그가 사용된다.

updateContextConsumer, Consumer 컴포넌트 업데이트

function updateContextConsumer(
  current: Fiber | null, // 현재 렌더링된 Fiber 노드
  workInProgress: Fiber, // 작업 중인 Fiber 노드
  renderLanes: Lanes // 렌더링 우선순위 정보
) {
  let context: ReactContext<any>;
  
  // context 객체 결정
  if (enableRenderableContext) {
    const consumerType: ReactConsumerType<any> = workInProgress.type;
    context = consumerType._context;
  } else {
    context = workInProgress.type;
  }
    
  // Consumer의 새로운 props와 render 함수(children) 가져오기
  const newProps = workInProgress.pendingProps;
  const render = newProps.children;

  // Context 읽기를 준비하고 실제로 Context 값을 읽어온다.
  prepareToReadContext(workInProgress, renderLanes);
  const newValue = readContext(context);
    
    
  // Consumer의 render 함수를 새로운 context 값으로 실행  
  let newChildren;
  newChildren = render(newValue);
  
  // 렌더링 프로파일링 종료
  if (enableSchedulingProfiler) {
    markComponentRenderStopped();
  }

  // Fiber 노드에 작업이 수행되었음을 표시
  workInProgress.flags |= PerformedWork;
  reconcileChildren(current, workInProgress, newChildren, renderLanes);
  return workInProgress.child;
}

이 함수는 Context Consumer가 새로운 Context 값을 받아 어떻게 처리하는지를 보여준다.

주요 기능은 Context 값을 읽고, render 함수를 실행하여 새로운 자식 요소를 생성한 후, 기존 Fiber 트리와 조정한다.


prepareToReadContext, 컨텍스트 값을 읽기 위한 환경 설정

export function prepareToReadContext(
  workInProgress: Fiber,
  renderLanes: Lanes,
): void {
  currentlyRenderingFiber = workInProgress; // 현재 렌더링 중인 Fiber 노드
  // 이전에 관찰된 컨텍스트 의존성 정보 초기화
  lastContextDependency = null; 
  lastFullyObservedContext = null;

  const dependencies = workInProgress.dependencies; // 현재 Fiber 노드가 의존하는 컨텍스트 목록
  if (dependencies !== null) {
    const firstContext = dependencies.firstContext; 
    if (firstContext !== null) {
      // 현재 렌더링 우선순위와 의존성 우선순위를 비교하여 업데이트가 필요한지 확인
      if (includesSomeLane(dependencies.lanes, renderLanes)) {
        markWorkInProgressReceivedUpdate(); // 업데이트가 필요함을 마킹(표시)
      }

      dependencies.firstContext = null; // firstContext 초기화
    }
  }
}
  • Fiber 상태 초기화: 현재 렌더링 중인 Fiber 노드를 설정하고, 이전에 관찰된 컨텍스트 의존성을 초기화

  • 컨텍스트 의존성 관리: Fiber의 의존성을 검사하여 필요한 경우 업데이트 플래그를 설정하고, 의존성을 초기화

  • 렌더링 준비: 이 과정을 통해 해당 Fiber가 새로운 컨텍스트 값을 읽고 반영할 준비를 한다.

prepareToReadContext 함수는 updateContextConsumer에서만 호출되는 것이 아니라 모든 컴포넌트를 렌더링할 때 기본적으로 호출된다.

dependencies는 컴포넌트에서 사용된 모든 Context를 추적하는 것으로, 나중에 React는 Context 값이 변경될 때 어떤 컴포넌트를 업데이트해야 하는지 알 수 있다.


readContext, 컨텍스트 값을 읽고 의존성 리스트 업데이트

export function readContext<T>(context: ReactContext<T>): T {
  return readContextForConsumer(currentlyRenderingFiber, context);
}

function readContextForConsumer<T>(
  consumer: Fiber | null, // Context를 사용하는 컴포넌트의 Fiber 노드
  context: ReactContext<T>, // 읽으려는 context 객체
): T {
  // 렌더러에 맞게 context 값 선택
  const value = isPrimaryRenderer 
    ? context._currentValue
    : context._currentValue2;

  // 현재 context와 그 값을 포함하는 객체 생성, 연결 리스트의 노드 역할
  const contextItem = {
    context: ((context: any): ReactContext<mixed>),
    memoizedValue: value,
    next: null,
  };

  // 현재 처리 중인 컴포넌트가 어떤 Context도 사용하지 않은 상태에서 처음 Context 사용한 경우
  if (lastContextDependency === null) {
    lastContextDependency = contextItem; // 새 아이템 설정
    consumer.dependencies = { // dependencies 객체 초기화
      lanes: NoLanes,
      firstContext: contextItem,
    };
    
    if (enableLazyContextPropagation) {
      consumer.flags |= NeedsPropagation;
    }
  } else {
    // 새 아이템을 연결 리스트의 끝에 추가
    lastContextDependency = lastContextDependency.next = contextItem;
  }

  return value;
}
  • 컨텍스트 의존성 추적: 컴포넌트가 어떤 Context를 사용하는지 추적한다. 이 정보는 나중에 Context 값이 변경될 때 어떤 컴포넌트를 다시 렌더링해야 하는지 결정한다.

  • 연결 리스트 구조: 여러 Context를 사용하는 경우, 각 Context 의존성은 연결 리스트 형태로 저장된다. 이는 효율적인 메모리 사용과 빠른 순회를 가능하게 한다.


useContext는 readContext의 별칭이다.

const HooksDispatcherOnMount: Dispatcher = {
  useCallback: mountCallback,
  useContext: readContext, 
  useEffect: mountEffect,
  // another code..
};
const HooksDispatcherOnUpdate: Dispatcher = {
  useCallback: updateCallback,
  useContext: readContext,
  useEffect: updateEffect,
  // another code..
};

propagateContextChange, 컨텍스트 값의 변경을 전파

export function propagateContextChange<T>(
  workInProgress: Fiber, 
  context: ReactContext<T>, 
  renderLanes: Lanes, 
): void {
  propagateContextChange_eager(workInProgress, context, renderLanes);
}

function propagateContextChange_eager<T>(
  workInProgress: Fiber, // 현재 작업 중인 Fiber
  context: ReactContext<T>, // 변경된 컨텍스트
  renderLanes: Lanes, // 렌더링 우선순위
): void {
  let fiber = workInProgress.child;
  if (fiber !== null) {
    fiber.return = workInProgress;
  }
  while (fiber !== null) { // Fiber 트리 순회
    let nextFiber;

    const list = fiber.dependencies;
    if (list !== null) {
      nextFiber = fiber.child;

      let dependency = list.firstContext;
      while (dependency !== null) {
        if (dependency.context === context) {
          if (fiber.tag === ClassComponent) {
            const lane = pickArbitraryLane(renderLanes);
            const update = createUpdate(lane);
            update.tag = ForceUpdate; // 강제 업데이트 스케줄링

            const updateQueue = fiber.updateQueue;
            const sharedQueue: SharedQueue<any> = (updateQueue: any).shared;
            const pending = sharedQueue.pending;
            if (pending === null) {
              update.next = update;
            } else {
              update.next = pending.next;
              pending.next = update;
            }
            sharedQueue.pending = update;
          }

          fiber.lanes = mergeLanes(fiber.lanes, renderLanes);
          const alternate = fiber.alternate;
          if (alternate !== null) {
            alternate.lanes = mergeLanes(alternate.lanes, renderLanes);
          }
          scheduleContextWorkOnParentPath(
            fiber.return,
            renderLanes,
            workInProgress,
          );

          list.lanes = mergeLanes(list.lanes, renderLanes);
          break;
        }
        dependency = dependency.next;
      }
    } else if (fiber.tag === ContextProvider) {
      nextFiber = fiber.type === workInProgress.type ? null : fiber.child;
    } else if (fiber.tag === DehydratedFragment) {

      const parentSuspense = fiber.return;

      parentSuspense.lanes = mergeLanes(parentSuspense.lanes, renderLanes);
      const alternate = parentSuspense.alternate;
      if (alternate !== null) {
        alternate.lanes = mergeLanes(alternate.lanes, renderLanes);
      }

      scheduleContextWorkOnParentPath(
        parentSuspense,
        renderLanes,
        workInProgress,
      );
      nextFiber = fiber.sibling;
    } else {
      nextFiber = fiber.child;
    }

    if (nextFiber !== null) {
      nextFiber.return = fiber;
    } else {
      nextFiber = fiber;
      while (nextFiber !== null) {
        if (nextFiber === workInProgress) {
          nextFiber = null;
          break;
        }
        const sibling = nextFiber.sibling;
        if (sibling !== null) {
          sibling.return = nextFiber.return;
          nextFiber = sibling;
          break;
        }

        nextFiber = nextFiber.return;
      }
    }
    fiber = nextFiber;
  }
}

코드가 굉장히 길지만, 핵심 동작 부분만 요약하면 다음과 같다.

1. Fiber 트리를 순회하며 각 Fiber 노드를 검사

2. 각 Fiber 노드의 dependencies 리스트를 확인하여 변경된 context를 사용하는지 검사

3. 해당 context를 사용하는 컴포넌트를 발견한 경우

  • 컴포넌트의 lanes에 새로운 renderLanes를 병합하여 업데이트 우선순위를 설정한다.
  • scheduleContextWorkOnParentPath - 현재 컴포넌트에서 시작하여 루트까지 부모 경로를 따라 올라가면서 각 부모 컴포넌트에 context 변경 작업을 스케줄링한다.

4. Context Provider를 만나면 해당 Provider의 자식들만 검사한다.

5. DehydratedFragment(서버 렌더링 관련)를 처리한다.


📝 정리하기

- createContext
➔ Provider와 Consumer라는 속성을 가진 context 객체를 생성한다.


- updateContextProvider
➔ Context Provider의 새 값을 설정하고, 이전 값과 비교하여 변경사항을 처리한다.

💡 값이 변경되었다면 컨텍스트 변경을 전파(propagateContextChange)하고 그렇지 않다면 최적화(bail out)를 수행한다.


- Provider Render 과정(pushProvider, popProvider)
➔ 렌더링 과정에서 Provider를 만나면 Fiber Stack에 컨텍스트 값을 push하고, Provider 범위 밖으로 나가면 pop하여 이전에 저장된 상태로 돌아간다.


- updateContextConsumer
➔ 현재 Context 값을 읽어와 Consumer의 render 함수(children)를 실행하고, 그 결과로 새로운 자식 요소를 생성한다.

💡 이 새로운 자식 요소를 기존 Fiber 트리와 조정(reconcile)하여 필요한 업데이트를 수행한다.


- prepareToReadContext
➔ 이전에 관찰된 컨텍스트 의존성 정보를 초기화하고, 기존 의존성이 있다면 현재 렌더링 우선순위(lanes)와 관련이 있는지 확인하고 필요한 경우 업데이트 플래그를 설정한다.

💡 새로운 컨텍스트 읽기 작업을 위해 이전 상태를 정리하고, 필요한 경우 업데이트 플래그를 설정하는 준비 작업을 수행한다.


- readContext(useContext, Context.Consumer)
➔ 컨텍스트에서 현재 값을 가져온 후, 현재 렌더링 중인 Fiber 노드의 의존성 리스트에 컨텍스트와 가져온 값을 추가하고 반환하여 컴포넌트에서 사용할 수 있게 한다.

만약 이미 의존성 리스트가 존재한다면 기존 리스트의 끝에 추가하고, 첫 번째 의존성이라면 dependencies 객체를 초기화하고 firstContext로 설정한다.

💡 나중에 컨텍스트 값이 변경될 때 컴포넌트가 특정 컨텍스트에 의존한다는 정보를 React에 알리는 역할을 한다.


- propagateContextChange
➔ 컨텍스트 값이 변경되었을 때 호출되며, 변경된 컨텍스트를 사용하는 모든 하위 Fiber 노드(readContext를 통해 얻은 정보)를 탐색하여 해당 컨텍스트에 의존하는 각 Fiber 노드에 대해 업데이트를 스케줄링하고, 렌더링 우선순위(lanes)를 설정한다.

영향을 받는 각 컴포넌트에 대해 부모 경로를 따라 올라가면 각 부모 노드에 대해 context 변경 작업을 스케줄링한다.

💡 이 과정이 필요한 이유는 React.memoshouldComponentUpdate와 같은 최적화 기법을 사용하는 컴포넌트들도 Context 변경을 올바르게 처리할 수 있도록 한다.

클래스 컴포넌트의 경우 일반적인 업데이트를 스케줄링하며, 필요한 경우 컴포넌트의 updateQueue에 업데이트를 추가하고, 함수형 컴포넌트의 경우 useContext 훅을 통해 자동으로 리렌더링이 트리거된다.


🎯 Context Rendering Flow

1. Context Provider 렌더링

  • Context.Provider가 새로운 value로 렌더링되면, React는 이 값을 이전 값과 비교(Object.is)한다.

2. propagateContextChange 호출

  • 값이 변경되었음을 감지하면, React는 propagateContextChange 함수를 호출하여 변경된 Context를 사용하는 모든 하위 Fiber 노드에 대해 업데이트를 스케줄링한다.

  • 이 함수는 각 의존하는 Fiber 노드의 렌더링 우선순위(lanes)를 설정하고, 필요한 경우 부모 경로를 따라 Context 작업을 스케줄링한다.

3. Consumer 컴포넌트 업데이트

  • propagateContextChange 함수의 결과로, 해당 Context에 의존하는 Consumer 컴포넌트들이 업데이트 대상이 된다.

  • 이 과정에서 updateContextConsumer 함수를 통해 각 Consumer 컴포넌트는 새로운 Context 값으로 다시 렌더링된다.

4. 렌더링 및 커밋 단계

  • 새로운 컨텍스트 값으로 Consumer 컴포넌트들이 렌더링되고, 이전 렌더링 결과와 비교(diffing)하여 변경 사항이 있다면 DOM에 커밋된다.

👉 Consumer 관점에서 함수형 컴포넌트와 클래스 컴포넌트

함수형 컴포넌트, useContext 사용

function MyComponent() {
  const value = useContext(MyContext);
  return <div>{value}</div>;
}

장점

  • 간결하고 직관적인 문법
  • 여러 컨텍스트를 쉽게 사용 가능

클래스 컴포넌트, Context.Consumer or contextType 사용

// Context.Consumer(함수형 컴포넌트에서도 사용 가능)
class MyComponent extends React.Component {
  render() {
    return (
      <MyContext.Consumer>
        {value => <div>{value}</div>}
      </MyContext.Consumer>
    );
  }
}

// contextType(한 번에 하나의 컨텍스트만 사용 가능)
class MyComponent extends React.Component {
  static contextType = MyContext;
  render() {
    return <div>{this.context}</div>;
  }
}

장점

  • 클래스 컴포넌트의 생명주기 메서드 활용 가능
  • contextType을 사용하는 경우 this.context로 간단히 접근 가능

🤔 Consumer 방식 - Context.Consumer vs useContext

Context.Consumer

  • 작동 방식: Render Prop 패턴을 사용하여 컨텍스트 값을 자식 함수에 전달
  • 내부 처리: React는 Consumer를 만날 때마다 현재 컨텍스트 스택에서 해당 컨텍트스의 값을 찾아 전달한다.
  • 장점: 클래스 컴포넌트, 함수형 컴포넌트 모두 사용 가능
  • 단점: 중첩된 구조로 인해 코드가 복잡해질 수 있다.

useContext

  • 작동 방식: 훅을 통해 직접 컨텍스트 값을 컴포넌트에 주입
  • 내부 처리: React는 컴포넌트 렌더링 시 useContext를 호출을 감지하고, 해당 컨텍스트의 현재 값을 반환
  • 장점: 간결한 문법, 여러 컨텍스트를 쉽게 사용할 수 있다.
  • 단점: 함수형 컴포넌트에서만 사용 가능하다.

두 가지 방식이 나뉘게 된 이유

  • Context.Consumer는 React 16.3에서 새로운 Context API와 함께 도입되었으며, 클래스 컴포넌트 시대에 맞춰 설계되었다.

  • useContext는 React 16.8에서 훅(Hooks)과 함께 도입되고, 함수형 컴포넌트와 훅 패러다임에 맞춰 설계 되었다.


🙃 도움이 되었던 자료들

createContext - 리액트 공식 문서
useContext - 리액트 공식 문서
How does Context work internally in React?
React 源码解读之 Context(一)

profile
🏁

0개의 댓글