[React 공식문서 정독] useContext

김진서·2025년 5월 29일

우아한테크코스 7기

목록 보기
48/56
post-thumbnail

1. useContext?

  • useContext는 컴포넌트에서 Context를 읽고 구독할 수 있는 React Hook.
  • createContext로 생성한 Context 객체를 매개변수로 받으며, 이 Hook은 호출하는 컴포넌트 트리에서 상위 방향으로 가장 가까운 SomeContext.Provider에 전달된 value 값을 반환한다.
  • 이때, useContext()를 호출하는 컴포넌트 내부의 Provider는 고려되지 않는다. 상위에 Provider가 전혀 없을 경우, createContext에 지정한 기본값이 사용된다. Context 값이 변경되면 React는 해당 Context를 읽는 컴포넌트를 자동으로 다시 렌더링한다.

2. useContext 사용법

  • useContext는 컴포넌트의 최상위 수준에서 호출하여 Context 값을 읽고 구독할 수 있다. 이는 Context 값이 변경되면 해당 컴포넌트가 자동으로 다시 렌더링됨을 의미한다.
  • useContext를 사용하면 createContext로 생성된 특정 Context 객체를 매개변수로 전달하여, 컴포넌트 트리에서 상위 방향으로 가장 가까운 SomeContext.Provider에 전달된 value 값을 반환받는다.
  • 이 방식을 통해 트리의 깊은 곳에 있는 컴포넌트에서도 상위 컴포넌트에서 제공된 데이터를 props 드릴링 없이 쉽게 접근하고 전달받을 수 있다. Provider와 useContext를 호출하는 컴포넌트 사이에 얼마나 많은 컴포넌트 레이어가 있는지는 중요하지 않다.
import React, { createContext, useContext, useState } from 'react';

// 1) Context 생성 (기본값은 'light')
const ThemeContext = createContext<'light' | 'dark'>('light');

function App() {
  // 2) 상태로 테마 관리
  const [theme, setTheme] = useState<'light' | 'dark'>('light');

  return (
    // 3) Provider 로 값을 내려줌
    <ThemeContext.Provider value={theme}>
      <Toolbar />
      <button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}>
        토글 테마
      </button>
    </ThemeContext.Provider>
  );
}

function Toolbar() {
  return (
    <div>
      {/* 중간 컴포넌트를 거쳐도 props 드릴링 없이 사용할 수 있음 */}
      <ThemedButton />
    </div>
  );
}

function ThemedButton() {
  // 4) 최상위에서 useContext 호출 → 가장 가까운 Provider의 value를 반환
  const theme = useContext(ThemeContext);

  return (
    <button
      style={{
        background: theme === 'light' ? '#eee' : '#333',
        color: theme === 'light' ? '#000' : '#fff',
      }}
    >
      현재 테마: {theme}
    </button>
  );
}

export default App;

3. Context 값 업데이트

  • Context 값이 시간이 지남에 따라 변경되도록 하려면 React의 State와 Context를 결합해야 한다. 부모 컴포넌트에서 State 변수를 선언하고, 이 현재 State 값을 Context Provider의 value prop으로 전달한다.
  • 이렇게 하면 Provider에 전달된 State 변수 값이 업데이트될 때 (State setter 함수 호출 등을 통해), 해당 Provider는 새로운 Context 값을 받게 된다. React는 이 변화를 감지하여 해당 Context를 읽는 모든 컴포넌트를 자동으로 다시 렌더링한다. 이를 통해 트리의 깊은 곳에 있는 컴포넌트들이 최신 Context 값을 받도록 할 수 있다.
  • createContext 호출 시 지정하는 기본값(defaultValue)은 변경되지 않는다. Context 값을 업데이트하려면 반드시 State와 함께 사용해야 한다.
import React, { createContext, useState, useContext } from 'react';

// 1) Context 생성 (기본값은 [0, () => {}])
const CountContext = createContext([0, () => {}]);

