Flutter Redux 코드 분석

1. 데이터 흐름 및 구조

Redux

  • UI → Action → Reducer → 새로운 State → UI

  • 모든 변경은 Action(명령 객체)을 통해 발생

  • Reducer 함수가 불변 상태(State)를 만듦

  • Store에서 모든 상태를 관리 (전역 Singleton)

BLoC

  • UI → Event → Bloc → Stream<State> → UI

  • Event를 sink에 넣으면 Bloc이 처리, State를 stream으로 방출

  • 로직(Bloc)과 상태 표시(UI) 분리

  • 여러 BLoC 인스턴스를 만들어 모듈화 용이


3. 폴더 구조 예시 및 파일 역할

Redux 폴더 구조 예시

  • actions: 액션 객체와 타입 정의 (상태 변화 트리거)

  • reducers: 상태 변경 함수

  • models: 상태 구조 정의 (State 클래스)

  • middleware: 사이드이펙트, 비동기 로직

  • store: 단일 Store 객체 생성 및 초기화


1. models/app_state.dart


2. actions/counter_actions.dart

3. actions/user_actions.dart


4. reducers/counter_reducer.dart


5. reducers/user_reducer.dart


6. reducers/app_reducer.dart


7. store/store.dart


구조 요약

  • models/app_state.dart : 전체 상태 설계

  • actions/: 상태를 바꿀 때 사용할 명령 정의

  • reducers/: 상태 변경 함수(리듀서) 정의

    • 도메인별 reducer, 이를 합치는 appReducer

  • store/: Store 객체 생성, 앱의 중앙 상태 저장소


사용 예시 (dispatch)

1. UI 코드 예시 (Counter와 Username 변경)

아래는 Flutter에서 Redux 구조를 사용할 때의 UI 코드 예시입니다. StoreProvider, StoreConnector를 활용해 상태 변경과 구독, 그리고 dispatch를 보여줍니다.


2. store와 dispatch 구조 쉽게 설명

Store란?

  • 우리 앱의 상태가 저장되어 있는 "큰 박스" 역할.

  • 박스 안에는 counter, username 등 앱에서 중요한 값들이 전부 들어있음.

dispatch란?

  • "뭔가 바꿔주세요!"라고 store에 **명령(Action)**을 보내는 것.

  • 예를 들어, + 버튼을 누르면 IncrementCounter()라는 액션을 만들고, store.dispatch(IncrementCounter()) 이렇게 박스에게 전달함.

어떻게 작동할까?

  1. 버튼을 누름 → dispatch로 액션 보냄

  2. Store가 "이 액션이 오면 어떻게 상태를 바꿔야 하지?" 하고 Reducer에 전달

  3. Reducer가 새 상태(AppState)를 만듦

  4. Store가 이 새 상태를 보관, UI는 자동으로 최신 상태를 다시 그림

쉽게 예를 들어


이런 구조라서 얻게 되는 장점

  • 상태 추적과 로그가 쉬움 (지금 상태가 왜 이렇게 됐는지 추적 가능)

  • 테스트가 쉽고, 변경이 예측 가능함

  • 모든 상태 변화가 액션을 통해 한 곳으로 모여 관리! (버그 찾기가 수월)


이렇게 UI는 오로지 dispatch만 담당하고, 상태 변화의 로직은 store+reducer가 책임집니다. 즉, "내가 직접 상태를 바꿔!"가 아니라 "이런 변화 요청(액션)이 있어 → store가 알아서 변경 후 결과 알려줘!" => 명확한 역할 분리!

3. ViewModelfromStore의 역할

ViewModel이란?

  • **UI(Widget)**와 store 상태를 연결해주는 중간 "데이터 전달자" 역할.

  • Flutter에서는 보통 Store의 상태와 액션(콜백)을 UI 위젯에서 바로 직접 다루는 대신, ViewModel로 “일회용 데이터 바구니”를 만들어 깔끔하게 연결.

  • UI 위에는 “오로지 UI에 필요한 값과 함수”(counter, username, increment 등)만 담김.

fromStore factory란?

  • Store에서 옵저버블 상태(state)와 액션(변경 함수)을 뽑아 ViewModel로 변환하는 생성자(factory) 메서드.

  • 즉, Store의 값을 counter, username처럼 UI에서 바로 사용할 수 있게, 그리고 액션(버튼 누르면 dispatch되는 함수)도 같이 ViewModel에 포함.

  • 덕분에 Widget은 store의 실제 타입, dispatch 방법 등은 몰라도 되고, 오로지 ViewModel만 보고 UI에 집중할 수 있음.

ViewModel은 꼭 필요한가?

  • 반드시 필요한 건 아님!

  • 하지만, ViewModel을 쓰면 이런 장점이 생김:

    • UI와 상태 관리가 명확히 분리 (코드 읽기, 테스트, 확장에 매우 유리)

    • UI 재사용/Mocking/Test가 쉬워짐

    • UI 코드가 깔끔해짐 (복잡한 store/state/액션 처리 logic은 ViewModel 쪽에서 다 처리)

  • 아주 간단한 앱(상테, 액션이 1~2개)은 생략 가능. 하지만 실전/규모 있는 프로젝트에서는 거의 항상 ViewModel 패턴을 사용함.


