React에서 useContext는 컴포넌트 트리에서 데이터를 효율적으로 공유하기 위한 훅입니다.
주로 전역 상태 관리나 상위 컴포넌트에서 내려주는 데이터를 자식 컴포넌트들에서 쉽게 사용할 수 있도록 도와줍니다.
React의 기본적인 데이터 흐름은 단방향 데이터 흐름입니다.
즉, 부모 컴포넌트가 자식 컴포넌트에게 props를 통해 데이터를 전달하는 방식입니다.
그러나 컴포넌트 구조가 깊어질수록 중간 컴포넌트들이 단순히 데이터를 전달하는 역할만 하게 되고, 이를 위해 계속해서 props를 내려줘야 합니다. 이를 prop drilling이라고 부릅니다.
useContext는 이런 prop drilling을 피하고, 상위 컴포넌트의 데이터를 하위 컴포넌트에 직접 전달할 수 있게 해줍니다.
상태가 여러 컴포넌트에서 공유될 때, useContext를 사용하면 훨씬 더 효율적으로 데이터를 관리할 수 있습니다.
useContext는 기본적으로 Context 객체를 인자로 받아서 해당 Context의 값을 반환하는 훅입니다.
내부적으로 Context.Provider가 제공하는 값을 자동으로 구독(subscribe)하여, 해당 값이 변경되면 컴포넌트가 다시 렌더링됩니다.
//CounterContext.ts
import { createContext } from "react";
type CounterContextType = {
count: number;
increment: () => void;
decrement: () => void;
reset: () => void;
};
export const CounterContext = createContext<CounterContextType | null>(null);
//CounterProvider.tsx
import { useState } from "react";
import { CounterContext } from "../CounterContext";
export default function CounterProvider({
children,
}: {
children: React.ReactNode;
}) {
const [count, setCount] = useState(0);
const increment = () => {
setCount((prev) => prev + 1);
};
const decrement = () => {
setCount((prev) => prev - 1);
};
const reset = () => {
setCount(0);
};
return (
<CounterContext.Provider value={{ count, increment, decrement, reset }}>
{children}
</CounterContext.Provider>
);
}
CounterProvider를 구독하는 컴포넌트에서 useContext를 사용하여 필요한 값을 가져올 수 있습니다.
//main.tsx
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import App from "./App.tsx";
import "./css/index.css";
import CounterProvider from "./context/provider/CounterProvider.tsx";
createRoot(document.getElementById("root")!).render(
<StrictMode>
<CounterProvider>
<App />
</CounterProvider>
</StrictMode>
);
Provider는 범위를 지정해 주는 것일 뿐, App의 상위 노드가 아닙니다.
//CompA.tsx
import { useContext } from "react";
import CompoB from "./CompoB";
import { CounterContext } from "../context/CounterContext";
export default function CompoA() {
const { count, increment } = useContext(CounterContext)!;
console.log("A rendering");
return (
<>
<h1>A</h1>
<div>
<div>{count}</div>
<button onClick={increment}>A증가</button>
</div>
<CompoB />
</>
);
}
버튼을 클릭하면 다음과 같이 출력됩니다.

버튼을 클릭하면 provider에 있는 state가 업데이트되고, Context를 구독하고 있는 모든 컴포넌트가 리렌더링됩니다.
CounterContext에서 제공하는 count가 업데이트되면, 새로운 값을 가진 객체가 생성되므로 CompoA와 CompoB 모두 다시 렌더링됩니다.
//CompB.tsx
import React from "react";
import CompoC from "./CompoC";
export default React.memo(function CompoB() {
console.log("B rendering");
return (
<>
<h1>B</h1>
<CompoC />
</>
);
});
count와 increment를 별도의 Context로 분리하여, 필요한 부분만 리렌더링되도록 합니다.
상태와 상태 업데이트 함수를 분리합니다.
//CounterContext.ts
import { createContext } from "react";
type CounterContextType = {
count: number;
};
type CounterActionContextType = {
increment: () => void;
decrement: () => void;
reset: () => void;
};
export const CounterContext = createContext<CounterContextType | null>(null);
export const CounterActionContext = createContext<CounterActionContextType | null>(null);
proiver 수정
count와 관련된 Context와 increment와 관련된 Context를 따로 제공합니다.
useMemo로 동작을 메모이제이션합니다.
increment, decrement, reset 함수가 불필요하게 재생성되는 것을 방지합니다.
//CounterProvider.tsx
import { useMemo, useState } from "react";
import { CounterActionContext, CounterContext } from "../CounterContext";
export default function CounterProvider({
children,
}: {
children: React.ReactNode;
}) {
const [count, setCount] = useState(0);
const increment = () => {
setCount((prev) => prev + 1);
};
const decrement = () => {
setCount((prev) => prev - 1);
};
const reset = () => {
setCount(0);
};
const memo = useMemo(() => ({ increment, decrement, reset }), []);
return (
<CounterActionContext.Provider value={memo}>
<CounterContext.Provider value={{ count }}>
{children}
</CounterContext.Provider>
</CounterActionContext.Provider>
);
}
useContext 사용하는 컴포넌트 수정
//CompA.tsx
import { useContext } from "react";
import CompoB from "./CompoB";
import { CounterContext } from "../context/CounterContext";
export default function CompoA() {
const { count } = useContext(CounterContext)!;
console.log("A rendering");
return (
<>
<h1>A</h1>
<div>
<div>{count}</div>
</div>
<CompoB />
</>
);
}
//CompB.tsx
import React, { useContext } from "react";
import CompoC from "./CompoC";
import { CounterActionContext } from "../context/CounterContext";
export default React.memo(function CompoB() {
const { increment, decrement, reset } = useContext(CounterActionContext)!;
console.log("B rendering");
return (
<>
<h1>B</h1>
<button onClick={increment}>증가</button>
<button onClick={decrement}>감소</button>
<button onClick={reset}>리셋</button>
<CompoC />
</>
);
});
이제 버튼을 눌러도 count를 렌더링하는 컴포넌트A만 리렌더링됩니다.
