플렉스(Flex)로 디자인하고 플럭스(Flux)로 설계하라.
기존 애플리케이션에서는 MVC 패턴을 사용하고 있었다. MVC는 Model, View, Controller의 약자이다. Model에 데이터를 저장하고, Controller를 이용하여 Model의 데이터를 관리(CRUD)한다. Model의 데이터가 변경되면 View로 전달되어 사용자에게 보여진다. 여기서 중요한 점은 사용자가 View를 통해 데이터를 입력하면 Model을 업데이트를 할 수 있었다는 데 있다. 즉, 데이터가 양방향으로 흐르는 경우가 있었다.
문제는 여기에서 시작되는데, 애플리케이션의 규모가 커질수록 상호작용이 일어나는 경우가 늘어났다. 그로 인해, 데이터의 흐름이 복잡해지기 시작했다. 이로 인해, 애플리케이션의 동작을 예측하기가 어려워졌다. 다시 말해, 추적하기가 어려워졌다. 이에 대한 해결 방안으로 단방향 데이터 흐름을 갖는 Flux 패턴을 고안했다.
Flux 패턴
Facebook에서 개발한 클라이언트 사이드 웹 애플리케이션을 위한 아키텍처 패턴이다. 주요 목적은 애플리케이션의 데이터 흐름을 단방향으로 만들어 예측 가능하고 관리하기 쉬운 상태를 유지하는 것이다.
Flux 패턴의 주요 구성 요소
- Action: 사용자 상호작용이나 서버 응답 등 애플리케이션에서 발생하는 이벤트를 나타낸다.
- Dispatcher: 액션을 받아 등록된 모든 스토어에 전달하는 중앙 허브 역할을 한다.
- Store: 애플리케이션의 상태와 로직을 포함합니다. 디스패처로부터 액션을 받아 상태를 업데이트한다.
- View: 스토어의 상태를 받아 화면에 표시합니다. React 컴포넌트가 이 역할을 수행할 수 있다.
각 요소들은 단방향 흐름에 따라 순서대로 역할을 수행하고, View로부터 새로운 데이터 변경이 생기면 처음부터 다시 이 순서대로 실행한다. 이렇게 함으로써 예외 없이 데이터를 처리할 수 있게 됐다.
데이터 흐름
- 사용자가 뷰와 상호작용한다.
- 뷰는 액션을 생성한다.
- 액션은 디스패처로 전달된다.
- 디스패처는 액션을 관련 스토어에 전달한다.
- 스토어는 상태를 업데이트하고 변경 이벤트를 발생시킨다.
- 뷰는 변경 이벤트를 감지하고 새로운 상태로 다시 렌더링된다.
Flux의 장점
- 단방향 데이터 흐름으로 애플리케이션의 상태 변화를 예측하기 쉽다.
- 복잡한 데이터 흐름을 단순화하여 디버깅과 테스트가 용이하다.
- 상태 관리의 중앙화로 애플리케이션의 구조가 명확해진다.
단점
- 작은 규모의 애플리케이션에서는 과도한 보일러플레이트 코드가 필요할 수 있다.
- 학습 곡선이 있을 수 있다.
Flux의 개념은 Redux, MobX 등의 상태 관리 라이브러리에 영향을 미쳤다.
연관성
useState()
직접적인 연관성은 없지만 유사점은 있다.
1. 상태관리
- useState()는 컴포넌트 내에서 상태를 관리한다.
2. 단방향 데이터 흐름
- 상태 업데이트는 setState 함수를 통해 이루어지며, 이는 단방향 데이터 흐름을 형성한다.
3. 예측 가능한 상태 변화
- useState()를 사용하면 상태 변화가 예측 가능하다.
useState()는 주로 단일 컴포넌트 내의 로컬 상태 관리에 사용되며 간단한 상태 관리에 적합하다. 그러나 Flux 패턴은 애플리케이션 전체의 상태 관리를 위한 아키텍처인 만큼, 더 복잡한 상태 관리와 데이터 흐름을 다루는 데 적합하다. Action, Dispatcher, Store, View 등의 구체적인 구조를 갖고 있다.
useReducer()
useState()는 useReducer()로 구현할 수 있다. useState()는 useReducer()를 기반으로 만들어졌다. 직접 구현하면 다음과 같다.
function useStateWithReducer(initialState) {
const [state, dispatch] = useReducer((state, action) => {
return typeof action === 'function' ? action(state) : action;
}, initialState);
const setState = (newState) => {
dispatch(newState);
};
return [state, setState];
}
1.useReducer()를 사용하여 상태와 디스패치 함수를 생성한다.
2. reducer 함수
- action이 함수라면, 이전 상태를 인자로 받아 새 상태를 반환
- 그렇지 않으면 action을 그대로 새 상태로 사용
3. setState 함수는 단순히 dispatch를 호출한다.
4. useState()와 동일한 인터페이스 [state, setState]를 반환
이 useStateWithReducer는 useState()와 거의 동일하게 동작한다.
또한, 다음과 같이 useReducer()를 사용하여 useState()의 기능을 복제할 수 있다. 본질적으로 useState()가 useReducer()의 특수한 경우라고 말할 수 있는 대목이다.
function Counter() {
const [count, setCount] = useStateWithReducer(0);
const increment = () => setCount(prev => prev + 1);
const decrement = () => setCount(prev => prev - 1);
return (
<div>
<p>Count: {count}</p>
<button onClick={increment}>+</button>
<button onClick={decrement}>-</button>
</div>
);
}
useState()와 useReducer(), 두 Hook 모두 컴포넌트 내에서 상태를 관리하는 데 사용되며, 이전 상태를 기반으로 새로운 상태를 계산하는 '함수형 업데이트'를 지원한다. 또한 상태가 변경되면 컴포넌트의 리렌더링이 트리거 된다.
// useState()
function Counter() {
const [count, setCount] = useState(0);
const increment = () => setCount(prevCount => prevCount + 1);
const decrement = () => setCount(prevCount => prevCount - 1);
return (
<div>
<p>Count: {count}</p>
<button onClick={increment}>+</button>
<button onClick={decrement}>-</button>
</div>
);
}
// useReducer()
function reducer(state, action) {
switch (action.type) {
case 'INCREMENT':
return { count: state.count + 1 };
case 'DECREMENT':
return { count: state.count - 1 };
default:
return state;
}
}
function Counter() {
const [state, dispatch] = useReducer(reducer, { count: 0 });
return (
<div>
<p>Count: {state.count}</p>
<button onClick={() => dispatch({ type: 'INCREMENT' })}>+</button>
<button onClick={() => dispatch({ type: 'DECREMENT' })}>-</button>
</div>
);
}
그러나 차이점은 존재한다.
1. 복잡도
- useState() : 단순한 상태 관리에 적합하다.
- useReducer() : 더 복잡한 상태 로직을 다루는 데 적합하다.
2. 상태 업데이트 방식
- useState() : 직접 새 상태 값을 설정한다.
- useReducer() : 액션 객체를 디스패치하여 상태를 업데이트한다.
3. 상태 구조
- useState() : 주로 단일 값이나 간단한 객체에 사용된다.
- useReducer() : 복잡한 객체나 배열 같은 더 큰 상태 구조를 다루는 데 유용하다.
4. 로직의 위치
- useState() : 상태 업데이트 로직이 주로 컴포넌트 내부에 있다.
- useReducer() : 상태 업데이트 로직이 reducer 함수에 집중되어 있다.
5. 테스트 용이성
- useReducer()는 순수 함수인 reducer를 사용하기 때문에 useState()보다 테스트 하기 쉽다.
useContext()
유사점
1. 전역 상태 관리
- Flux : Store를 통해 애플리케이션의 전역 상태를 관리한다.
- useContext() : 컴포넌트 트리 전체에 걸쳐 데이터를 공유할 수 있게 해준다.
2. 데이터 흐름
- Flux : 단방향 데이터 흐름을 강조(Action -> Dispatcher -> Store -> View)
- useContext() : 의존성 주입을 통해 단방향으로 데이터를 전달한다.
3. 상태 접근
- Flux : Store에서 관리되는 상태에 컴포넌트들이 접근한다.
- useContext() : Context를 통해 여러 컴포넌트에서 공유된 상태에 접근할 수 있다.
import React, { createContext, useContext, useReducer } from 'react';
// Context 생성 (Store 역할)
const StateContext = createContext();
const DispatchContext = createContext();
// Reducer 함수 (Store 내부 로직)
function reducer(state, action) {
switch (action.type) {
case 'INCREMENT':
return { count: state.count + 1 };
case 'DECREMENT':
return { count: state.count - 1 };
default:
return state;
}
}
// Provider 컴포넌트 (Dispatcher와 Store 역할 결합)
function CountProvider({ children }) {
const [state, dispatch] = useReducer(reducer, { count: 0 });
return (
<StateContext.Provider value={state}>
<DispatchContext.Provider value={dispatch}>
{children}
</DispatchContext.Provider>
</StateContext.Provider>
);
}
// 커스텀 Hook (Store에 접근하는 방식)
function useCountState() {
return useContext(StateContext);
}
function useCountDispatch() {
return useContext(DispatchContext);
}
// 컴포넌트 (View 역할)
function Counter() {
const state = useCountState();
const dispatch = useCountDispatch();
return (
<div>
Count: {state.count}
<button onClick={() => dispatch({ type: 'INCREMENT' })}>+</button>
<button onClick={() => dispatch({ type: 'DECREMENT' })}>-</button>
</div>
);
}
// 앱 컴포넌트
function App() {
return (
<CountProvider>
<Counter />
</CountProvider>
);
}
Context API
Flux 패턴의 일부 개념을 반영하고 있지만, 복잡한 상태 관리나 최적화된 성능이 필요한 대규모 애플리케이션에서는 상태 관리 라이브러리가 선호된다.
- 전역 상태 관리:
- Flux: Store를 사용하여 애플리케이션의 전역 상태를 관리합니다.
- Context API: Provider를 통해 컴포넌트 트리 전체에 걸쳐 데이터를 공유할 수 있습니다.
- 단방향 데이터 흐름:
- Flux: Action → Dispatcher → Store → View의 흐름을 따릅니다.
- Context API: Provider에서 Consumer로의 단방향 데이터 흐름을 제공합니다.
- 중앙화된 상태:
- Flux: Store에 상태를 중앙화합니다.
- Context API: Provider에 상태를 중앙화할 수 있습니다.
- 상태 업데이트 메커니즘:
- Flux: Action을 통해 상태를 업데이트합니다.
- Context API: Provider의 상태를 업데이트하는 함수를 Context를 통해 전달할 수 있습니다.
- 컴포넌트 분리:
- Flux: View 컴포넌트와 상태 관리 로직을 분리합니다.
- Context API: Provider와 Consumer를 사용하여 데이터 제공 로직과 사용 로직을 분리할 수 있습니다.
// Flux 패턴(Redux)
// Action
const increment = () => ({ type: 'INCREMENT' });
// Reducer
function counterReducer(state = 0, action) {
switch (action.type) {
case 'INCREMENT':
return state + 1;
default:
return state;
}
}
// Store
const store = createStore(counterReducer);
// Component
function Counter() {
const count = useSelector(state => state);
const dispatch = useDispatch();
return (
<div>
Count: {count}
<button onClick={() => dispatch(increment())}>Increment</button>
</div>
);
}
// Context API
const CountContext = React.createContext();
function CountProvider({ children }) {
const [count, setCount] = useState(0);
const increment = () => setCount(prev => prev + 1);
return (
<CountContext.Provider value={{ count, increment }}>
{children}
</CountContext.Provider>
);
}
function Counter() {
const { count, increment } = useContext(CountContext);
return (
<div>
Count: {count}
<button onClick={increment}>Increment</button>
</div>
);
}
useContext() 와 Context API 차이
- Context API:
- React의 기능으로, 컴포넌트 트리 전체에 데이터를 제공하는 방법입니다.
- React.createContext()를 사용하여 Context를 생성합니다.
- Provider와 Consumer 컴포넌트를 포함합니다.
- useContext:
- React Hook의 하나로, 함수형 컴포넌트에서 Context를 쉽게 사용할 수 있게 해줍니다.
- Context의 현재 값을 읽는 데 사용됩니다.
요약하면, Context API는 React의 컨텍스트 시스템 전체를 가리키는 반면, useContext는 이 시스템 함수형 컴포넌트에서 쉽게 사용할 수 있게 해주는 Hook이다. useContext는 Context API의 일부이며, Context의 값을 소비하는 더 간단한 방법을 제공한다.
주요 차이점
- 사용 방식
- Context API: Provider와 Consumer 컴포넌트를 직접 사용합니다.
- useContext: Hook을 사용하여 Context 값을 직접 가져옵니다.
- 컴포넌트 타입
- Context API: 클래스 컴포넌트와 함수형 컴포넌트 모두에서 사용 가능합니다.
- useContext: 오직 함수형 컴포넌트에서만 사용 가능합니다.
- 문법의 간결성
- Context API: Consumer를 사용할 때 render props 패턴을 사용합니다.
- useContext: 더 간결한 문법으로 Context 값을 사용할 수 있습니다.
// Context API
const MyContext = React.createContext();
function ParentComponent() {
return (
<MyContext.Provider value="Hello from context">
<ChildComponent />
</MyContext.Provider>
);
}
function ChildComponent() {
return (
<MyContext.Consumer>
{value => <div>{value}</div>}
</MyContext.Consumer>
);
}
// useContext()
const MyContext = React.createContext();
function ParentComponent() {
return (
<MyContext.Provider value="Hello from context">
<ChildComponent />
</MyContext.Provider>
);
}
function ChildComponent() {
const value = useContext(MyContext);
return <div>{value}</div>;
}
Flux 패턴과 상태 관리 라이브러리
Redux는 Flux 패턴을 기반으로 만들어졌으며, Flux의 핵심 아이디어를 더욱 단순화하고 예측 가능하게 만들었다.
1. 단일 스토어
- Redux는 Flux와 달리 단일 스토어를 사용한다. 이는 상태 관리를 더 단순하고 예측 가능하게 만든다.
2. 단방향 데이터 흐름
3. 액션
- Flux와 마찬가지로 액션을 통해 상태 변경을 시작한다.
4. 리듀서
- Flux의 스토어 개념을 더욱 순수하고 예측 가능한 형태로 발전시킨 것이 Redux의 리듀서
5. 불변성
- 상태의 불변성을 강조하여 예측 가능성과 디버깅을 용이하게 한다.
6. 미들웨어
- Flux의 개념을 확장하여 비동기 액션 처리 등을 위한 미들웨어 시스템을 제공한다.
이외에도 Flux 패턴을 기반으로 한 다른 라이브러리들이 있다.
1. MobX
- Flux에서 영감을 받았지만, 보다 반응형 프로그래밍 방식을 채택했다.
2. Vuex
- Vue.js 생태계에서 Flux와 Redux의 개념을 차용한 상태 관리 라이브러리
3. Recoil --> Jotai
- Facebook에서 Flux의 개념을 더 React 스럽게 해석한 상태 관리 라이브러리
4. Zustand
- Redux의 복잡성을 줄이면서 Flux의 핵심 개념을 유지하는 간단한 상태 관리 라이브러리
댓글