[Flutter 공부] GetX 정리
- Developer/Flutter
- 2025. 3. 18.
Flutter로 개발하다 보면 상태 관리 방식을 선택하는 일이 가장 큰 고민거리다. Provider, Bloc, Riverpod, MobX... 선택지는 많은데 무엇이 최선인지 판단하기 어렵다. 이 글에서는 그 중에서 GetX의 핵심 기능들과 실제 프로젝트에서 어떻게 활용할 수 있는지 살펴보자.

목차
GetX란 무엇인가? 특징과 장점
GetX는 Flutter에서 가장 경량화된 상태 관리, 의존성 주입, 라우트 관리 솔루션 중 하나다. 다른 상태 관리 라이브러리들과 달리, GetX는 단순히 상태 관리만을 위한 라이브러리가 아닌 Flutter 애플리케이션 개발에 필요한 거의 모든 기능을 제공한다. 특히 코드 복잡성을 낮추면서도 고성능을 유지하는 데 초점을 맞추고 있다.
작고 큰 프로젝트에서 모두 GetX를 사용해봤지만, 특히 대규모 프로젝트에서 진가를 발휘한다. Bloc 패턴은 보일러플레이트 코드가 너무 많고, Provider는 복잡한 상태 관리에서 한계가 있다. GetX는 이런 문제들을 해결하면서도 성능 저하 없이 직관적인 API를 제공한다.
GetX의 3가지 핵심 기능
GetX는 다음 세 가지 핵심 기능을 중심으로 구성되어 있다:
- 상태 관리(State Management): 반응형과 단순 상태 관리를 모두 지원한다.
- 라우트 관리(Route Management): 네비게이션과 라우트를 간단하게 관리할 수 있다.
- 의존성 관리(Dependency Management): 서비스 로케이터 패턴을 통해 쉽게 의존성을 주입하고 관리할 수 있다.
GetX 설치 및 기본 설정
GetX를 사용하기 위해 먼저 pubspec.yaml 파일에 의존성을 추가해야 한다. 최신 버전을 사용하는 것이 좋지만, 프로젝트의 안정성을 위해 특정 버전을 고정해서 사용할 수도 있다.
dependencies:
flutter:
sdk: flutter
get: ^4.6.5 # 작성 시점 최신 버전
GetX를 프로젝트에 통합하는 방법은 크게 두 가지가 있다:
| 접근 방식 | 장점 | 단점 | 적합한 프로젝트 |
|---|---|---|---|
| GetMaterialApp 사용 | GetX의 모든 기능을 완벽하게 활용 | 기존 MaterialApp을 교체해야 함 | 새 프로젝트 또는 전체 리팩토링 |
| 일부 기능만 사용 | 기존 코드 구조 유지 가능 | 일부 고급 기능 제한 | 기존 프로젝트 점진적 도입 |
| 혼합 방식 | 유연한 도입 방식 | 일관성 없는 코드 가능성 | 중간 규모 프로젝트 |
개인적으로는 새 프로젝트에서는 GetMaterialApp을 사용하는 것이 좋다. 기존 프로젝트에 GetX를 도입할 때는 점진적으로 상태 관리부터 시작해 라우트 관리, 의존성 주입 순으로 도입하는 것이 안전하다.
import 'package:flutter/material.dart';
import 'package:get/get.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return GetMaterialApp( // MaterialApp 대신 GetMaterialApp 사용
title: 'GetX Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
visualDensity: VisualDensity.adaptivePlatformDensity,
),
home: HomePage(),
);
}
}
GetX를 활용한 상태 관리 방법
GetX의 상태 관리는 직관적이면서도 강력하다. 기본적으로 두 가지 상태 관리 방식을 제공한다: 반응형(Reactive) 접근법과 단순(Simple) 접근법. 대부분의 경우 반응형 접근법을 사용하는 것이 좋다.
반응형 상태 관리
반응형 상태 관리는 Rx 변수와 .obs (observable) 접미사를 사용하여 상태를 관리한다. 이는 해당 변수가 변할 때 UI가 자동으로 업데이트되도록 한다.
- 컨트롤러 생성: 비즈니스 로직과 상태를 캡슐화한다.
- Observable 변수 정의: .obs를 사용하여 변수를 관찰 가능하게 만든다.
- UI에 컨트롤러 바인딩: GetX 또는 Obx 위젯을 사용하여 UI와 상태를 연결한다.
- 상태 업데이트: 컨트롤러의 메서드를 통해 상태를 변경한다.
이러한 방식은 상태 변화가 즉시 UI에 반영되도록 하며, Provider나 Bloc에 비해 보일러플레이트 코드가 훨씬 적다.
Q GetBuilder와 Obx의 차이점은 무엇인가?
A GetBuilder는 수동 상태 관리(update() 메서드 호출 필요)를 위한 것이고, Obx는 반응형 상태 관리(자동 업데이트)를 위한 것이다. 성능이 중요한 경우 GetBuilder를, 개발 편의성이 중요한 경우 Obx를 사용해라.
Q GetX의 메모리 관리는 어떻게 작동하는가?
A GetX는 컨트롤러가 더 이상 사용되지 않을 때 자동으로 메모리에서 해제된다. 그러나 Get.put()으로 영구적으로 등록된 컨트롤러는 Get.delete()를 호출하여 명시적으로 제거해야 한다. worker 또한 controller가 dispose 될 때 같이 dispose 되기 때문에 별도 메모리 관리가 필요 없다.
GetX의 의존성 주입(Dependency Injection)
GetX의 의존성 주입 시스템은 서비스 로케이터 패턴을 기반으로 한다. 복잡한 설정 없이도 간단하게 의존성을 등록하고 필요한 곳에서 접근할 수 있다. 이는 클린 아키텍처를 구현하고 테스트 가능한 코드를 작성하는 데 매우 유용하다.
GetX에서 의존성을 주입하는 방법은 다음과 같이 요약할 수 있다:
// 의존성 등록 방법들
Get.put(MyController()); // 일반적인 등록
Get.lazyPut(() => MyService()); // 필요할 때만 초기화
Get.putAsync(() async => await MyAsyncService()); // 비동기 초기화
Get.create(() => MyRepository()); // 호출될 때마다 새 인스턴스 생성
// 의존성 찾기
final controller = Get.find<MyController>();
GetX 의존성 주입 방식의 장단점
GetX의 의존성 주입 방식은 간단하고 직관적이지만, 대규모 프로젝트에서는 몇 가지 제한사항이 있다. 경험에 기반한 장단점을 살펴보자.
- 장점: 설정이 간단하고 직관적이다. 별도의 Provider 설정이나 BLoC 생성이 필요 없다.
- 장점: 라이프사이클 관리가 자동화되어 있다. 상황에 따라 인스턴스 생성과 제거가 자동으로 처리된다.
- 단점: 글로벌 상태를 쉽게 접근할 수 있어 무분별한 사용 시 구조적 문제가 발생할 수 있다.
- 단점: 타입 안전성이 부족하다. 런타임 에러가 발생할 가능성이 있다.
의존성 주입을 효과적으로 사용하려면, 다음과 같은 패턴을 따르는 것이 좋다:
// 의존성 모듈 패턴
class AppBindings extends Bindings {
@override
void dependencies() {
// 레포지토리
Get.put(UserRepository(apiClient: Get.find()));
// 서비스 레이어
Get.put(AuthService(repository: Get.find()));
// 컨트롤러
Get.put(UserController(authService: Get.find()));
}
}
GetX 라우트 관리로 네비게이션 간소화
Flutter의 기본 네비게이션 시스템은 중첩 네비게이션이나 복잡한 라우팅 로직을 다룰 때 번거롭다. GetX는 이러한 복잡성을 크게 줄이면서도 강력한 라우팅 기능을 제공한다.
GetX 라우트 관리의 주요 특징은 다음과 같다:
- 간결한 구문: context 없이 네비게이션 가능
- 명명된 라우트: 문자열 기반 라우트 정의
- 중간 라우트 접근: 네비게이션 스택의 특정 라우트에 직접 접근
- 라우트 미들웨어: 라우트 전환 전/후에 로직 실행
다음은 GetX 라우팅을 설정하고 사용하는 방법이다:
| 네비게이션 작업 | GetX 코드 | 일반 Flutter 코드 |
|---|---|---|
| 새 화면으로 이동 | Get.to(() => NextScreen()); |
Navigator.push(context, MaterialPageRoute(builder: (context) => NextScreen())); |
| 이전 화면으로 돌아가기 | Get.back(); |
Navigator.pop(context); |
| 명명된 라우트로 이동 | Get.toNamed('/details'); |
Navigator.pushNamed(context, '/details'); |
| 데이터 전달 | Get.toNamed('/details', arguments: {'id': 1}); |
Navigator.pushNamed(context, '/details', arguments: {'id': 1}); |
| 모든 화면 제거하고 이동 | Get.offAll(() => HomeScreen()); |
Navigator.pushAndRemoveUntil(context, MaterialPageRoute(builder: (context) => HomeScreen()), (route) => false); |
// 앱 시작 시 라우트 설정
GetMaterialApp(
initialRoute: '/',
getPages: [
GetPage(name: '/', page: () => HomeScreen()),
GetPage(
name: '/profile/:id',
page: () => ProfileScreen(),
transition: Transition.rightToLeft,
// 미들웨어 설정 가능
middlewares: [AuthMiddleware()],
),
GetPage(name: '/settings', page: () => SettingsScreen()),
],
)
// 라우트에서 매개변수 접근
class ProfileScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
final id = Get.parameters['id'];
return Scaffold(
appBar: AppBar(title: Text('Profile $id')),
body: Center(
child: Text('User ID: $id'),
),
);
}
}
실전 패턴: GetX를 활용한 아키텍처 설계
GetX는 단순히 상태 관리 도구를 넘어 애플리케이션의 아키텍처를 결정하는 중요한 요소가 된다. 여러 대규모 프로젝트 경험을 바탕으로 GetX를 활용한 효과적인 아키텍처 패턴을 소개한다.
GetX 기반 Clean Architecture
GetX를 Clean Architecture와 결합하면 테스트 가능하고 유지보수가 용이한 코드를 작성할 수 있다. 다음은 이를 구현하기 위한 폴더 구조와 패턴이다:
- Data Layer: 데이터 소스, 레포지토리 구현, 모델을 포함
- Domain Layer: 비즈니스 로직, 엔티티, 유스 케이스를 포함
- Presentation Layer: 화면, 컨트롤러(GetX), 바인딩을 포함
- Core: 유틸리티, 상수, 전역 설정을 포함
이 구조를 따르면 관심사가 명확히 분리되고, 테스트와 유지보수가 용이해진다.
lib/
├── core/
│ ├── constants/
│ ├── errors/
│ ├── network/
│ └── utils/
├── data/
│ ├── datasources/
│ ├── models/
│ └── repositories/
├── domain/
│ ├── entities/
│ ├── repositories/
│ └── usecases/
├── presentation/
│ ├── bindings/
│ ├── controllers/
│ ├── pages/
│ └── widgets/
└── main.dart
Q GetX 프로젝트에서 코드 재사용성을 높이는 가장 좋은 방법은?
A GetX 프로젝트에서 코드 재사용성을 높이기 위해서는 서비스 계층을 활용하는 것이 좋다. 컨트롤러 대신 비즈니스 로직을 서비스에 두고, 컨트롤러는 이러한 서비스를 호출하는 역할만 담당하게 하면 동일한 비즈니스 로직을 여러 화면에서 재사용할 수 있다. 또한 컨트롤러 로직과 UI 분리가 잘 되어서 테스트를 더 용이하게 만든다.
Q GetX와 기존 Flutter 위젯을 섞어서 사용해도 괜찮은가?
A 물론이다. GetX는 기존 Flutter 위젯과 완벽하게 호환된다. StatelessWidget과 StatefulWidget을 GetX 컨트롤러와 함께 사용할 수 있다. 점진적으로 GetX를 도입하는 경우, 기존 위젯을 유지하면서 새로운 기능을 GetX로 개발하는 것이 가능하다. 다만 일관성을 위해 한 화면에서는 가능하면 동일한 패턴을 사용하는 것이 좋다.
Q GetX를 사용할 때 메모리 사용량을 최적화하는 방법은?
A GetX의 메모리 사용량을 최적화하기 위해 다음 패턴을 사용해라. 1) GetBuilder를 사용하여 수동으로 UI 업데이트를 제어, 2) SmartManagement.full 옵션으로 자동 메모리 관리 활성화, 3) 폐기된 화면의 컨트롤러는 Get.delete<T>()를 사용하여 명시적으로 제거, 4) 큰 데이터 집합은 필요한 부분만 메모리에 로드. 또한 영구적으로 필요한 컨트롤러는 permanent: true 옵션으로 등록하여 메모리에서 제거되지 않도록 하고, 임시로 필요한 컨트롤러는 fenix: true 옵션으로 등록하여 필요할 때만 생성되고 필요 없을 때 제거되도록 할 수 있다.
Q GetX에서 렌더링 성능을 높이는 방법은?
A GetX의 렌더링 성능을 최적화하려면: 1) GetView 대신 GetWidget을 사용하여 컨트롤러 재사용, 2) 세밀한 상태 관리를 위해 전체 화면이 아닌 작은 위젯에 Obx 적용, 3) .obs 변수를 남용하지 말고 필요한 변수만 반응형으로 만들기, 4) worker를 사용할 때 debounce, once 등을 적절히 활용하여 불필요한 작업 줄이기, 5) GetX의 ever, debounce, interval과 같은 리액티브 함수를 사용하여 UI 업데이트 최적화. 리스트 렌더링 시에는 반드시 ListView.builder와 함께 사용하고, RxList에 대한 변경은 가능한 한 배치 처리하라.
종합 코드 예제: GetX를 활용한 완전한 CRUD 앱
이제 GetX의 모든 기능을 활용하여 완전한 CRUD(Create, Read, Update, Delete) 기능을 갖춘 간단한 Todo 앱을 구현해보자. 이 예제는 상태 관리, 라우팅, 의존성 주입 등 GetX의 핵심 기능을 모두 보여준다.
전체 코드는 다음과 같이 구성된다:
// 1. 모델 클래스 정의
class Todo {
final int id;
final String title;
final bool completed;
Todo({
required this.id,
required this.title,
this.completed = false,
});
Todo copyWith({
int? id,
String? title,
bool? completed,
}) {
return Todo(
id: id ?? this.id,
title: title ?? this.title,
completed: completed ?? this.completed,
);
}
factory Todo.fromJson(Map<String, dynamic> json) {
return Todo(
id: json['id'],
title: json['title'],
completed: json['completed'],
);
}
Map<String, dynamic> toJson() {
return {
'id': id,
'title': title,
'completed': completed,
};
}
}
// 2. API 서비스 클래스
class TodoApiService {
final _baseUrl = 'https://jsonplaceholder.typicode.com/todos';
Future<List<Todo>> getTodos() async {
try {
final response = await http.get(Uri.parse(_baseUrl));
if (response.statusCode == 200) {
final List<dynamic> data = json.decode(response.body);
return data.map((json) => Todo.fromJson(json)).toList();
} else {
throw Exception('Failed to load todos');
}
} catch (e) {
throw Exception('Error fetching todos: $e');
}
}
Future<Todo> createTodo(String title) async {
try {
final response = await http.post(
Uri.parse(_baseUrl),
body: json.encode({
'title': title,
'completed': false,
}),
headers: {'Content-Type': 'application/json'},
);
if (response.statusCode == 201) {
return Todo.fromJson(json.decode(response.body));
} else {
throw Exception('Failed to create todo');
}
} catch (e) {
throw Exception('Error creating todo: $e');
}
}
// 삭제 및 업데이트 메서드도 비슷한 패턴으로 구현
}
// 3. 컨트롤러 클래스
class TodoController extends GetxController {
final TodoApiService apiService;
TodoController({required this.apiService});
var todos = <Todo>[].obs;
var isLoading = false.obs;
var errorMessage = ''.obs;
@override
void onInit() {
super.onInit();
fetchTodos();
}
Future<void> fetchTodos() async {
isLoading.value = true;
errorMessage.value = '';
try {
final fetchedTodos = await apiService.getTodos();
todos.value = fetchedTodos;
} catch (e) {
errorMessage.value = e.toString();
} finally {
isLoading.value = false;
}
}
Future<void> addTodo(String title) async {
isLoading.value = true;
errorMessage.value = '';
try {
final newTodo = await apiService.createTodo(title);
todos.add(newTodo);
} catch (e) {
errorMessage.value = e.toString();
} finally {
isLoading.value = false;
}
}
void toggleTodoStatus(int id) {
final index = todos.indexWhere((todo) => todo.id == id);
if (index != -1) {
final todo = todos[index];
todos[index] = todo.copyWith(completed: !todo.completed);
}
}
void removeTodo(int id) {
todos.removeWhere((todo) => todo.id == id);
}
}
// 4. 바인딩 클래스
class TodoBinding extends Bindings {
@override
void dependencies() {
Get.put(TodoApiService());
Get.put(TodoController(apiService: Get.find()));
}
}
// 5. 메인 화면
class TodoListScreen extends GetView<TodoController> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('GetX Todo App')),
body: Obx(() {
if (controller.isLoading.value) {
return Center(child: CircularProgressIndicator());
}
if (controller.errorMessage.value.isNotEmpty) {
return Center(child: Text('Error: ${controller.errorMessage.value}'));
}
return ListView.builder(
itemCount: controller.todos.length,
itemBuilder: (context, index) {
final todo = controller.todos[index];
return ListTile(
title: Text(
todo.title,
style: TextStyle(
decoration: todo.completed ? TextDecoration.lineThrough : null,
),
),
leading: Checkbox(
value: todo.completed,
onChanged: (_) => controller.toggleTodoStatus(todo.id),
),
trailing: IconButton(
icon: Icon(Icons.delete),
onPressed: () => controller.removeTodo(todo.id),
),
);
},
);
}),
floatingActionButton: FloatingActionButton(
child: Icon(Icons.add),
onPressed: () => Get.toNamed('/add'),
),
);
}
}
// 6. 할 일 추가 화면
class AddTodoScreen extends GetView<TodoController> {
final TextEditingController textController = TextEditingController();
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Add Todo')),
body: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
children: [
TextField(
controller: textController,
decoration: InputDecoration(
labelText: 'Todo Title',
border: OutlineInputBorder(),
),
),
SizedBox(height: 16),
ElevatedButton(
onPressed: () {
if (textController.text.isNotEmpty) {
controller.addTodo(textController.text);
Get.back();
}
},
child: Text('Add Todo'),
),
],
),
),
);
}
}
// 7. 앱 구성
void main() {
runApp(
GetMaterialApp(
title: 'GetX Todo App',
initialRoute: '/todos',
getPages: [
GetPage(
name: '/todos',
page: () => TodoListScreen(),
binding: TodoBinding(),
),
GetPage(
name: '/add',
page: () => AddTodoScreen(),
),
],
),
);
}
이 코드 예제에서 볼 수 있듯이, GetX를 사용하면 많은 보일러플레이트 코드 없이도 완전한 CRUD 애플리케이션을 구현할 수 있다. 특히 주목할 점은 다음과 같다:
- 상태 관리의 단순함: Obx 위젯과 .obs 변수만으로 UI가 자동으로 업데이트된다.
- 의존성 주입의 간결함: TodoBinding 클래스를 통해 의존성이 자동으로 관리된다.
- 라우팅의 간소화: GetPage를 사용하여 명명된 라우트를 쉽게 정의하고, Get.toNamed()를 사용하여 간단하게 네비게이션을 구현했다.
- 코드 분리: 모델, 서비스, 컨트롤러, 화면이 명확하게 분리되어 있어 유지보수가 용이하다.
이 코드를 기반으로 자신의 프로젝트에 맞게 수정하고 확장하여 GetX의 장점을 최대한 활용할 수 있다. GetX가 제공하는 직관적인 API와 패턴을 따르면 코드 복잡성을 크게 줄이면서도 강력한 애플리케이션을 개발할 수 있다.
마무리: GetX를 실무에 도입하기
GetX는 강력하지만 만능이 아니다. 모든 프로젝트에 무조건 GetX를 도입하기보다는 프로젝트의 요구사항과 팀의 성향을 고려해 결정해야 한다. 개인적으로 5개 이상의 대규모 프로젝트에 GetX를 도입해 본 결과, 중소규모 팀에서 빠르게 개발해야 하는 프로젝트에 특히 효과적이었다. 복잡한 상태 관리와 라우팅을 간소화하면서도 성능을 유지할 수 있는 GetX의 특성은 프로젝트 일정이 촉박할 때 큰 도움이 된다.
다만 몇 가지 주의점도 있다. GetX의 전역 상태 접근 패턴은 남용하면 코드 이해도를 떨어뜨릴 수 있다. 또한 GetX의 편리함에 의존하여 아키텍처 설계를 소홀히 하면 장기적으로 유지보수 문제가 발생할 수 있다. 이를 방지하기 위해 레이어드 아키텍처나 클린 아키텍처와 같은 설계 원칙을 GetX와 함께 적용하는 것이 좋다.
GetX는 지속적으로 발전하고 있으며, Flutter 업데이트에 따라 최적화되고 있다. 이 글에서 다룬 내용은 2025년 3월 기준 GetX 4.6.x 버전을 기반으로 한다. 새로운 버전이 출시되면 공식 문서를 참고하여 변경 사항을 확인하는 것이 좋다.
마지막으로, GetX는 상태 관리를 넘어 Flutter 개발 전반의 생산성을 높여주는 도구다. 직접 GetX를 사용해보면 그 편리함과 효율성을 체감할 수 있을 것이다. MVC, MVP, MVVM 등 어떤 아키텍처 패턴을 선호하든 GetX는 그 패턴에 맞게 적용할 수 있다. 주저하지 말고 GetX를 시도해보고, Flutter 개발의 효율성을 높여보자.
'Developer > Flutter' 카테고리의 다른 글
| [Flutter 공부] 복잡한 애니메이션 구현하기 (0) | 2025.03.18 |
|---|---|
| [Flutter 공부] Riverpod (0) | 2025.03.18 |
| [Flutter 공부] 상태관리, BLoC 패턴 (0) | 2025.03.17 |
| [Flutter 공부] 성능 최적화, 앱 성능 모니터링과 개선 방법 (0) | 2025.03.16 |
| [Flutter 공부] 반응형 UI 구현하기 - 다양한 화면 크기에 적응하는 앱 개발 (0) | 2025.03.16 |