TIL) 11/24 provider를 이용해서 상태 관리를 해봤다

100·2025년 11월 24일

TIL

목록 보기
5/11

Flutter 상태관리 정리

Flutter에서는 화면을 구성하는 모든 요소가 위젯이며, 위젯들은 부모-자식 관계로 이루어진 트리 구조를 형성한다. 위젯은 불변이기 때문에, UI가 변경될 때는 기존 위젯을 수정하는 대신 새로운 위젯을 다시 그려주는 방식으로 동작한다.


StatelessWidget과 StatefulWidget

공통점

두 위젯 모두 build(BuildContext) 메서드 안에서 “이 위젯이 어떤 UI인지”를 선언한다.

StatelessWidget

  • 내부에 변경되는 상태가 없다.
  • Text, Icon, Padding 같은 정적인 UI에 사용된다.
  • 동일한 입력이 주어지면 항상 동일한 결과를 반환하며, 부모 위젯이 재빌드될 때만 다시 그려진다.

StatefulWidget

  • 변경 가능한 상태를 가진다.
  • StatefulWidget과 실제 상태를 보관하는 State 객체가 쌍으로 동작한다.
  • setState()를 호출하면 해당 위젯만 다시 빌드되어 동적 UI를 만들 수 있다.
  • 사용자 입력, 값 변화, 애니메이션 등 시간이 지나며 변하는 UI에 사용한다.

상태(State)의 개념

상태는 UI를 그리기 위해 필요한 데이터나 리소스를 의미한다. Flutter에서는 상태를 크게 두 가지로 나눈다.

Ephemeral State (지역 상태)

  • 한 위젯 내부에서만 잠시 필요한 상태
  • 예: 체크박스 선택 여부, 특정 화면의 입력값
  • State + setState()로 관리한다.

App State (전역/공유 상태)

  • 여러 화면 또는 여러 위젯에서 함께 사용하는 상태
  • 예: 로그인 정보, 장바구니, 앱 설정 값
  • InheritedWidget, Provider 등 상태관리 도구를 사용해 관리한다.

주요 상태관리 방식 요약

StatefulWidget + setState

  • 간단한 로컬 상태에 적합
  • 추가 패키지 없이 사용할 수 있고 이해하기 쉽다
  • 하지만 공유 상태가 많아질수록 구조가 복잡해진다

InheritedWidget 계열 (InheritedNotifier / InheritedModel)

  • 트리 상단에서 하위 위젯들에게 상태를 전달하는 Flutter 기본 메커니즘
  • 직접 활용하면 코드가 복잡해 주로 기반 개념으로만 사용된다

ChangeNotifier + Provider

  • Flutter 공식이 초보자에게 추천하는 방식
  • ChangeNotifier 안에 상태를 보관하고 notifyListeners()로 변경을 알린다
  • Provider는 이 변경을 구독하고 필요한 위젯만 다시 빌드한다
  • 전역 상태를 깔끔하게 관리할 수 있고, MultiProvider로 여러 상태를 함께 관리할 수 있다
  • 상태가 커지면 ChangeNotifier가 비대해지는 단점이 있다

기타 상태 관리 패키지 (Bloc, Riverpod 등)

  • 팀 규모나 프로젝트 복잡도에 따라 선택
  • 구조적이고 테스트하기 쉬운 상태관리 패턴 제공
  • 보일러플레이트가 줄거나, 더 명확한 아키텍처를 만들 수 있다
  • 러닝 커브가 존재하고 패키지 의존성이 추가된다

Flutter 상태관리와 Provider, 그리고 Repository 아키텍처 정리

Flutter에서 화면은 모두 위젯으로 이루어져 있으며, UI는 위젯 트리 형태로 그려진다. 위젯은 불변이기 때문에 상태가 변경되면 기존 위젯을 수정하는 것이 아니라 새로운 위젯을 다시 생성하는 방식으로 갱신된다. 이로 인해 상태가 필요 없는 UI는 StatelessWidget, 상태가 필요한 UI는 StatefulWidget으로 나뉘게 된다.

StatefulWidget은 특정 위젯 내부에서만 유효한 “지역 상태(ephemeral state)”를 관리하기에는 충분하지만, 앱의 여러 화면에서 공유해야 하는 전역 상태를 관리하기에는 한계가 있다. setState는 오직 자신의 위젯 내부만 업데이트하기 때문에, 상태를 다른 화면에 전달하려면 각 단계마다 파라미터로 넘겨야 하고, 구조가 빠르게 복잡해진다.

이 문제를 해결하기 위해 Flutter에서 널리 사용하는 방식이 Provider다. Provider는 ChangeNotifier 기반으로 상태를 객체 하나에 모아두고, notifyListeners()를 호출했을 때 해당 객체를 구독하는 Consumer만 다시 빌드된다. 이 덕분에 부모 위젯이 데이터를 일일이 전달하지 않아도 되고, 화면 어디에서든 동일한 상태를 쉽게 접근하고 변경할 수 있다.


나는 provider로 전역적으로 상태관리가 가능하도록 해서 학습을 하고 있었는데,
팀장님께서 실무에서 쓰는 상황들을 더 설명해주셨다.
아래 내용은 설명해주신걸 기억하기 위해 정리해둔 글이다.


Provider의 범위(Scope)와 라이프사이클

Provider는 흔히 “전역 상태관리 도구”로 오해되지만, 실제로는 상태를 원하는 범위의 서브트리에만 주입할 수 있는 도구다.
즉, Provider는 반드시 루트에만 두라는 규칙이 없다.

예를 들어 앱 구조가 다음과 같다고 하자.

