|
| 1 | +--- |
| 2 | +title: Redux 핵심 개념과 Flux 아키텍쳐 |
| 3 | +createdAt: 2024-09-10 |
| 4 | +category: React |
| 5 | +description: Redux의 핵심 개념과 Flux 아키텍처에 대해 알아봅니다. MVC, MVVM, Container-Presenter 패턴과 비교하며 Redux가 어떻게 상태 관리를 단순화하는지 알아봅니다. |
| 6 | +comment: true |
| 7 | +--- |
| 8 | + |
| 9 | +# Redux 핵심 개념과 Flux 아키텍쳐 |
| 10 | + |
| 11 | +Redux 와 Flux 아키텍쳐에 대해 공부하던 중 |
| 12 | + |
| 13 | +> Redux 가 MVC 아키텍쳐의 한계를 없애기위해 Flux 단방향 흐름의 아키텍쳐를 사용하는데 그렇다면 Redux 도입 이전 React 는 MVC 아키텍쳐 인가? <br/> |
| 14 | +> 근데 React 는 양방향 바인딩을 하지 않는데? props 로 단방향으로 데이터를 내려주지 않음? |
| 15 | +
|
| 16 | +라는 의문이 들었고, |
| 17 | +MVC, MVVM, Flux 아키텍쳐와 관련된 내용을 찾아보면서 정리한 포스트입니다 |
| 18 | + |
| 19 | +## 🤔 Redux 는 왜 만들어졌을까 ? |
| 20 | + |
| 21 | +Redux 가 왜 만들어졌는지 알아보기 전에, |
| 22 | +React 가 왜 / 어떻게 발전하며 만들어졌는지에 대해 알고 있으면 좋습니다. |
| 23 | + |
| 24 | +### ✍️ MVC 아키텍쳐의 한계 |
| 25 | + |
| 26 | +Meta (Facebook) 에서는 기존에 PHP 를 이용해 웹 애플리케이션을 개발했었습니다. |
| 27 | + |
| 28 | +PHP 기반의 웹 프레임워크는 기본적으로 MVC (Model - View - Controller) 아키텍쳐를 따르고 있는데, MVC 아키텍쳐는 소프트웨어를 Model / View / Controller 세 가지 구성요소로 분리하여 개발하는 아키텍쳐입니다. |
| 29 | + |
| 30 | +> `모델 (Model)` : 데이터 / 비즈니스 로직을 나타내고, 데이터베이스에서 데이터를 가져오거나, 갱신하는 역할을 합니다. <br /> |
| 31 | +> `뷰 (View)` : 사용자에게 보이는 인터페이스로, HTML, CSS 이나 템플릿엔진을 활용해 화면을 구성합니다. <br /> |
| 32 | +> `컨트롤러 (Controller)` : Model 과 View 사이의 상호작용을 관리합니다. 사용자의 요청 / 입력을 받아 Model 을 업데이트하고, 그에 따른 View 를 갱신하는 작업을 합니다. |
| 33 | +
|
| 34 | + |
| 35 | + |
| 36 | +하지만, 애플리케이션의 규모가 커지면서, MVC 구조는 점점 더 복잡해져 갔습니다. |
| 37 | + |
| 38 | +하나의 View 가 여러 개의 Model 을 업데이트하고, 변경된 Model 은 다시 Controller 에 의해 View 에 반영되고 ... |
| 39 | + |
| 40 | +이 문제는 크게 |
| 41 | + |
| 42 | +> 1. 양방향 데이터 바인딩 <br/> |
| 43 | +> 2. 복잡한 의존성 |
| 44 | +
|
| 45 | +때문에 발생하는 것이었고, 이로 인해 MVC 아키텍쳐는 |
| 46 | + |
| 47 | +> 1. 확장에 용이하지 않다 <br/> |
| 48 | +> 2. 깨지기 쉽고 예측 불가능하다 |
| 49 | +
|
| 50 | +라는 단점으로 다가왔습니다. |
| 51 | + |
| 52 | + |
| 53 | + |
| 54 | +### ✍️ MVVM 아키텍쳐과 Component 패턴 |
| 55 | + |
| 56 | +이 문제는 MVVM 아키텍쳐 (Model - View - ViewModel, DOM 을 템플릿과 바인딩을 통해 선언적으로 조작하는 아키텍쳐) 를 거쳐, 작게 재사용 할 수 있는 단위로 만들어 조립하는 Component 패턴으로 발전되었습니다. |
| 57 | + |
| 58 | +> ReactJS 는 Component 패턴을 사용하는 단방향 흐름으로 설계된 Single Page Application 라이브러리 라고 할 수 있습니다. |
| 59 | +
|
| 60 | +### ✍️ Container Presenter 패턴 |
| 61 | + |
| 62 | +하지만 Component 패턴도 한계가 존재했습니다. |
| 63 | + |
| 64 | +컴포넌트에 비즈니스 로직이 들어가게 되면 컴포넌트의 재사용성이 떨어지는 경험이 한번씩 있을겁니다. |
| 65 | + |
| 66 | +이때문에, 컴포넌트는 재사용이 가능해야 한다는 원칙에 따라 가급적 비즈니스 로직을 포함시키지 않으려고 개발을 진행하게 되었습니다. |
| 67 | + |
| 68 | +이는, 최상단 / 페이지 단위로 `Container` 컴포넌트를 두고 비즈니스 로직을 관리하고, |
| 69 | +비즈니스 로직을 가지고 있지 않은 데이터만 뿌려주는 형태의 Presenter 컴포넌트로 분리하여 작성하는 |
| 70 | +`Container - Presenter 패턴`으로 발전하게 되었습니다. |
| 71 | + |
| 72 | +하지만, Container-Presenter 패턴을 이용해 만들었을때, 컴포넌트 구조가 복잡해짐에 따라, 하위 컴포넌트에 값을 전달하기 위해, `Props Drilling Problem` 이 발생하게 됩니다. |
| 73 | + |
| 74 | + |
| 75 | + |
| 76 | +### ✍️ Flux 아키텍쳐 |
| 77 | + |
| 78 | +Container-Presenter 패턴에서 발생한 Prop Drilling 을 통해 데이터를 전달하는 문제는, Model (state, 데이터) 의 파편화를 불러 일으켰습니다. |
| 79 | + |
| 80 | +그래서 단방향 데이터 흐름을 활용한 리액트용 애플리케이션 아키텍쳐인 Flux 아키텍쳐가 탄생했습니다. |
| 81 | + |
| 82 | + |
| 83 | + |
| 84 | +데이터를 변화시키려는 동작(Action) 이 발생하면 |
| 85 | +Dispatcher 는 Action 을 받아 Redux 에 Action 이 발생했음을 알리고, |
| 86 | +변화된 데이터가 Store에 저장되면 View 에서 데이터를 가져와서 보여줍니다 |
| 87 | + |
| 88 | +### ✍️ Flux 아키텍쳐를 구현한 Redux |
| 89 | + |
| 90 | +Redux 는 Flux 아키텍쳐를 구현한 것으로, 예측가능하고 중앙화된 디버깅이 쉽고 유연한 상태관리 라이브러리 라고 Redux 공식 홈페이지에 설명되어 있습니다 |
| 91 | + |
| 92 | +> A Predictable State Container for JS Apps <br/> |
| 93 | +> **Predictable & Centralized & Debuggable & Flexible** |
| 94 | +
|
| 95 | +이런 예측가능하고 중앙화된, 디버깅이 쉽고 유연함을 유지하기 위해서 Redux 는 3가지 원칙을 정했습니다 |
| 96 | + |
| 97 | +#### 1. 단일 진실의 근원 (Single Source of Truth) |
| 98 | + |
| 99 | +Redux에서 애플리케이션의 상태는 Redux Store 에 저장하게 되는데, 이 Store 는 단 하나여야 한다는 제약 조건입니다. |
| 100 | + |
| 101 | +Store 가 한개가 되면, 상태의 변경내역을 단 하나의 Store 에서 어떻게 변하는지 확인하여 알 수 있고, 상태의 변화를 직렬화 시켜 디버깅이 쉬워집니다. |
| 102 | + |
| 103 | +#### 2. 상태는 읽기 전용 (State is Read-Only) |
| 104 | + |
| 105 | +State 상태값은 읽기 전용이어야 한다는 제약조건입니다. |
| 106 | + |
| 107 | +상태는 직접 변경할 수 없고, 사전에 정의해 둔 상황(Action) 이 발생했을 경우, 정해진 대로(Reducer)로만 상태를 변경 할수 있습니다. |
| 108 | + |
| 109 | +이를 통해 상태를 변경할 때 마다 어떤 목적과 값으로 상태를 변경하는지 파악 할 수 있습니다. |
| 110 | + |
| 111 | +#### 3. 변경은 순수 함수로 작성 (Changes are made with Pure Functions) |
| 112 | + |
| 113 | +상태의 변화는 순수함수를 통해 일어나야한다는 제약조건입니다. |
| 114 | + |
| 115 | +Pure Function, 순수함수는 동일 입력값에 대해 항상 같은 출력을 반환하는 함수입니다. |
| 116 | +여기서 말하는 상태변화를 만들어내는 순수함수는 Reducer 로, Reducer 는 이전 상태에 변화를 주고 다음 상태를 리턴하는데, |
| 117 | +입력으로 받은 이전 상태를 직접 변경하지 않고, 새로운 상태 객체를 만들어 리턴한다는 것입니다. |
| 118 | + |
| 119 | +> 👉 `Immutability` (불변성) <br/> |
| 120 | +> 참고로, Redux Toolkit 에서는 ImmerJS 를 통해 불변성을 유지하며, <br/> |
| 121 | +> 내부에서 새로운 상태를 생성하고 관리해주기 때문에 가독성이 올라가고 코드 작성이 쉽습니다. |
| 122 | +
|
| 123 | +## ⚛️ Redux 의 구성요소와 데이터 흐름 |
| 124 | + |
| 125 | +### ✏️ Redux 의 구성요소 |
| 126 | + |
| 127 | +Redux 는 다음과 같은 요소로 구성되어 있습니다. |
| 128 | + |
| 129 | +> Store : Redux 의 상태를 저장하기 위한 저장소 <br/> |
| 130 | +> State : Redux Store 에 저장되어있는 데이터 <br/> |
| 131 | +> Action : Redux Store 에 저장된 State 에 변화를 주기 위한 행동으로 JS 객체로 존재 <br/> |
| 132 | +> Action Creator : Action 객체를 생성하는 역할을 하는 함수 <br/> |
| 133 | +> Reducer : Action 발생시 Action 을 처리하는 함수로 Redux State 를 변경 |
| 134 | +
|
| 135 | +### ✏️ Redux 의 데이터 흐름 |
| 136 | + |
| 137 | +Redux 의 구성요소와 함께 Flux 아키텍쳐가 어떻게 적용되어 Redux 의 상태가 변화하고, View 에 반영되는지 이전에 봤던 그림과 함께 알아보겠습니다. |
| 138 | + |
| 139 | + |
| 140 | + |
| 141 | +실제 Counter 예제를 통해 Redux의 데이터 흐름이 어떻게 동작하는지 단계별로 살펴보겠습니다. |
| 142 | + |
| 143 | +#### `1단계` : View에서 Action이 만들어지고 Dispatch 됩니다 |
| 144 | + |
| 145 | +먼저 사용자가 View (React 컴포넌트)에서 버튼을 클릭하면, Action이 생성되고 dispatch됩니다. |
| 146 | + |
| 147 | +```tsx |
| 148 | +// Counter.tsx |
| 149 | +import React from "react"; |
| 150 | +import { useSelector, useDispatch } from "react-redux"; |
| 151 | +import { increment, decrement, incrementByAmount } from "./counterActions"; |
| 152 | + |
| 153 | +interface RootState { |
| 154 | + counter: { value: number }; |
| 155 | +} |
| 156 | + |
| 157 | +function Counter() { |
| 158 | + const count = useSelector((state: RootState) => state.counter.value); |
| 159 | + const dispatch = useDispatch(); |
| 160 | + |
| 161 | + return ( |
| 162 | + <div> |
| 163 | + <h2>Count: {count}</h2> |
| 164 | + {/* 1단계: 버튼 클릭 시 Action이 생성되고 dispatch됨 */} |
| 165 | + <button onClick={() => dispatch(increment())}>+1</button> |
| 166 | + <button onClick={() => dispatch(decrement())}>-1</button> |
| 167 | + <button onClick={() => dispatch(incrementByAmount(5))}>+5</button> |
| 168 | + </div> |
| 169 | + ); |
| 170 | +} |
| 171 | + |
| 172 | +export default Counter; |
| 173 | +``` |
| 174 | + |
| 175 | +Action Creator 함수들이 Action 객체를 생성합니다: |
| 176 | + |
| 177 | +```ts |
| 178 | +// counterActions.ts - Action Types 정의 |
| 179 | +export const INCREMENT = "INCREMENT"; |
| 180 | +export const DECREMENT = "DECREMENT"; |
| 181 | +export const INCREMENT_BY_AMOUNT = "INCREMENT_BY_AMOUNT"; |
| 182 | + |
| 183 | +// Action Creator 함수들이 생성하는 Action 객체 |
| 184 | +export const increment = () => ({ type: INCREMENT }); |
| 185 | + |
| 186 | +export const decrement = () => ({ type: DECREMENT }); |
| 187 | + |
| 188 | +export const incrementByAmount = (amount: number) => ({ |
| 189 | + type: INCREMENT_BY_AMOUNT, |
| 190 | + payload: amount, |
| 191 | +}); |
| 192 | +``` |
| 193 | + |
| 194 | +#### `2단계`: Dispatch된 Action은 현재 State와 함께 Reducer로 전달됩니다 |
| 195 | + |
| 196 | +dispatch된 Action 객체는 Redux Store로 전달되어, 현재 state와 함께 Reducer 함수로 전달됩니다. |
| 197 | + |
| 198 | +이 단계에서 Redux Store는 다음과 같이 동작합니다: |
| 199 | + |
| 200 | +``` |
| 201 | +1. 사용자가 dispatch(increment()) 실행 |
| 202 | +2. Redux Store가 Action 객체 { type: 'INCREMENT' }를 받음 |
| 203 | +3. Store가 현재 state { value: 0 }과 Action을 counterReducer에 전달 |
| 204 | +4. counterReducer(state, action) 함수 호출 |
| 205 | +
|
| 206 | +// Redux Store 내부에서 일어나는 과정 |
| 207 | +counterReducer( |
| 208 | + { value: 0 }, // 현재 state |
| 209 | + { type: 'INCREMENT' } // dispatch된 Action |
| 210 | +); |
| 211 | +``` |
| 212 | + |
| 213 | +#### `3단계`: Reducer에서는 변경된 State가 리턴됩니다 |
| 214 | + |
| 215 | +Reducer는 현재 state를 직접 수정하지 않고, 새로운 state 객체를 생성하여 반환합니다. |
| 216 | + |
| 217 | +```ts |
| 218 | +// counterReducer.ts - 완전한 Reducer 구현 |
| 219 | +import { INCREMENT, DECREMENT, INCREMENT_BY_AMOUNT } from "./counterActions"; |
| 220 | + |
| 221 | +interface CounterState { |
| 222 | + value: number; |
| 223 | +} |
| 224 | + |
| 225 | +const initialState: CounterState = { |
| 226 | + value: 0, |
| 227 | +}; |
| 228 | + |
| 229 | +const counterReducer = (state = initialState, action: any): CounterState => { |
| 230 | + switch (action.type) { |
| 231 | + // 3단계: 이전 state를 수정하지 않고 새로운 state 객체를 반환 |
| 232 | + case INCREMENT: |
| 233 | + return { ...state, value: state.value + 1 }; |
| 234 | + |
| 235 | + case DECREMENT: |
| 236 | + return { ...state, value: state.value - 1 }; |
| 237 | + |
| 238 | + case INCREMENT_BY_AMOUNT: |
| 239 | + return { ...state, value: state.value + action.payload }; |
| 240 | + |
| 241 | + default: |
| 242 | + return state; |
| 243 | + } |
| 244 | +}; |
| 245 | + |
| 246 | +export default counterReducer; |
| 247 | +``` |
| 248 | + |
| 249 | +#### `4단계`: 변경된 State는 View에 나타납니다 |
| 250 | + |
| 251 | +새로운 state가 Store에 저장되면, 해당 state를 구독하고 있던 React 컴포넌트들이 자동으로 리렌더링되어 변경된 상태를 화면에 표시합니다. |
| 252 | + |
| 253 | +```tsx |
| 254 | +// Counter.tsx |
| 255 | +import React from "react"; |
| 256 | +import { useSelector, useDispatch } from "react-redux"; |
| 257 | + |
| 258 | +function Counter() { |
| 259 | + // 4단계: useSelector Hook이 Store의 state 변경을 감지하고 컴포넌트 리렌더링 |
| 260 | + const count = useSelector((state: RootState) => state.counter.value); |
| 261 | + const dispatch = useDispatch(); |
| 262 | + |
| 263 | + // State 변경 감지 과정: |
| 264 | + // 1. 버튼 클릭 → dispatch(increment()) → Action 객체 { type: 'INCREMENT' } 생성 |
| 265 | + // 2. Redux Store가 현재 state { value: 0 }과 Action을 counterReducer에 전달 |
| 266 | + // 3. counterReducer가 새로운 state { value: 1 } 반환 |
| 267 | + // 4. Store의 state가 업데이트됨 |
| 268 | + // 5. useSelector가 state 변경을 감지하고 Counter 컴포넌트 리렌더링 트리거 |
| 269 | + // 6. 화면에 "Count: 1"이 표시됨 |
| 270 | + |
| 271 | + return ( |
| 272 | + <div> |
| 273 | + <h2>Count: {count}</h2> {/* 변경된 값이 화면에 표시 */} |
| 274 | + <button onClick={() => dispatch(increment())}>+1</button> |
| 275 | + <button onClick={() => dispatch(decrement())}>-1</button> |
| 276 | + <button onClick={() => dispatch(incrementByAmount(5))}>+5</button> |
| 277 | + </div> |
| 278 | + ); |
| 279 | +} |
| 280 | + |
| 281 | +export default Counter; |
| 282 | +``` |
| 283 | + |
| 284 | +#### Redux Store 설정 |
| 285 | + |
| 286 | +마지막으로 Redux Store를 설정하고 React 앱에 연결하는 코드입니다: |
| 287 | + |
| 288 | +```ts |
| 289 | +// store.ts |
| 290 | +import { createStore, combineReducers } from "redux"; |
| 291 | +import counterReducer from "./counterReducer"; |
| 292 | + |
| 293 | +// 여러 reducer를 결합 (현재는 counter만 있지만 확장 가능) |
| 294 | +const rootReducer = combineReducers({ |
| 295 | + counter: counterReducer, |
| 296 | +}); |
| 297 | + |
| 298 | +// Redux Store 생성 (순수 Redux 방식) |
| 299 | +export const store = createStore(rootReducer); |
| 300 | + |
| 301 | +// TypeScript 타입 정의 |
| 302 | +export type RootState = ReturnType<typeof rootReducer>; |
| 303 | +export type AppDispatch = typeof store.dispatch; |
| 304 | +``` |
| 305 | + |
| 306 | +```tsx |
| 307 | +// index.tsx 또는 App.tsx |
| 308 | +import React from "react"; |
| 309 | +import ReactDOM from "react-dom"; |
| 310 | +import { Provider } from "react-redux"; |
| 311 | +import { store } from "./store"; |
| 312 | +import Counter from "./Counter"; |
| 313 | + |
| 314 | +// React 앱을 Redux Store와 연결 |
| 315 | +ReactDOM.render( |
| 316 | + <Provider store={store}> |
| 317 | + <Counter /> |
| 318 | + </Provider>, |
| 319 | + document.getElementById("root"), |
| 320 | +); |
| 321 | +``` |
| 322 | + |
| 323 | +### 📊 Redux 데이터 흐름 요약 |
| 324 | + |
| 325 | +1. **Action 생성 및 Dispatch**: 사용자가 버튼을 클릭하면 `dispatch(increment())`가 실행되어 `{ type: 'counter/increment' }` Action 객체가 Store로 전달됩니다. |
| 326 | + |
| 327 | +2. **Reducer 실행**: Store가 현재 state `{ value: 0 }`과 Action `{ type: 'counter/increment' }`을 counter reducer에 전달합니다. |
| 328 | + |
| 329 | +3. **새로운 State 생성**: Reducer가 불변성을 지키며 새로운 state `{ value: 1 }`을 반환합니다. |
| 330 | + |
| 331 | +4. **UI 업데이트**: `useSelector`가 state 변경을 감지하고 Counter 컴포넌트가 리렌더링되어 화면에 "Count: 1"이 표시됩니다. |
| 332 | + |
| 333 | +이처럼 Redux는 예측 가능한 단방향 데이터 흐름을 통해 애플리케이션의 상태를 체계적으로 관리합니다. |
0 commit comments