Context API 보일러플레이트를 추상화한 팩토리 함수입니다. 상태와 액션을 분리하여 액션만 사용하는 컴포넌트에서 불필요한 리렌더링을 방지합니다.
구현
'use client';
import { createContext, type Dispatch, type JSX, type PropsWithChildren, useContext, useMemo, useState } from 'react';
type AnyFunction = (...args: never[]) => unknown;
type Set<T extends Record<string, unknown>> = Partial<T> | ((prev: T) => Partial<T>);
type CreateSharedState<TState extends Record<string, unknown>, TActions extends Record<string, AnyFunction>> = {
Provider: ({ children, initState }: PropsWithChildren<{ initState?: Partial<TState> }>) => JSX.Element;
useSharedState: () => TState;
useSharedActions: () => TActions;
};
export function createSharedState<TState extends Record<string, unknown>, TActions extends Record<string, AnyFunction>>(
initState: TState,
createActions: (set: Dispatch<Set<TState>>) => TActions,
): CreateSharedState<TState, TActions> {
const StateContext = createContext<TState | undefined>(undefined);
const ActionsContext = createContext<TActions | undefined>(undefined);
const Provider: CreateSharedState<TState, TActions>['Provider'] = ({ children, initState: providedInitState }) => {
const [state, setState] = useState({ ...initState, ...providedInitState });
const set: Dispatch<Set<TState>> = action =>
setState(prev => {
const newState = typeof action === 'function' ? action(prev) : action;
return { ...prev, ...newState };
});
// actions는 useMemo로 안정적인 참조 유지
// → actions만 사용하는 컴포넌트는 state 변경 시 리렌더링 안됨
const actions = useMemo(() => createActions(set), []);
return (
<StateContext.Provider value={state}>
<ActionsContext.Provider value={actions}>{children}</ActionsContext.Provider>
</StateContext.Provider>
);
};
const useSharedState: CreateSharedState<TState, TActions>['useSharedState'] = () => {
const context = useContext(StateContext);
if (!context) {
throw new Error('Provider 안에서 사용되어야 합니다.');
}
return context;
};
const useSharedActions: CreateSharedState<TState, TActions>['useSharedActions'] = () => {
const context = useContext(ActionsContext);
if (!context) {
throw new Error('Provider 안에서 사용되어야 합니다.');
}
return context;
};
return { Provider, useSharedState, useSharedActions };
}
사용법
1. 스토어 생성
type CountState = { count: number };
type CountActions = { increment: () => void; decrement: () => void };
export const {
Provider: CountProvider,
useSharedState: useCountState,
useSharedActions: useCountActions,
} = createSharedState<CountState, CountActions>({ count: 0 }, set => ({
increment: () => set(prev => ({ count: prev.count + 1 })),
decrement: () => set(prev => ({ count: prev.count - 1 })),
}));
2. Provider로 감싸고 훅 사용
// 상태만 구독 → 액션 변경 시 리렌더링 없음
function Counter() {
const { count } = useCountState();
return <span>{count}</span>;
}
// 액션만 사용 → 상태 변경 시 리렌더링 없음
function CounterButtons() {
const { increment, decrement } = useCountActions();
return (
<>
<button onClick={increment}>+</button>
<button onClick={decrement}>-</button>
</>
);
}
function App() {
return (
<CountProvider>
<Counter />
<CounterButtons />
</CountProvider>
);
}
3. Provider에서 초기 상태 주입
<CountProvider initState={{ count: 10 }}>
<Counter />
</CountProvider>