// 2) Provider 컴포넌트: 내부에서 State를 관리하고 value로 전달
function CountProvider({ children }) {
  const [count, setCount] = useState(0);
  return (
    <CountContext.Provider value={[count, setCount]}>
      {children}
    </CountContext.Provider>
  );
}

// 3) Context 값을 읽어 표시하는 컴포넌트
function CounterDisplay() {
  const [count] = useContext(CountContext);
  return <p>현재 카운트: {count}</p>;
}

// 4) Context 통해 받은 setter로 값을 업데이트하는 컴포넌트
function CounterButton() {
  const [, setCount] = useContext(CountContext);
  return <button onClick={() => setCount(c => c + 1)}>증가</button>;
}

// 5) App에서 Provider로 감싸고 자식 컴포넌트들 렌더링
export default function App() {
  return (
    <CountProvider>
      <CounterDisplay />
      <CounterButton />
    </CountProvider>
  );
}

4. 기본값(Fallback) 지정

  • React는 컴포넌트 트리에서 상위 방향으로 특정 Context Provider를 찾을 수 없는 경우가 있다. 이럴 때 useContext()가 반환하는 Context 값은 해당 Context를 createContext 호출 시 지정한 기본값과 동일하게 된다.
  • 예를 들어, const ThemeContext = createContext('light');와 같이 Context를 생성할 때 'light'라는 기본값을 지정할 수 있다. 이 기본값은 Provider가 없을 때 사용되며, 실수로 Provider 없이 컴포넌트가 렌더링되어도 깨지지 않게 하거나 테스트에 유용할 수 있다.
  • 중요한 점은 createContext에 지정된 기본값은 변경되지 않는다. Context 값이 업데이트되도록 하려면 반드시 Provider를 사용하고 State와 결합해야 한다. 부모 트리에 <SomeContext.Provider value={undefined}>와 같이 value가 undefined인 Provider가 있다면, useContext는 기본값 대신 undefined를 반환한다. 기본값은 위에 일치하는 Provider가 전혀 없는 경우에만 사용된다.
import React, { createContext, useContext } from 'react';

// 1) 기본값 'light'로 생성
const ThemeContext = createContext<'light' | 'dark'>('light');

function DisplayTheme({ label }: { label: string }) {
  const theme = useContext(ThemeContext);
  return (
    <p>
      {label}: 현재 테마는 <strong>{theme}</strong>입니다.
    </p>
  );
}

export default function App() {
  return (
    <div>
      {/* Provider가 없으므로 기본값 'light'가 사용됨 */}
      <DisplayTheme label="Fallback 예시" />

      {/* value를 undefined로 넘긴 Provider가 있으므로 undefined가 반환됨 */}
      <ThemeContext.Provider value={undefined as any}>
        <DisplayTheme label="Undefined Provider 예시" />
      </ThemeContext.Provider>

      {/* 실제 Provider를 사용해 값을 'dark'로 설정 */}
      <ThemeContext.Provider value="dark">
        <DisplayTheme label="정상 Provider 예시" />
      </ThemeContext.Provider>
    </div>
  );
}

5. Context 오버라이딩

  • Context 오버라이딩은 트리의 특정 부분을 다른 값의 Provider로 감싸서 해당 부분에 대한 Context 값을 변경하는 것을 의미한다. 이렇게 하면 부모 트리에서 제공되는 Context 값과 다른 값을 해당 하위 트리 컴포넌트들이 사용하게 된다.
  • React에서는 필요한 만큼 Provider를 중첩하여 Context를 오버라이딩 할 수 있다. 가장 가까운 상위 Provider의 value가 useContext 호출 컴포넌트에게 전달되기 때문에, 중첩된 Provider는 해당 Provider 내부에 있는 컴포넌트들에게 새로운 Context 값을 제공하게 된다.
import React, { createContext, useContext } from 'react';

// 1) Context 생성 (기본값 'light')
const ThemeContext = createContext<'light' | 'dark'>('light');

