플러터 프로젝트 구조

소요 시간: 3분

플러터(Flutter) 프로젝트의 폴더 구조에 대해 좀 더 깊이 공부해봤다. 평소에 프로젝트를 진행하면서 여러 디렉토리가 어떻게 구성되어 있는지 궁금했는데, 이번 기회에 제대로 정리해보고자 한다.


플러터 프로젝트를 새로 생성하면 여러 폴더들이 보인다. 그 중에서 가장 중요한 곳은 lib 폴더다. 이 폴더에는 앱의 모든 주요 소스 코드가 담겨 있다.

특히 main.dart 파일이 앱의 진입점인데, 여기에 있는 main() 함수에서 앱이 시작된다. 예시 코드를 보면 이해가 쉽다.

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(title: Text('My Flutter App')),
        body: Center(child: Text('Hello, Flutter!')),
      ),
    );
  }
}

이 코드는 기본적인 플러터 앱의 구조다. runApp()을 통해 앱이 실행되고, MaterialApp으로 앱의 전반적인 구성을 정리한다.


좀 더 복잡한 앱을 만들 때는 lib 폴더 아래에 추가적인 디렉토리를 나누기도 한다. 예를 들어:

screens/ 폴더는 각 페이지나 화면을 정의하는 곳이다. HomeScreen.dart나 LoginScreen.dart 같은 파일을 여기에 두어 페이지별로 코드를 분리할 수 있다. 예를 들어, HomeScreen은 이렇게 만들 수 있다.

import 'package:flutter/material.dart';

class HomeScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Home')),
      body: Center(child: Text('Welcome to the Home Screen')),
    );
  }
}

widgets/ 폴더는 재사용 가능한 위젯을 모아두는 곳이다. 예를 들어, 커스텀 버튼을 만들고 싶을 때 CustomButton.dart라는 파일을 만들 수 있다.

import 'package:flutter/material.dart';

class CustomButton extends StatelessWidget {
  final String label;
  final VoidCallback onPressed;

  CustomButton({required this.label, required this.onPressed});

  @override
  Widget build(BuildContext context) {
    return ElevatedButton(
      onPressed: onPressed,
      child: Text(label),
    );
  }
}

models/ 폴더에는 데이터 모델을 정의할 수 있다. 예를 들어 UserModel.dart라는 파일을 통해 사용자 데이터를 다룰 수 있다.

class UserModel {
  final String name;
  final int age;

  UserModel({required this.name, required this.age});

  factory UserModel.fromJson(Map<String, dynamic> json) {
    return UserModel(
      name: json['name'],
      age: json['age'],
    );
  }

  Map<String, dynamic> toJson() {
    return {
      'name': name,
      'age': age,
    };
  }
}

이제 test/ 디렉토리에 대해 살펴보자. 여기엔 테스트 코드가 들어가며, 위젯이나 비즈니스 로직을 테스트할 수 있다. 기본적으로 생성되는 widget_test.dart 파일은 앱의 위젯이 잘 동작하는지를 확인할 수 있는 테스트를 포함하고 있다. 예시로 보이는 코드는 아래와 같다.

import 'package:flutter_test/flutter_test.dart';
import 'package:flutter/material.dart';
import 'package:my_app/main.dart'; // main.dart를 가리키는 경로

void main() {
  testWidgets('Counter increments smoke test', (WidgetTester tester) async {
    await tester.pumpWidget(MyApp());

    expect(find.text('0'), findsOneWidget);
    expect(find.text('1'), findsNothing);

    await tester.tap(find.byIcon(Icons.add));
    await tester.pump();

    expect(find.text('0'), findsNothing);
    expect(find.text('1'), findsOneWidget);
  });
}


그 밖에도 android/와 ios/ 디렉토리는 각각 네이티브 설정을 다룬다.

예를 들어, android 디렉토리 안의 AndroidManifest.xml에서는 앱의 퍼미션을 설정할 수 있다. ios 디렉토리의 Info.plist는 iOS 앱 설정에 필요하다.

마지막으로 pubspec.yaml 파일도 중요한데, 앱에서 사용하는 의존성과 리소스를 관리한다. 예를 들어, 새로운 패키지를 추가하고 싶을 때는 여기에 추가하면 된다.

dependencies:
  flutter:
    sdk: flutter
  http: ^0.13.0  # 예시로 HTTP 패키지를 추가

오늘은 이렇게 정리하면서 플러터 프로젝트의 구조가 어떻게 작동하는지를 알게 되었다. 처음에는 복잡하게 보였지만, 예시 코드와 함께 보니 한결 이해가 쉽다. 앞으로 프로젝트를 진행할 때 이 구조를 잘 활용해보고 싶다.


플러터 리스트