2. StoreConnector의 역할

  • Redux 패턴에서, UI 위젯이 store와 연결되는 곳.

  • StoreConnector의 구조는 이렇게 해석할 수 있음:

    1. store가 바뀔 때마다, StoreConnector가 다시 빌드됨

    2. StoreConnector가 내부적으로 store.state를 구독(감시)함 → 상태 변하면 UI에 자동 반영

    3. builder:에 넘겨주는 값은 항상 converter에서 가공한 값(ViewModel)


3. converter란? 왜 필요한가?

  • converter는 (Store<AppState> store) => T 타입의 함수.

  • 즉, Store(AppState 전체 객체)를 받아서, 우리가 builder에서 실제로 쓸 값(T = ViewModel 등)으로 변환해줌.

  • converter의 목적:

    • Store 구조가 복잡해도, UI가 오로지 필요한 정보(예: counter 값, username, onPressed 콜백 등)만 가질 수 있게 중간 가공.

    • 성능 최적화 (안쓰는 값까지 rebuild하지 않게 원하는 정보만 넘김)

    • UI는 Store 구조를 몰라도 됨 (OCP/캡슐화, 테스팅 장점)


예시로 구조 다시 해석

  • converter: store → ViewModel로 변환

  • builder: 변환된 ViewModel(vm)을 받아 실제 위젯 빌드


요약

요소
역할/필요성

ViewModel

UI가 쓸 데이터·액션만 뽑아주는 전달자(분리, 깔끔함, 테스트 강화)

fromStore

Store → ViewModel 변환, store 구조 노출 없이 UI가 쓸 값 전달

StoreConnector

store 구독(연결) 위젯, 상태 변화 시 UI 자동 새로고침

converter

store 전체 ➔ UI 필요한 정보만 추출하는 함수 (ViewModel 생성 담당)

비동기 처리는 어떻게 해야할까?

1. Repository 역할 및 폴더 구조

  • Repository는 외부 데이터 소스(API, DB, 로컬 등)와 앱 비즈니스 로직을 분리

  • Store/State는 내부 데이터만 다룸, Repository는 데이터 “가져오기/저장하기”만 책임

폴더 구조 예시


2. Repository 및 API(Mock) 예시

repository/user_repository.dart


3. 액션: 비동기 요청을 위한 Action(Thunk)

actions/user_actions.dart


4. 리듀서 예시

reducers/user_reducer.dart


5. models/app_state.dart 업데이트


6. reducers/app_reducer.dart (전체 상태 합치기)


7. store/store.dart (Thunk 미들웨어 추가)


8. UI에서 호출하는 예시

screens/main_screen.dart


요약 흐름

  1. 버튼 클릭 → fetchUsername 콜백 → thunk fetchUsernameThunk가 실행

  2. thunk에서 UserRepository().fetchUsername() 호출

  3. 성공/실패에 따라 Success/Failure 액션 dispatch

  4. 리듀서에서 상태를 바꿈 → UI에 변경사항 자동 반영

MiddleWare ,Thunk 는 왜 필요할까?

1. 미들웨어(Middleware)가 없다면?

순수 Redux 플로우:

  • UI → dispatch(Action) → Reducer → Store State 변경

이 때 "Action"은 반드시 동기적 데이터여야 하고, dispatch에는 단순 객체만 들어갈 수 있음.

예시 (미들웨어 없이):

  • 이 구조에서는 아래처럼 API 호출, 딜레이, 로그 기록 등중간에 넣을 방법이 없음!


2. 미들웨어로 비동기/로깅 등을 삽입

예시: 간단 로그 미들웨어

등록:

이렇게 하면 모든 액션 dispatch 때마다 로그를 남길 수 있음. (이외에도 인증 토큰 갱신, Crashlytics, 외부 통신 등 다 여기서 처리 가능!)


3. Thunk 미들웨어 상세 예시

Thunk가 없다면?

  • Redux 기본 dispatch에는 오직 “객체”만 들어감(비동기 함수 X)

  • 비동기 액션, API 콜백, await 같은 것을 데이터 흐름에 넣을 수 없음


Thunk 미들웨어가 있으면?

Thunk 미들웨어는 "함수"를 dispatch해도 동작! (dispatch에 진짜 동작하는 함수까지 통과시켜줌)

1) 비동기 액션(함수) 정의

2) thunkMiddleware 등록

3) 사용 예시(함수 dispatch 가능!)

  • thunk 없으면 → 에러


  • thunk 있으면 → 함수를 받아서 실행해줌, 그 함수 안에서 여러 번 액션 dispatch OK!

Last updated