// 2) Context 값을 읽어 화면에 표시하는 컴포넌트
function DisplayTheme({ label }: { label: string }) {
  const theme = useContext(ThemeContext);
  return (
    <p>
      {label}: 현재 테마는 <strong>{theme}</strong>입니다.
    </p>
  );
}

export default function App() {
  return (
    <div>
      {/* 3) 최상위 Provider: 전체 트리에 'light' 테마 제공 */}
      <ThemeContext.Provider value="light">
        <DisplayTheme label="전체 트리(최상위) 예시" />

        {/* 4) 중첩된 Provider: 이 하위 트리만 'dark' 테마로 오버라이딩 */}
        <ThemeContext.Provider value="dark">
          <DisplayTheme label="오버라이딩된 서브트리 예시" />
        </ThemeContext.Provider>

        {/* 5) 다시 바깥 Provider 값으로 돌아감 */}
        <DisplayTheme label="다시 최상위 테마 예시" />
      </ThemeContext.Provider>
    </div>
  );
}

6. 리렌더링 최적화 (객체와 함수 전달 시)

  • Context를 통해 객체나 함수를 포함한 모든 종류의 값을 전달할 수 있다. 하지만 Context Provider에 객체나 함수를 value로 전달할 때 주의할 점이 있다. 부모 컴포넌트가 리렌더링될 때, 새로운 객체나 함수가 매번 생성되어 Provider의 value prop으로 전달될 수 있다.
  • React는 Context 값이 변경되었는지 Object.is를 통해 비교하고, 새로운 객체나 함수는 이전 것과 다르다고 판단한다. 이 경우, 해당 Context를 읽는 모든 하위 컴포넌트가 자동으로 다시 렌더링된다. 작은 앱에서는 문제가 되지 않을 수 있지만, 불필요한 리렌더링을 막기 위한 성능 최적화가 필요할 수 있다.
  • 이 문제를 최적화하려면 React의 다른 훅인 useCallback과 useMemo를 함께 사용해야 한다.
    • useCallback: Context를 통해 전달하는 함수를 useCallback으로 감싸면, 함수의 의존성이 변경되지 않는 한 동일한 함수 참조를 유지할 수 있다.
    • useMemo: Context value로 전달하는 객체 생성을 useMemo로 감싸면, 객체의 의존성(예: 객체 내부의 상태 값이나 함수)이 변경되지 않는 한 동일한 객체 참조를 유지할 수 있다.
  • 이처럼 useCallbackuseMemo를 사용하여 Provider의 value prop으로 전달되는 객체와 함수의 참조를 안정화하면, 부모 컴포넌트가 리렌더링되더라도 실제 Context 값이 변경되지 않았다면 Context를 사용하는 하위 컴포넌트의 불필요한 리렌더링을 방지할 수 있다.
import React, {
  createContext,
  useContext,
  useState,
  useCallback,
  useMemo,
} from 'react';

// 1) Context 생성 (기본값 형태만 정의)
type CounterContextType = {
  count: number;
  increment: () => void;
};
const CounterContext = createContext<CounterContextType>({
  count: 0,
  increment: () => {},
});

// 2) Provider 컴포넌트: state, useCallback, useMemo 결합
function CounterProvider({ children }: { children: React.ReactNode }) {
  const [count, setCount] = useState(0);

  // increment 함수는 의존성이 없으므로 참조가 고정됨
  const increment = useCallback(() => {
    setCount((c) => c + 1);
  }, []);

  // 객체 형태의 value를 useMemo로 묶어, count가 바뀔 때만 새로운 참조가 생성되도록 함
  const value = useMemo(
    () => ({ count, increment }),
    [count, increment]
  );

  return (
    <CounterContext.Provider value={value}>
      {children}
    </CounterContext.Provider>
  );
}

// 3) Context를 소비하는 컴포넌트
function CounterDisplay() {
  const { count, increment } = useContext(CounterContext);
  return (
    <div>
      <p>현재 카운트: {count}</p>
      <button onClick={increment}>증가</button>
    </div>
  );
}

