Flutter에서는 화면을 구성하는 모든 요소가 위젯이며, 위젯들은 부모-자식 관계로 이루어진 트리 구조를 형성한다. 위젯은 불변이기 때문에, UI가 변경될 때는 기존 위젯을 수정하는 대신 새로운 위젯을 다시 그려주는 방식으로 동작한다.
두 위젯 모두 build(BuildContext) 메서드 안에서 “이 위젯이 어떤 UI인지”를 선언한다.
Text, Icon, Padding 같은 정적인 UI에 사용된다.StatefulWidget과 실제 상태를 보관하는 State 객체가 쌍으로 동작한다.setState()를 호출하면 해당 위젯만 다시 빌드되어 동적 UI를 만들 수 있다.상태는 UI를 그리기 위해 필요한 데이터나 리소스를 의미한다. Flutter에서는 상태를 크게 두 가지로 나눈다.
State + setState()로 관리한다.notifyListeners()로 변경을 알린다Flutter에서 화면은 모두 위젯으로 이루어져 있으며, UI는 위젯 트리 형태로 그려진다. 위젯은 불변이기 때문에 상태가 변경되면 기존 위젯을 수정하는 것이 아니라 새로운 위젯을 다시 생성하는 방식으로 갱신된다. 이로 인해 상태가 필요 없는 UI는 StatelessWidget, 상태가 필요한 UI는 StatefulWidget으로 나뉘게 된다.
StatefulWidget은 특정 위젯 내부에서만 유효한 “지역 상태(ephemeral state)”를 관리하기에는 충분하지만, 앱의 여러 화면에서 공유해야 하는 전역 상태를 관리하기에는 한계가 있다. setState는 오직 자신의 위젯 내부만 업데이트하기 때문에, 상태를 다른 화면에 전달하려면 각 단계마다 파라미터로 넘겨야 하고, 구조가 빠르게 복잡해진다.
이 문제를 해결하기 위해 Flutter에서 널리 사용하는 방식이 Provider다. Provider는 ChangeNotifier 기반으로 상태를 객체 하나에 모아두고, notifyListeners()를 호출했을 때 해당 객체를 구독하는 Consumer만 다시 빌드된다. 이 덕분에 부모 위젯이 데이터를 일일이 전달하지 않아도 되고, 화면 어디에서든 동일한 상태를 쉽게 접근하고 변경할 수 있다.
나는 provider로 전역적으로 상태관리가 가능하도록 해서 학습을 하고 있었는데,
팀장님께서 실무에서 쓰는 상황들을 더 설명해주셨다.
아래 내용은 설명해주신걸 기억하기 위해 정리해둔 글이다.
Provider는 흔히 “전역 상태관리 도구”로 오해되지만, 실제로는 상태를 원하는 범위의 서브트리에만 주입할 수 있는 도구다.
즉, Provider는 반드시 루트에만 두라는 규칙이 없다.
예를 들어 앱 구조가 다음과 같다고 하자.
MultiProvider
└── App
├── Route A
└── Route B
Provider는 다음과 같은 범위 중 어디든 주입할 수 있다.
Provider는 자신이 주입된 위치에 따라 lifespan(생명주기)이 결정된다.
Provider가 트리에 붙어 있는 동안만 살아 있고, 해당 트리가 사라지면 Provider도 자동으로 dispose된다.
이 특징 때문에 실무에서는 다음과 같이 나누는 패턴이 일반적이다.
앱 전체에서 필요한 상태
특정 플로우 안에서만 필요한 상태
이렇게 하면 “전역에 올릴 필요 없는 상태”를 전역에 올려서 메모리를 낭비하거나 잔존 데이터를 남기는 문제를 피할 수 있다.
같은 타입의 Provider가 여러 레벨에 있을 수도 있다.
이 경우 Consumer<T> 또는 context.watch<T>()는 트리 아래에서 위로 올라가며 가장 가까운 Provider를 읽는다.
즉,
이 특징을 이용하면 다음과 같은 패턴도 가능하다.
실무에서 화면별 상태와 전역 상태를 자연스럽게 분리하는 데 매우 유용한 방식이다.
상태관리와 별개로, Flutter의 아키텍처에서도 자바/Spring과 동일한 방식으로 Repository 패턴을 사용할 수 있다.
Repository의 목적은 명확하다.
“UI나 비즈니스 로직이 데이터가 어디서, 어떻게 오는지 모르게 한다."
Dart에는 interface 키워드는 없지만, abstract class를 통해 인터페이스와 동일한 역할을 만들 수 있다.
Cubit/Bloc은 Repository의 인터페이스만 의존하고, API/캐시/Mock 구현체는 뒤에서 교체 가능하다.
여러 디자인 패턴들이 있지만 다 비슷비슷하다. 중요한건 인터페이스를 잘 만드는 것이다.
내가 참여할 파트의 구조는 이런 식으로 되어있다.
Screen → Cubit/Bloc → Repository(인터페이스) → 구현체(API/캐시/Local)
이 구조가 가지는 가장 큰 장점은 UI가 데이터 출처를 전혀 모른다는 점이다.
UI 입장에서는 단지 getUser()를 호출하는 것뿐이며,
그 뒤에서 실제 요청이 어떤 방식으로 이루어지는지는 Repository가 책임진다.
Repository 패턴의 가치는 단순히 “코드 나누기”가 아니라,
UI가 비즈니스 의미만 이해하고 구현 세부 사항을 모르게 만드는 데 있다.
Bad 예시:
Future<Response> get(String url);
Good 예시:
abstract class MusicRepository {
Future<List<Music>> getRecommended();
}
이렇게 Repository 이름과 메서드가 “도메인 용어”를 표현해야
UI와 Cubit/Bloc이 데이터 구조 변경에 영향을 받지 않는다.
즉, Flutter에서도 Repository 레이어는
데이터 로직을 독립시키고 UI·상태 로직을 깨끗하게 유지하는 핵심 아키텍처다.
이제 cubit을 이용한 걸로 다시 한번 로그인-회원가입-ㅁㅁ 등록 흐름을 만들어볼거다.
Event → State 식으로 하면 복잡해지는 부분이? 있기 때문에 cubit만을 사용할 것 같다.
지금 일단 알려주신 단축키들 잘 활용해보자.
cmd + .: 전구기능 실행 /option + shift + F: 줄맞춤 /Snippet + tap: 자동 완성