카테고리 없음

리액트 Context 정리

harubanbo 2026. 5. 23. 23:31

React에서 Context 개념이 정리용으로 요약해서 남기는 글이다.

1. 컨텍스트란?

React Context는 props drilling 문제를 해결하기 위한 기능이다.

  • 상위 컴포넌트가 데이터를 제공하면, 하위 컴포넌트는 props로 전달받지 않고 직접 사용 가능
  • 핵심 3요소: createContextProvideruseContext

Context 사용 방식에는 useContext 훅을 사용하는 방법과 Consumer라는 것을 사용하는 방법이 있으나 함수형에서는 useContext가 사실상 표준으로 Consumer는 클래스 컴포넌트나 일부 조건부 렌더링 같은 특수 케이스에서만 쓴다고 한다.


2. 기본 사용 예제

import { createContext, useContext } from 'react';

// 1) 컨텍스트 객체 생성
const ThemeContext = createContext<'light' | 'dark'>('light');

// 2) Provider로 값 제공
function App() {
  return (
    <ThemeContext.Provider value="dark">
      <Page />
    </ThemeContext.Provider>
  );
}

// 3) 사용처에서 useContext로 소비
function Page() {
  const theme = useContext(ThemeContext);
  return <div>현재 테마: {theme}</div>;
}

3. 리렌더링 동작

예제 코드

// src/app/test2/page.tsx
'use client';

import ComponentA from "@/component/test2/ComponentA";
import ComponentB from "@/component/test2/ComponentB";
import CounterProvider from "@/component/test2/CounterProvider";
import { useState } from "react";

export default function ContextPage() {
    const parentCounter = useState(7);
    console.log("page");
    return (
        <CounterProvider parentCounter={parentCounter}>
            <h1>Context 리렌더링 테스트</h1>
            <ComponentA />
            <ComponentB />
        </CounterProvider>
    );
}
// src/component/test2/CounterProvider.tsx
import React, { createContext, useContext, useState } from "react";

const counterContext = createContext<[
    [number, React.Dispatch<React.SetStateAction<number>>],
    [number, React.Dispatch<React.SetStateAction<number>>]
]>([
    [0, () => {}],
    [0, () => {}],
]);

// 부모로부터 값을 받아오거나 프로바이더 자체도 값을 가지고 있다.
export default function CounterProvider({
    children,
    parentCounter,
}: {
    children: React.ReactNode;
    parentCounter: [number, React.Dispatch<React.SetStateAction<number>>];
}) {
    const providerCounter = useState(0);
    console.log("CounterProvider");
    return (
        <counterContext.Provider value={[providerCounter, parentCounter]}>
            {children}
        </counterContext.Provider>
    );
}

export function useCounterContext() {
    return useContext(counterContext);
}
// src/component/test2/ComponentA.tsx
import ComponentA_1 from "./ComponentA_1";

export default function ComponentA() {
    console.log("ComponentA");
    return (
        <>
            <div>ComponentA</div>
            <ComponentA_1 />
        </>
    );
}
// src/component/test2/ComponentA_1.tsx
import { useCounterContext } from "./CounterProvider";

export default function ComponentA_1() {
    const [, parentCounter] = useCounterContext();
    const [counter, setCounter] = parentCounter;
    console.log("ComponentA_1");
    return (
        <div>
            <button onClick={() => setCounter(counter + 1)}>
                ComponentA_1 parent button: {counter}
            </button>
        </div>
    );
}
// src/component/test2/ComponentB.tsx
import ComponentB_1 from "./ComponentB_1";

export default function ComponentB() {
    console.log("ComponentB");
    return (
        <>
            <div>ComponentB</div>
            <ComponentB_1 />
        </>
    );
}
// src/component/test2/ComponentB_1.tsx
import { useCounterContext } from "./CounterProvider";

export default function ComponentB_1() {
    const [providerCounter] = useCounterContext();
    const [counter, setCounter] = providerCounter;
    console.log("ComponentB_1");
    return (
        <div>
            <button onClick={() => setCounter(counter + 1)}>
                ComponentB_1 provider button: {counter}
            </button>
        </div>
    );
}

트리 구조

ContextPage (parentCounter state 소유)
└─ CounterProvider (providerCounter state 소유, context value 제공)
   ├─ ComponentA
   │  └─ ComponentA_1  ← parentCounter 구독
   └─ ComponentB
      └─ ComponentB_1  ← providerCounter 구독

각 버튼 클릭 시 console 출력

ComponentB_1 버튼 클릭providerCounter 변경 (state 소유자: CounterProvider)

CounterProvider
ComponentA_1
ComponentB_1

ComponentA_1 버튼 클릭parentCounter 변경 (state 소유자: ContextPage)

page
CounterProvider
ComponentA
ComponentA_1
ComponentB
ComponentB_1

리렌더링 규칙 (자주 하는 착각 정정)

여기서 ComponentB_1 버튼을 누르면 중간에 ComponentA와 ComponentB도 리렌더링 될 것이라고 생각할 수 있으나 그렇지 않다.

착각: "부모가 리렌더되면 자식은 모두 리렌더된다."

정확한 규칙:

  1. 컴포넌트는 자기 상태(state)나 props가 변할 때만 리렌더된다.
  2. 리렌더된 컴포넌트의 JSX 본문 안에서 새로 생성된 자식 React Element만 리렌더 대상이 된다.
  3. {children}처럼 props로 받은 엘리먼트는 호출자가 만든 객체다. 호출자가 리렌더되지 않으면 객체 참조가 그대로라서 리액트는 그 서브트리 호출을 생략한다.
  4. 컨텍스트를 사용하는 측은 트리 구조와 무관하게, Provider의 value가 바뀌면 별도로 리렌더가 예약된다.

"children as props" 패턴

<ComponentA/>, <ComponentB/> 엘리먼트는 ContextPage가 만든 객체다. CounterProvider는 그것을 children으로 받아서 그대로 꽂아둘 뿐이다.

CounterProvider가 자기 state 변경으로 리렌더돼도, children 참조가 그대로라서 ComponentA, ComponentB는 호출되지 않는다.

→ 단, ComponentA_1, ComponentB_1useContext 구독자이므로 컨텍스트 값이 바뀌면 별도로 리렌더된다.

핵심: "JSX 트리상의 부모-자식"과 "엘리먼트를 만든 책임자"는 다르다. 리렌더 전파는 후자(엘리먼트를 만든 컴포넌트)를 기준으로 일어난다.


4. 동일 컨텍스트를 중첩해서 쓰는 경우

규칙: 가장 가까운(closest) 상위 Provider의 값이 적용된다.

Parent
└─ <ThemeContext value="dark">          ← (A)
   ├─ Child1   ─ useContext → "dark"   (A가 가장 가까움)
   └─ <ThemeContext value="light">      ← (B)
      └─ Child2 ─ useContext → "light" (B가 가장 가까움)

활용: 전체는 dark 테마지만, 특정 모달/섹션만 light로 덮어쓰고 싶을 때 같은 부분 오버라이드가 가능하다.


5. 서로 다른 컨텍스트를 중첩해서 쓰는 경우

핵심: 순서는 동작에 영향이 없다. 두 컨텍스트가 서로 독립적이면 누가 상위든 하위든 결과는 같다.

// 둘은 동작상 동일
<ThemeContext.Provider value="dark">
  <UserContext.Provider value={user}>
    <App />
  </UserContext.Provider>
</ThemeContext.Provider>

<UserContext.Provider value={user}>
  <ThemeContext.Provider value="dark">
    <App />
  </ThemeContext.Provider>
</UserContext.Provider>