// 4) App에서 Provider로 감싸기
export default function App() {
  return (
    <CounterProvider>
      <CounterDisplay />
    </CounterProvider>
  );
}

7. 문제 해결

  • 컴포넌트가 Provider의 값을 인식하지 못하는 경우:
    • useContext()를 호출하는 컴포넌트와 동일한 컴포넌트 내부 또는 그 아래에 <SomeContext.Provider>를 렌더링했을 수 있다. Provider는 useContext()를 호출하는 컴포넌트 상위에 배치되어야 한다.
    • 컴포넌트를 <SomeContext.Provider>로 감싸는 것을 잊었거나 예상과 다른 위치에 배치했을 수 있다. React 개발자 도구를 사용하여 컴포넌트 계층 구조를 확인해 보자.
    • 빌드 시스템 문제(예: 심볼릭 링크)로 인해 Context 객체 자체가 서로 다르게 처리될 수 있다. Provider에서 사용하는 SomeContext와 값을 읽는 곳에서 사용하는 SomeContext가 정확히 동일한 객체인지 확인해야 한다.
import React, { createContext, useContext } from 'react';

// Context 생성 (기본값 'default')
const MyContext = createContext<string>('default');

function Problem1() {
  return (
    <div>
      {/* Provider가 아직 내려오기 전이어서 기본값 'default'를 읽습니다 */}
      <Consumer label="문제1 – Provider 이전" />

      {/* 이 Provider는 Consumer 아래에 있으므로, 여기서만 'provided'가 읽힙니다 */}
      <MyContext.Provider value="provided">
        <Consumer label="Provider 내부" />
      </MyContext.Provider>
    </div>
  );
}

function Consumer({ label }: { label: string }) {
  const value = useContext(MyContext);
  return <p>{label}: {value}</p>;
}

export default Problem1;  
  • 기본값이 있는데 Context가 undefined를 반환하는 경우:
    • 트리 어딘가에 <SomeContext.Provider> 컴포넌트가 있지만, value prop을 지정하는 것을 잊었을 수 있다. value prop이 없으면 value={undefined}를 전달하는 것과 동일하게 작동한다. 실수로 다른 Prop의 이름을 사용했을 수도 있다. React는 이 경우 콘솔에 경고를 표시한다.
    • createContext(defaultValue) 호출의 기본값은 위에 일치하는 Provider가 전혀 없는 경우에만 사용된다. 부모 트리 어딘가에 <SomeContext.Provider value={undefined}> 컴포넌트가 있는 경우, useContext(SomeContext)는 기본값 대신 undefined를 Context 값으로 반환한다.
import React, { createContext, useContext } from 'react';

// Context 생성 (기본값 'default')
const MyContext = createContext<string>('default');

function Problem2() {
  return (
    // value가 지정되지 않으면 useContext는 undefined를 반환합니다
    <MyContext.Provider>
      <Consumer label="문제2 – value 누락" />
    </MyContext.Provider>
  );
}

function Consumer({ label }: { label: string }) {
  const value = useContext(MyContext);
  return <p>{label}: {String(value)}</p>;
}

export default Problem2;

8. 요약 및 정리

  • useContext는 컴포넌트에서 가장 가까운 Provider가 전달하는 값을 읽고, 그 값이 변경되면 자동으로 리렌더링을 트리거하는 Hook.
  • Provider가 없으면 createContext의 기본값(defaultValue)이 반환되고, value를 undefined로 넘기면 기본값 대신 undefined가 사용된다.
  • Context로 객체나 함수를 전달할 땐 매 렌더링마다 새로운 참조가 생겨 불필요한 리렌더링이 발생하므로 useMemo/useCallback으로 값과 함수를 메모이제이션해야 한다.
profile
PAy IT forwaRD를 실천하는 프론트엔드 개발자.

0개의 댓글