첫 Flutter 프로젝트 이해하기

지난 수업에서 첫 Flutter 프로젝트를 만들었습니다. 프로젝트가 매우 간단한데 버튼 하나만 있고 버튼을 클릭하면 숫자가 증가하는 기능입니다. 아래는 이 프로젝트의 디렉터리 구조입니다.

프로젝트 디렉터리

android, web, windows, lib, test 등 많은 폴더와 파일이 보일 겁니다. 그렇다면 우리가 작성할 소스 코드는 어디에 있을까요? android? windows? 사실 그곳이 아니라 lib 폴더 안에 있습니다. lib를 열어보면 main.dart 파일이 보입니다.

이 lib 폴더는 앞으로 모든 소스 코드를 저장하는 곳입니다. 작성하는 언어는 Dart이며 Java나 C++가 아닙니다. Flutter는 Dart 언어를 사용합니다. 그런데 왜 프로젝트 루트에 android, web, windows 같은 디렉터리가 존재할까요?

이해하기 쉽게 설명하면 Flutter가 여러 플랫폼용 앱으로 컴파일될 수 있는 건 이 폴더들 덕분입니다.

Flutter는 단 한 세트의 Dart 코드로 다중 플랫폼에서 실행하는 프레임워크입니다. lib 폴더는 직접 작성하는 플랫폼 공통 비즈니스 코드를 담고 있습니다. 반면 android / windows / web / macos / linux 폴더들은 각 플랫폼 네이티브 호스트 프로젝트로, Flutter 엔진을 해당 시스템에 내장해 실행하는 역할을 수행합니다.

1 android 폴더

안드로이드 플랫폼 네이티브 프로젝트입니다.

안드로이드 앱 실행 컨테이너를 제공하고 Flutter 렌더링 엔진을 탑재합니다. 안드로이드 관련 하위 설정은 모두 여기서 관리합니다. 예를 들어 앱 권한 설정, 아이콘, 패키지 이름 등을 수정할 수 있습니다. 즉 안드로이드 고유 기능을 설정하거나 카메라, 블루투스, 알림, 위치 등 안드로이드 네이티브 API를 호출할 수 있습니다.

2 windows 폴더

Windows 데스크톱 네이티브 프로젝트입니다.

C++ Win32 네이티브 프로젝트로 Windows 시스템 창을 생성하고 Skia 렌더링 엔진을 초기화합니다. Skia는 구글 그래픽 라이브러리로 Flutter 데스크톱 앱의 창과 각종 컨트롤을 렌더링하는 데 사용됩니다.

windows 디렉터리 안에는 Windows 데스크톱 소프트웨어 하위 설정이 담겨있습니다. 창 크기, 창 제목 표시줄, 아이콘, 시스템 트레이(Windows 오른쪽 하단 아이콘) 등을 설정할 수 있습니다.

최종적으로 exe, msix 형식으로 패키징해 마이크로소프트 스토어에 배포할 수 있습니다.

3. web 폴더

정적 웹 페이지 프로젝트(HTML/JS/CSS)로 웹 진입점 파일 index.html이 제공됩니다. 작성한 Dart 소스는 최종 웹 리소스로 컴파일됩니다.

웹 페이지 제목, 아이콘, 정적 리소스, 라우팅을 설정할 수 있으며 컴파일하면 HTML/CSS/JS 파일이 출력되어 서버에 업로드한 뒤 브라우저로 열 수 있습니다.

기타 폴더

프로젝트 생성 당시 ios, macos, Linux 등 옵션을 체크하지 않았기 때문에 이 폴더들은 없지만 원리는 모두 동일합니다.

ios:애플 iOS 네이티브 Xcode 프로젝트, ipa 파일로 패키징해 아이폰에 출시

macos:macOS 데스크톱 코코아 프로젝트, 맥 컴퓨터 클라이언트용

linux:Linux 데스크톱 GTK 프로젝트

안드로이드 스튜디오 오른쪽 상단에서 플랫폼을 선택하면 각 버전의 앱을 컴파일할 수 있습니다. 명령줄로 직접 실행하는 것도 가능하며, 안드로이드 스튜디오에서 플랫폼을 고르고 초록색 삼각형 실행 버튼을 누르는 행위는 내부적으로 Flutter 명령어를 실행하는 것과 같습니다.

