React context api
2025-10-16
1) Context API 핵심 정리
컴포넌트 트리 전체에 걸쳐 전역적인 데이터를 공유할 수 있게 해주는 기능임 prop drilling 문제를 해소할 수 있음.
-
무엇? 컴포넌트 트리 전체에 걸쳐 전역 데이터(상태/액션)를 prop drilling 없이 전달하는 메커니즘.
-
왜? 하위 단계가 깊어질수록 props 전달이 복잡해지는 문제(prop drilling)를 해결.
-
어떻게?
createContext로 컨텍스트 생성Provider에서 값(value) 공급- 하위에서
useContext로 소비
prop drilling
React에서 데이터를 여로 컴포넌트 계층을 통해 전달하는 과정을 의미 (하위 컴포넌트가 많아질수록 데이터를 전달하는 코드가 복잡 해져서 복잡성이 증가함)
Flow
Context 생성: createContext 함수를 사용하여 context를 생성. Provider: 생성한 context의 Provider 컴포넌트를 사용하여 하위 컴포넌트들에게 데이터를 전달. Consumer: Consumer 컴포넌트 또는 useContext 훅을 사용하여 context의 데이터를 가져옴.
State Reducer
React에서 상태 관리 로직이 복잡해질 때 유용하게 사용할 수 있는 도구 State를 업데이트 하는 모든 로직을 reducer를 사용하여 컴포넌트 외부 단일 함수로 통합해 관리가 가능하다.
Flow
Reducer 함수: 현재 상태와 액션 객체를 받아 새로운 상태를 반환하는 함수 Action 객체: 상태를 업데이트하기 위한 정보를 담고 있는 객체. Dispatch 함수: 액션 객체를 리듀서 함수에 전달하는 함수.
2) 구현 Data Flow & 폴더 구조
src/
├── datafetchs/ # ✅ HTTP 통신(API 호출) 모듈
│ └── daemonsets.api.ts
├── store/ # ✅ 전역/최상위 상태관리
│ ├── contexts/
│ │ ├── monitoring/
│ │ │ ├── daemonsets.context.tsx # Context + Provider
│ │ │ └── daemonsets.reducer.ts # Reducer + Action 정의
│ └── hooks/
│ └── useDaemonSets.ts # Custom Hook (selector/액션 래핑)
├── pages/
│ └── monitoring/
│ ├── DaemonSetsPage.tsx # 리스트 화면 (Control Props 지원)
│ └── DaemonSetsPodPage.tsx # 상세/Pod 화면
└── app/
└── routes.tsx # 라우팅(Provider 배치, Outlet)
데이터 흐름(Flux-like 단방향)
Component(Event) → dispatch(Action) → reducer(State 변경) → Context value 업데이트 → UI 렌더
3) 구현 예시
3-1. datafetchs (API 호출)
// src/datafetchs/daemonsets.api.ts
import axios from "axios";
export interface DaemonSetItem {
Name: string;
Namespace: string;
// ...필요 필드들
}
export const getAllDaemonSets = async (): Promise<DaemonSetItem[]> => {
const { data } = await axios.get("/api/daemonsets");
return data;
};
export const downloadDaemonSet = async (ns: string, name: string) => {
const { data } = await axios.get(`/api/daemonsets/${ns}/${name}/download`, {
responseType: "blob",
});
return data;
};
export const deleteDaemonSet = async (ns: string, name: string) => {
await axios.delete(`/api/daemonsets/${ns}/${name}`);
};
3-2. Reducer + Actions
// src/store/contexts/monitoring/daemonsets.reducer.ts
import { DaemonSetItem } from "@/datafetchs/daemonsets.api";
export type DaemonSetsState = {
list: DaemonSetItem[];
loading: boolean;
error?: string | null;
};
export const initialDaemonSetsState: DaemonSetsState = {
list: [],
loading: false,
error: null,
};
export type DaemonSetsAction =
| { type: "GET_ALL_REQUEST" }
| { type: "GET_ALL_SUCCESS"; payload: DaemonSetItem[] }
| { type: "GET_ALL_FAILURE"; payload: string }
| { type: "DELETE_SUCCESS"; payload: { Namespace: string; Name: string } };
export function daemonSetsReducer(
state: DaemonSetsState,
action: DaemonSetsAction
): DaemonSetsState {
switch (action.type) {
case "GET_ALL_REQUEST":
return { ...state, loading: true, error: null };
case "GET_ALL_SUCCESS":
return { ...state, loading: false, list: action.payload };
case "GET_ALL_FAILURE":
return { ...state, loading: false, error: action.payload };
case "DELETE_SUCCESS": {
const { Namespace, Name } = action.payload;
return {
...state,
list: state.list.filter(
(d) => !(d.Namespace === Namespace && d.Name === Name)
),
};
}
default:
return state;
}
}
3-3. Context + Provider (State Reducer + Context API)
// src/store/contexts/monitoring/daemonsets.context.tsx
import {
createContext,
useContext,
useEffect,
useReducer,
ReactNode,
useCallback,
} from "react";
import {
daemonSetsReducer,
initialDaemonSetsState,
DaemonSetsAction,
DaemonSetsState,
} from "./daemonsets.reducer";
import {
getAllDaemonSets,
deleteDaemonSet,
downloadDaemonSet,
} from "@/datafetchs/daemonsets.api";
type DaemonSetsContextValue = {
state: DaemonSetsState;
dispatch: React.Dispatch<DaemonSetsAction>;
actions: {
refresh: () => Promise<void>;
download: (ns: string, name: string) => Promise<void>;
remove: (ns: string, name: string) => Promise<void>;
};
};
const DaemonSetsContext = createContext<DaemonSetsContextValue | null>(null);
export const DaemonSetsProvider = ({ children }: { children: ReactNode }) => {
const [state, dispatch] = useReducer(daemonSetsReducer, initialDaemonSetsState);
const refresh = useCallback(async () => {
dispatch({ type: "GET_ALL_REQUEST" });
try {
const list = await getAllDaemonSets();
dispatch({ type: "GET_ALL_SUCCESS", payload: list });
} catch (e: any) {
dispatch({ type: "GET_ALL_FAILURE", payload: e?.message ?? "error" });
}
}, []);
const download = useCallback(async (ns: string, name: string) => {
await downloadDaemonSet(ns, name);
// 필요시 파일 저장 처리(FileSaver 등)
}, []);
const remove = useCallback(async (ns: string, name: string) => {
await deleteDaemonSet(ns, name);
dispatch({ type: "DELETE_SUCCESS", payload: { Namespace: ns, Name: name } });
}, []);
useEffect(() => {
// 첫 렌더 시 전체 조회
refresh();
}, [refresh]);
return (
<DaemonSetsContext.Provider
value={{ state, dispatch, actions: { refresh, download, remove } }}
>
{children}
</DaemonSetsContext.Provider>
);
};
export const useDaemonSetsContext = () => {
const ctx = useContext(DaemonSetsContext);
if (!ctx) throw new Error("useDaemonSetsContext must be used within Provider");
return ctx;
};
3-4. Custom Hook (Selector/액션 묶기)
// src/store/hooks/useDaemonSets.ts
import { useMemo } from "react";
import { useDaemonSetsContext } from "../contexts/monitoring/daemonsets.context";
export const useDaemonSets = () => {
const { state, actions } = useDaemonSetsContext();
const { list, loading, error } = state;
const namespaceMap = useMemo(() => {
const m = new Map<string, number>();
list.forEach((d) => m.set(d.Namespace, (m.get(d.Namespace) ?? 0) + 1));
return m;
}, [list]);
return { list, loading, error, namespaceMap, ...actions };
};
3-5. Page 컴포넌트 (Control Props + Context 소비)
Control Props: 부모가
namespace,daemonSet등을 외부에서 제어할 수도 있고, 전달이 없으면 내부 상태/컨텍스트로 동작.
// src/pages/monitoring/DaemonSetsPage.tsx
import { useMemo, useState } from "react";
import { useDaemonSets } from "@/store/hooks/useDaemonSets";
type Props = {
namespace?: string; // 🔸 외부 제어(Controlled) 가능
onSelectChange?: (ns: string) => void; // 🔸 외부 제어 콜백
};
export default function DaemonSetsPage({ namespace, onSelectChange }: Props) {
const { list, loading, error, remove, download } = useDaemonSets();
// 내부 상태(언컨트롤드). 외부 props가 오면 그 값을 우선 사용.
const [innerNs, setInnerNs] = useState<string>("default");
const effectiveNs = namespace ?? innerNs;
const filtered = useMemo(
() => list.filter((d) => (effectiveNs ? d.Namespace === effectiveNs : true)),
[list, effectiveNs]
);
const handleChangeNs = (ns: string) => {
if (onSelectChange) onSelectChange(ns);
else setInnerNs(ns);
};
if (loading) return <div>Loading…</div>;
if (error) return <div style={{ color: "red" }}>{error}</div>;
return (
<div>
<h2>DaemonSets ({effectiveNs || "All"})</h2>
<select
value={effectiveNs}
onChange={(e) => handleChangeNs(e.target.value)}
>
<option value="default">default</option>
<option value="kube-system">kube-system</option>
{/* 필요시 동적 namespace 목록으로 대체 */}
</select>
<ul style={{ marginTop: 12 }}>
{filtered.map((d) => (
<li key={`${d.Namespace}:${d.Name}`}>
{d.Namespace}/{d.Name}
<button onClick={() => download(d.Namespace, d.Name)}>download</button>
<button onClick={() => remove(d.Namespace, d.Name)}>delete</button>
</li>
))}
</ul>
</div>
);
}
3-6. 라우팅 + Provider 배치 (Outlet 패턴)
// src/app/routes.tsx
import { Outlet } from "react-router-dom";
import { DaemonSetsProvider } from "@/store/contexts/monitoring/daemonsets.context";
import AdminLayout from "@/layouts/AdminLayout";
export default function ProtectedAdmin() {
return (
<DaemonSetsProvider>
<AdminLayout>
<Outlet />
</AdminLayout>
</DaemonSetsProvider>
);
}
3-7. 라우트 파라미터를 Control Props로 주입
// src/pages/monitoring/DaemonSetsPageWithContext.tsx
import { useParams } from "react-router-dom";
import DaemonSetsPage from "./DaemonSetsPage";
import DaemonSetsPodPage from "./DaemonSetsPodPage";
export default function DaemonSetsPageWithContext() {
const { namespace, daemonSet } = useParams(); // /:namespace?/:daemonSet?
if (daemonSet) {
// 🔸 상세 화면 (Pod 페이지)
return (
<DaemonSetsPodPage
namespace={namespace!}
daemonSet={daemonSet}
/>
);
}
// 🔸 리스트 화면 (namespace를 Control Props로 주입)
return <DaemonSetsPage namespace={namespace} />;
}
4) 조합 전략: Control Props + (State Reducer + Context API + Custom Hook)
- State Reducer: 상태 전환/업데이트 로직을 한곳(reducer)으로 모아 예측 가능하고 테스트 가능한 상태 관리
- Context API: 해당 reducer의 상태와 액션을 트리 전체에 공급
- Custom Hook:
useDaemonSets로 선택자/액션을 캡슐화 → 컴포넌트는 깔끔해짐 - Control Props: 라우트/상위 화면에서
namespace,daemonSet등 외부 제어가 필요할 때 props로 주입, 없으면 내부 상태로 동작(양모드 지원)
결과적으로, 페이지 재사용성(Control Props) + 상태 일관성(Reducer/Context) + 코드 응집도(Custom Hook) 를 동시에 달성합니다.
추가 팁 (현재 코드에 바로 반영하기 좋은 것들)
dispatch({ type: "GET_ALL_DAEMONSETS" })같은 하드코딩 문자열 → Action 타입 상수/유니온으로 전환 (위 예시처럼)- 라우트 분기(
DaemonSetsPagevsDaemonSetsPodPage)는useParams를 Control Props에 연결하는 방식이 가장 깔끔 - 삭제/다운로드 액션은 Provider에 모아두고 컴포넌트에서는
actions.remove / actions.download만 호출 - 비동기 중복 호출 방지:
refresh호출 위치(마운트 1회)와 버튼 재요청 플래그(선택)를 구분