MultiProvider
  └── App
        ├── Route A
        └── Route B

Provider는 다음과 같은 범위 중 어디든 주입할 수 있다.

  • App 위 (전역)
  • App 아래
  • Route A 트리 내부
  • Route B의 하위 위젯에서만 사용

Provider는 자신이 주입된 위치에 따라 lifespan(생명주기)이 결정된다.
Provider가 트리에 붙어 있는 동안만 살아 있고, 해당 트리가 사라지면 Provider도 자동으로 dispose된다.

이 특징 때문에 실무에서는 다음과 같이 나누는 패턴이 일반적이다.

  • 앱 전체에서 필요한 상태

    • 예: 로그인한 User 정보, 현재 재생 중인 음악, 설정 값
    • → 루트(MultiProvider) 또는 최상단에 주입
  • 특정 플로우 안에서만 필요한 상태

    • 예: 회원가입 단계별 입력값, 특정 페이지에서만 쓰는 임시 필터 값
    • → 해당 Route 또는 해당 화면 트리에만 Provider 주입

이렇게 하면 “전역에 올릴 필요 없는 상태”를 전역에 올려서 메모리를 낭비하거나 잔존 데이터를 남기는 문제를 피할 수 있다.


Provider Shadowing: 가장 가까운 Provider를 읽는다

같은 타입의 Provider가 여러 레벨에 있을 수도 있다.
이 경우 Consumer<T> 또는 context.watch<T>()트리 아래에서 위로 올라가며 가장 가까운 Provider를 읽는다.

즉,

  • Consumer는 “이 값이 어디서 왔는지” 모른다.
  • 단지 “내 트리에서 가장 가까운 Provider”를 구독할 뿐이다.

이 특징을 이용하면 다음과 같은 패턴도 가능하다.

  • 플레이어 상태는 전역 Provider에 두고
  • 추천 페이지에서만 필요한 추천 알고리즘 상태는 추천 페이지 트리에 Provider를 두고
  • 페이지를 벗어나면 추천 상태만 자연스럽게 dispose됨
  • 하지만 전역 Provider에 있는 플레이어 상태는 유지됨

실무에서 화면별 상태와 전역 상태를 자연스럽게 분리하는 데 매우 유용한 방식이다.


Flutter에서도 Repository 레이어가 필요한 이유

상태관리와 별개로, Flutter의 아키텍처에서도 자바/Spring과 동일한 방식으로 Repository 패턴을 사용할 수 있다.

Repository의 목적은 명확하다.
“UI나 비즈니스 로직이 데이터가 어디서, 어떻게 오는지 모르게 한다."

Dart에는 interface 키워드는 없지만, abstract class를 통해 인터페이스와 동일한 역할을 만들 수 있다.
Cubit/Bloc은 Repository의 인터페이스만 의존하고, API/캐시/Mock 구현체는 뒤에서 교체 가능하다.


레이어 구조

여러 디자인 패턴들이 있지만 다 비슷비슷하다. 중요한건 인터페이스를 잘 만드는 것이다.
내가 참여할 파트의 구조는 이런 식으로 되어있다.

Screen → Cubit/Bloc → Repository(인터페이스) → 구현체(API/캐시/Local)
  • Screen
    UI 렌더링, 사용자 입력을 Cubit/Bloc에게 전달
  • Cubit/Bloc
    화면 상태/비즈니스 로직 담당. Repository 인터페이스만 호출
  • Repository (abstract class)
    앱 관점의 “데이터 가져오기 API” 정의
  • 구현체(Impl)
    실제 HTTP 요청, 로컬 DB, 캐시 등 상세한 데이터 접근 로직

이 구조가 가지는 가장 큰 장점은 UI가 데이터 출처를 전혀 모른다는 점이다.
UI 입장에서는 단지 getUser()를 호출하는 것뿐이며,
그 뒤에서 실제 요청이 어떤 방식으로 이루어지는지는 Repository가 책임진다.


인터페이스를 잘 만드는 것이 핵심

Repository 패턴의 가치는 단순히 “코드 나누기”가 아니라,
UI가 비즈니스 의미만 이해하고 구현 세부 사항을 모르게 만드는 데 있다.

Bad 예시:

Future<Response> get(String url);

Good 예시:

abstract class MusicRepository {
  Future<List<Music>> getRecommended();
}

이렇게 Repository 이름과 메서드가 “도메인 용어”를 표현해야
UI와 Cubit/Bloc이 데이터 구조 변경에 영향을 받지 않는다.


왜 Repository 패턴을 써야 하냐?

  • API 변경에도 UI 로직 수정 없음
  • 테스트 시 Mock Repository로 손쉽게 대체 가능
  • 로컬 캐시 + API + DB 같은 복합 데이터 소스 구성에 적합
  • 화면 로직(Cubit/Bloc)이 깔끔해지고 유지보수성 증가
  • 실무 앱 대부분이 이 방식으로 성장함

즉, Flutter에서도 Repository 레이어는
데이터 로직을 독립시키고 UI·상태 로직을 깨끗하게 유지하는 핵심 아키텍처다.

이제 cubit을 이용한 걸로 다시 한번 로그인-회원가입-ㅁㅁ 등록 흐름을 만들어볼거다.
Event → State 식으로 하면 복잡해지는 부분이? 있기 때문에 cubit만을 사용할 것 같다.
지금 일단 알려주신 단축키들 잘 활용해보자.
cmd + . : 전구기능 실행 / option + shift + F : 줄맞춤 / Snippet + tap : 자동 완성

profile
멋있는 사람이 되는 게 꿈입니다

0개의 댓글