# 안드로이드 실행, android 디렉터리 설정 참고
flutter run -d android

# Windows 데스크톱 실행, windows 내부 C++ 프로젝트 참고
flutter run -d windows

# 웹 실행, web 디렉터리 html 파일 참고
flutter run -d chromeCode language: PHP (php)

windows 폴더 안의 runner / main.cpp 파일을 열어보세요

#include <flutter/dart_project.h>
#include <flutter/flutter_view_controller.h>
#include <windows.h>

#include "flutter_window.h"
#include "utils.h"

int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev,
                      _In_ wchar_t *command_line, _In_ int show_command) {
  // Attach to console when present (e.g., 'flutter run') or create a
  // new console when running with a debugger.
  if (!::AttachConsole(ATTACH_PARENT_PROCESS) && ::IsDebuggerPresent()) {
    CreateAndAttachConsole();
  }

  // Initialize COM, so that it is available for use in the library and/or
  // plugins.
  ::CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED);

  flutter::DartProject project(L"data");

  std::vector<std::string> command_line_arguments =
      GetCommandLineArguments();

  project.set_dart_entrypoint_arguments(std::move(command_line_arguments));

  FlutterWindow window(project);
  Win32Window::Point origin(10, 10);
  Win32Window::Size size(1280, 720);
  if (!window.Create(L"myproject", origin, size)) {
    return EXIT_FAILURE;
  }
  window.SetQuitOnClose(true);

  ::MSG msg;
  while (::GetMessage(&msg, nullptr, 0, 0)) {
    ::TranslateMessage(&msg);
    ::DispatchMessage(&msg);
  }

  ::CoUninitialize();
  return EXIT_SUCCESS;
}
Code language: PHP (php)

Windows 데스크톱으로 빌드할 경우 이 main.cpp가 진짜 프로그램 진입점입니다. 먼저 이 C++ 파일이 실행된 뒤 lib/main.dart로 제어가 넘어갑니다.

마찬가지로 android 폴더를 열어보면 코틀린이나 자바로 개발하는 일반 안드로이드 앱과 거의 동일한 구조를 볼 수 있습니다.

src 폴더, AndroidManifest.xml 파일, MainActivity 등이 모두 존재해 안드로이드 개발 경험이 있다면 익숙할 겁니다. 사실 Flutter로 안드로이드 앱을 개발할 때 실행 흐름은 이 android 디렉터리부터 시작합니다.

마찬가지로 web 폴더를 열면 웹 진입점 파일인 index.html을 확인할 수 있습니다.

dart 파일 설명

lib 아래의 main.dart가 앞으로 개발의 핵심 파일입니다. lib 안에 하위 폴더를 만들고 다른 dart 파일을 추가할 수도 있습니다.

main.dart를 열어보겠습니다

// Material 컴포넌트 라이브러리 불러오기
import 'package:flutter/material.dart';

// 프로그램 진입 함수
void main() {
  runApp(const MyApp());
}

// 앱 루트 컴포넌트 (무상태 위젯)
class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      // 전역 테마 설정
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
      ),
      // 기본 메인 페이지
      home: const MyHomePage(title: 'Flutter Demo Home Page'),
    );
  }
}

// 메인 페이지 (상태 위젯)
class MyHomePage extends StatefulWidget {
  const MyHomePage({super.key, required this.title});

  // 페이지 제목 매개변수
  final String title;

  @override
  State<MyHomePage> createState() => _MyHomePageState();
}

// 메인 페이지 상태 관리 클래스
class _MyHomePageState extends State<MyHomePage> {
  // 카운터 변수
  int _counter = 0;

  // 카운터 증가 메서드
  void _incrementCounter() {
    setState(() {
      _counter++;
    });
  }

  @override
  Widget build(BuildContext context) {
    // 페이지 기본 레이아웃 틀
    return Scaffold(
      // 상단 앱바
      appBar: AppBar(
        backgroundColor: Theme.of(context).colorScheme.inversePrimary,
        title: Text(widget.title),
      ),
      // 페이지 본문 영역
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            const Text('You have pushed the button this many times:'),
            Text(
              '$_counter', // 숫자 변수 표시
              style: Theme.of(context).textTheme.headlineMedium, // 텍스트 스타일
            ),
          ],
        ),
      ),
      // 우측 하단 플로팅 버튼: onPressed 클릭 시 _incrementCounter 실행, tooltip 툴팁 텍스트, child 아이콘 지정
      floatingActionButton: FloatingActionButton(
        onPressed: _incrementCounter,
        tooltip: 'Increment',
        child: const Icon(Icons.add),
      ),
    );
  }
}Code language: JavaScript (javascript)

