1. 핵심 개념
실무에서는 원본 데이터는 한 군데에 통으로 저장하고,
필터링·정렬·검색 같은 파생 상태는 컴포넌트 혹은 selector에서 처리하는 방식이 표준이에요.
원칙 ✅
- 서버/클라이언트 구분 없이 원본 데이터는 단일 소스(Single Source of Truth)
- 필터·정렬 등은 항상 UI에서 파생
- JSX는 항상 파생 상태를 참조
2. 서버 상태 관리 패턴 (React Query / SWR / Apollo)
① 원본 데이터 캐싱
const { data: todos = [], isLoading } = useQuery({
queryKey: ['todos'],
queryFn: fetchTodos, // 서버에서 전체 todos 한 번에 가져오기
});
- React Query 캐시 →
['todos']단일 키로 통 관리 - API는 대부분 전체 데이터를 한 번에 내려주고, 프론트에서 가공
② 파생 상태는 컴포넌트에서 처리
const filteredTodos = useMemo(() => {
if (filterType === 'active') return todos.filter(t => !t.done);
if (filterType === 'completed') return todos.filter(t => t.done);
return todos;
}, [todos, filterType]);
- 원본
todos는 건드리지 않음 - 필터링·정렬은 컴포넌트에서만 처리 → JSX에 직접 연결
③ JSX에 바인딩
return (
<ul>
{filteredTodos.map(todo => (
<TodoItem key={todo.id} todo={todo} />
))}
</ul>
);
filteredTodos만 JSX에 연결filterType이 바뀌면 자동으로 다시 계산 → UI 즉시 갱신
3. 클라이언트 상태 관리 패턴 (Redux / Zustand / MobX)
① Redux 예시
// store.ts
const todosSlice = createSlice({
name: 'todos',
initialState: [] as Todo[],
reducers: {
setTodos: (_, action: PayloadAction<Todo[]>) => action.payload,
toggleTodo: (state, action: PayloadAction<string>) => {
const todo = state.find(t => t.id === action.payload);
if (todo) todo.done = !todo.done;
},
},
});
// component.tsx
const todos = useSelector((state: RootState) => state.todos);
const filteredTodos = useMemo(() => {
switch (filterType) {
case 'active': return todos.filter(t => !t.done);
case 'completed': return todos.filter(t => t.done);
default: return todos;
}
}, [todos, filterType]);
- 스토어에는 todos 통 데이터만 저장
- 필터링은 컴포넌트 내부 혹은
selector에서 처리
② MobX 예시
class TodoStore {
@observable todos: Todo[] = [];
@action setTodos(newTodos: Todo[]) {
this.todos = newTodos;
}
@computed get activeTodos() {
return this.todos.filter(t => !t.done);
}
@computed get completedTodos() {
return this.todos.filter(t => t.done);
}
}
todos는 원본 데이터activeTodos,completedTodos는 computed를 활용해 자동 파생
③ Zustand 예시
const useTodoStore = create<TodoStore>((set) => ({
todos: [],
setTodos: (todos) => set({ todos }),
toggleTodo: (id) =>
set((state) => ({
todos: state.todos.map(t =>
t.id === id ? { ...t, done: !t.done } : t
),
})),
}));
const todos = useTodoStore((s) => s.todos);
const filteredTodos = useMemo(() => {
if (filterType === 'active') return todos.filter(t => !t.done);
if (filterType === 'completed') return todos.filter(t => t.done);
return todos;
}, [todos, filterType]);
- 상태 저장소에는 원본 todos만 보관
- 필터링은 컴포넌트에서 직접 처리 → React Query와 동일한 패턴
4. 실무 패턴의 장점
| 항목 | 단일 캐시 + 파생 상태 (실무 표준) | 쿼리별 별도 캐싱 (강의식) |
|---|---|---|
| 데이터 일관성 | ✅ 원본 한 곳 → 어디서든 동일 데이터 | ❌ 각 캐시별 업데이트 필요 |
| 코드 복잡도 | ✅ setQueryData 한 번 | ❌ invalidateQueries 여러 번 |
| 성능 | ✅ 한 번만 캐싱 | ❌ 동일 데이터 중복 캐싱 |
| 유지보수성 | ✅ 간단하고 직관적 | ❌ 탭별 관리 필요 |
| 실무 활용도 | 표준 패턴 | 학습 단계에서만 주로 사용 |
5. 요약
실무 상태 관리 원칙 ✅
- 원본 데이터는 단일 소스에서 통으로 관리
- 필터링·정렬·검색 등은 컴포넌트에서 파생
- JSX는 항상 파생 상태만 참조
- 서버 상태(React Query)든 클라이언트 상태(Redux, Zustand, MobX)든 패턴은 동일