1. 진입 부분

import 'package:flutter/material.dart';
void main() {
  runApp(const MyApp());
}
Code language: JavaScript (javascript)
  • 구글 공식 Material UI 컴포넌트 묶음(버튼, 페이지, 텍스트 등)을 임포트합니다. 애플은 자체 UI 프레임워크를 따로 제공합니다.
  • main는 Dart 프로그램 유일한 진입점입니다. runApp()로 앱을 실행하고 루트 컴포넌트 MyApp을 연결합니다.

2. MyApp 전역 루트 위젯

StatelessWidget 즉 무상태 위젯입니다. 자세한 설명은 뒤에서 다루므로 지금은 간략히 알아두기만 하세요.

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  // 앱 최상단 컴포넌트
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
      ),
      home: const MyHomePage(title: 'Flutter Demo Home Page'),
    );
  }
}
Code language: JavaScript (javascript)
  • 전역 설정:앱 이름, 전체 테마, 기본 시작 페이지
  • MaterialApp:머티리얼 디자인 앱 외곽으로 라우팅, 테마, 제목을 통합 관리
  • home로 앱 실행 시 기본 페이지인 MyHomePage를 지정

3. MyHomePage 메인 페이지

창이나 앱을 열면 기본으로 보이는 페이지로 StatefulWidget 즉 상태 위젯입니다. 자세한 내용은 후술합니다.

class MyHomePage extends StatefulWidget {
  const MyHomePage({super.key, required this.title});
  
  final String title;

  @override
  State<MyHomePage> createState() => _MyHomePageState();
}Code language: JavaScript (javascript)
  • 카운터 숫자 같은 가변 데이터를 담고 있으므로 상태 위젯을 사용합니다.
  • 외부에서 제목 title을 전달받고 상태 클래스 안에서 widget.title로 읽어옵니다.
  • 반드시 _MyHomePageState 클래스를 짝으로 만들어 페이지 변수와 상호작용 로직을 보관합니다.

4. _MyHomePageState

상태 로직 클래스로 페이지 동작, 렌더링, 갱신 관련 처리를 담당합니다.

class _MyHomePageState extends State<MyHomePage> {
  int _counter = 0;

  void _incrementCounter() {
    setState(() {
      _counter++;
    });
  }

  @override
  Widget build(BuildContext context) {

    return Scaffold(
      appBar: AppBar(

        backgroundColor: Theme.of(context).colorScheme.inversePrimary,

        title: Text(widget.title),
      ),
      body: Center(

        child: Column(

          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            const Text('You have pushed the button this many times:'),
            Text(
              '$_counter',
              style: Theme.of(context).textTheme.headlineMedium,
            ),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _incrementCounter,
        tooltip: 'Increment',
        child: const Icon(Icons.add),
      ),
    );
  }
}Code language: JavaScript (javascript)
  1. int _counter = 0:페이지 가변 데이터인 카운터 값
  2. _incrementCounter():버튼 클릭 이벤트
    • setState():Flutter에 데이터 변경을 알리고 UI를 새로 갱신하도록 요청
    • 카운터를 1 증가
  3. build():화면 갱신할 때마다 실행되며 전체 페이지 레이아웃을 반환

5. Scaffold 페이지 틀

페이지 레이아웃 템플릿으로 _MyHomePageState 클래스 내부에 작성합니다.

  • appBar:상단 제목 바
  • body:페이지 본문 영역
    • Center:요소 가운데 정렬, Column:텍스트 수직 배치
  • floatingActionButton:우측 하단 원형 더하기 버튼, 클릭 시 증가 메서드 연결

전체 실행 흐름

  1. 프로그램 실행 → main 함수 실행 → MyApp 로드
  2. MyAppMaterialApp 렌더링 → 기본 페이지 MyHomePage 로드
  3. 화면에 제목바, 중앙 텍스트, 더하기 버튼이 표시됨
  4. 버튼 클릭 → _incrementCounter 실행 + setState로 화면 재렌더링 → 숫자 갱신

첫 Flutter 프로젝트 이해하기

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다