Flutter ํ…Œ์ŠคํŠธ ์™„๋ฒฝ ๊ฐ€์ด๋“œ 2026: ์œ„์ ฏ ํ…Œ์ŠคํŠธ, ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ ๋ฐ ๋ฉด์ ‘ ๋ฒ ์ŠคํŠธ ํ”„๋ž™ํ‹ฐ์Šค

Flutter์˜ ์œ„์ ฏ ํ…Œ์ŠคํŠธ, ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ, ๊ณจ๋“  ํ…Œ์ŠคํŠธ, ๋ชจํ‚น ์ „๋žต์„ ์‹ค์ „ ์ฝ”๋“œ์™€ ํ•จ๊ป˜ ํ•ด์„คํ•ฉ๋‹ˆ๋‹ค. 2026๋…„ ๊ธฐ์ˆ  ๋ฉด์ ‘์—์„œ ์ž์ฃผ ์ถœ์ œ๋˜๋Š” ํ…Œ์ŠคํŠธ ํŒจํ„ด๊ณผ ๋ชจ๋ฒ” ๋‹ต์•ˆ์„ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค.

Flutter ํ…Œ์ŠคํŠธ: ์œ„์ ฏ ํ…Œ์ŠคํŠธ, ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ ๋ฐ ๋ฉด์ ‘ ๋ฒ ์ŠคํŠธ ํ”„๋ž™ํ‹ฐ์Šค 2026

Flutter ํ…Œ์ŠคํŠธ๋Š” ํ”„๋กœํ† ํƒ€์ž…๊ณผ ํ”„๋กœ๋•์…˜ ์ˆ˜์ค€ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์„ ๊ตฌ๋ถ„ํ•˜๋Š” ํ•ต์‹ฌ ์š”์†Œ์ž…๋‹ˆ๋‹ค. ํ”„๋ ˆ์ž„์›Œํฌ์—๋Š” ์œ ๋‹› ํ…Œ์ŠคํŠธ, ์œ„์ ฏ ํ…Œ์ŠคํŠธ, ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ๋ผ๋Š” ์„ธ ๊ฐ€์ง€ ํ…Œ์ŠคํŠธ ์œ ํ˜•์ด ๋‚ด์žฅ๋˜์–ด ์žˆ์œผ๋ฉฐ, ๊ฐ๊ฐ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์˜ ์„œ๋กœ ๋‹ค๋ฅธ ๋ ˆ์ด์–ด๋ฅผ ๊ฒ€์ฆํ•ฉ๋‹ˆ๋‹ค. ์ด ๊ฐ€์ด๋“œ์—์„œ๋Š” ์œ„์ ฏ ํ…Œ์ŠคํŠธ, ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ, ๊ณจ๋“  ํ…Œ์ŠคํŠธ, ๋ชจํ‚น ์ „๋žต์— ๋Œ€ํ•ด ์ผ์ƒ์ ์ธ ๊ฐœ๋ฐœ๊ณผ 2026๋…„ ๊ธฐ์ˆ  ๋ฉด์ ‘ ๋ชจ๋‘์— ์ง์ ‘ ์ ์šฉํ•  ์ˆ˜ ์žˆ๋Š” ์‹ค์šฉ์ ์ธ ํŒจํ„ด์„ ๋‹ค๋ฃน๋‹ˆ๋‹ค.

Flutter ํ…Œ์ŠคํŠธ ํ”ผ๋ผ๋ฏธ๋“œ

์˜ฌ๋ฐ”๋ฅด๊ฒŒ ๊ตฌ์„ฑ๋œ Flutter ํ…Œ์ŠคํŠธ ์Šค์œ„ํŠธ๋Š” ํ…Œ์ŠคํŠธ ํ”ผ๋ผ๋ฏธ๋“œ๋ฅผ ๋”ฐ๋ฆ…๋‹ˆ๋‹ค. ๊ธฐ๋ฐ˜์—๋Š” ๋น ๋ฅธ ์œ ๋‹› ํ…Œ์ŠคํŠธ๋ฅผ ๋‹ค์ˆ˜ ๋ฐฐ์น˜ํ•˜๊ณ , ์ค‘๊ฐ„ ๊ณ„์ธต์—๋Š” ๊ฒฌ๊ณ ํ•œ ์œ„์ ฏ ํ…Œ์ŠคํŠธ๋ฅผ ์Œ“์œผ๋ฉฐ, ์ƒ๋‹จ์—๋Š” ํ•ต์‹ฌ ํ”Œ๋กœ์šฐ์— ์ง‘์ค‘ํ•˜๋Š” ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ๋ฅผ ๋ฐฐ์น˜ํ•ฉ๋‹ˆ๋‹ค. ์œ„์ ฏ ํ…Œ์ŠคํŠธ๋Š” ๋””๋ฐ”์ด์Šค ์—†์ด 1์ดˆ ์ด๋‚ด์— ์‹คํ–‰๋˜๋ฏ€๋กœ ๋Œ€๋ถ€๋ถ„์˜ Flutter ์•ฑ์—์„œ ๊ฐ€์žฅ ๋†’์€ ํˆฌ์ž ๋Œ€๋น„ ํšจ๊ณผ๋ฅผ ์ œ๊ณตํ•˜๋Š” ํ…Œ์ŠคํŠธ ๊ณ„์ธต์ž…๋‹ˆ๋‹ค.

Flutter ์œ„์ ฏ ํ…Œ์ŠคํŠธ์˜ ๊ธฐ์ดˆ

์œ„์ ฏ ํ…Œ์ŠคํŠธ๋Š” ์—๋ฎฌ๋ ˆ์ดํ„ฐ๋‚˜ ์‹ค์ œ ๋””๋ฐ”์ด์Šค ์—†์ด UI ์ปดํฌ๋„ŒํŠธ๋ฅผ ๋…๋ฆฝ์ ์œผ๋กœ ๊ฒ€์ฆํ•ฉ๋‹ˆ๋‹ค. Flutter๋Š” ํ—ค๋“œ๋ฆฌ์Šค ํ…Œ์ŠคํŠธ ํ™˜๊ฒฝ์—์„œ ์œ„์ ฏ ํŠธ๋ฆฌ๋ฅผ ์ „๊ฐœํ•˜์—ฌ ๋ ˆ์ด์•„์›ƒ ๋ฌธ์ œ, ํƒญ ํ•ธ๋“ค๋Ÿฌ, ์ƒํƒœ ์ „ํ™˜์„ ํ”„๋กœ๋•์…˜์— ๋„๋‹ฌํ•˜๊ธฐ ์ „์— ๋น ๋ฅธ ํ”ผ๋“œ๋ฐฑ ๋ฃจํ”„๋กœ ๊ฐ์ง€ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

flutter_test ํŒจํ‚ค์ง€๋Š” testWidgets(), ์ธํ„ฐ๋ž™์…˜์„ ์‹œ๋ฎฌ๋ ˆ์ด์…˜ํ•˜๋Š” WidgetTester, ํŠธ๋ฆฌ์—์„œ ์œ„์ ฏ์„ ๊ฒ€์ƒ‰ํ•˜๋Š” Finder ํด๋ž˜์Šค๋ฅผ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค.

counter_widget_test.dartdart
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:my_app/counter_widget.dart';

void main() {
  testWidgets('Counter increments on tap', (WidgetTester tester) async {
    // Inflate the widget tree
    await tester.pumpWidget(
      const MaterialApp(home: CounterWidget()),
    );

    // Verify initial state
    expect(find.text('0'), findsOneWidget);
    expect(find.text('1'), findsNothing);

    // Simulate user tap on the increment button
    await tester.tap(find.byIcon(Icons.add));
    await tester.pump(); // Rebuild after state change

    // Assert updated state
    expect(find.text('0'), findsNothing);
    expect(find.text('1'), findsOneWidget);
  });
}

pump()์€ ๋‹จ์ผ ํ”„๋ ˆ์ž„ ๋ฆฌ๋นŒ๋“œ๋ฅผ ํŠธ๋ฆฌ๊ฑฐํ•ฉ๋‹ˆ๋‹ค. ์• ๋‹ˆ๋ฉ”์ด์…˜์ด๋‚˜ ์ง€์—ฐ๋œ ์ž‘์—…์˜ ๊ฒฝ์šฐ pumpAndSettle()์ด ๋ชจ๋“  ํ”„๋ ˆ์ž„์ด ์™„๋ฃŒ๋  ๋•Œ๊นŒ์ง€ ๋Œ€๊ธฐํ•ฉ๋‹ˆ๋‹ค. ๊ฐ๊ฐ์˜ ์‚ฌ์šฉ ์‹œ์ ์„ ๊ตฌ๋ถ„ํ•˜๋Š” ๊ฒƒ์€ ๋ฉด์ ‘์—์„œ ์ž์ฃผ ๋‚˜์˜ค๋Š” ์งˆ๋ฌธ์ด๋ฉฐ, pump()์€ ์ •๋ฐ€ํ•œ ์ œ์–ด๋ฅผ ์ œ๊ณตํ•˜๊ณ  pumpAndSettle()์€ ํŽธ๋ฆฌํ•˜์ง€๋งŒ ๋ฌดํ•œ ์• ๋‹ˆ๋ฉ”์ด์…˜์—์„œ ํ–‰์ด ๊ฑธ๋ฆด ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๋น„๋™๊ธฐ ์ž‘์—…๊ณผ Stream ๊ธฐ๋ฐ˜ ์œ„์ ฏ ํ…Œ์ŠคํŠธ

์‹ค์ œ ์œ„์ ฏ์€ ๋„คํŠธ์›Œํฌ ํ˜ธ์ถœ, ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ๋˜๋Š” ์ŠคํŠธ๋ฆผ์— ์˜์กดํ•ฉ๋‹ˆ๋‹ค. ์ด๋Ÿฌํ•œ ํ…Œ์ŠคํŠธ์—์„œ๋Š” ๋น„๋™๊ธฐ ๋™์ž‘์„ ๋ช…์‹œ์ ์œผ๋กœ ์ œ์–ดํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. WidgetTester๋Š” Dart์˜ ์ด๋ฒคํŠธ ๋ฃจํ”„์™€ ํ†ตํ•ฉ๋˜์ง€๋งŒ, ์™ธ๋ถ€ ์˜์กด์„ฑ์€ ๋ถˆ์•ˆ์ •ํ•œ ํ…Œ์ŠคํŠธ๋ฅผ ๋ฐฉ์ง€ํ•˜๊ธฐ ์œ„ํ•ด ๋ชจํ‚น์ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค.

user_profile_widget_test.dartdart
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart';
import 'package:my_app/user_profile_widget.dart';
import 'package:my_app/user_repository.dart';

class MockUserRepository extends Mock implements UserRepository {}

void main() {
  late MockUserRepository mockRepo;

  setUp(() {
    mockRepo = MockUserRepository();
  });

  testWidgets('displays user name after loading', (tester) async {
    // Arrange: mock returns a user after a delay
    when(() => mockRepo.fetchUser(1)).thenAnswer(
      (_) async => User(id: 1, name: 'Alice'),
    );

    await tester.pumpWidget(
      MaterialApp(
        home: UserProfileWidget(repository: mockRepo),
      ),
    );

    // Assert: loading indicator visible initially
    expect(find.byType(CircularProgressIndicator), findsOneWidget);

    // Wait for the Future to complete and rebuild
    await tester.pumpAndSettle();

    // Assert: user name displayed, loader gone
    expect(find.text('Alice'), findsOneWidget);
    expect(find.byType(CircularProgressIndicator), findsNothing);

    // Verify the repository was called exactly once
    verify(() => mockRepo.fetchUser(1)).called(1);
  });
}

Mocktail์€ 2026๋…„ Flutter ์ƒํƒœ๊ณ„์—์„œ ๊ถŒ์žฅ๋˜๋Š” ๋ชจํ‚น ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์ž…๋‹ˆ๋‹ค. Mockito์™€ ๋‹ฌ๋ฆฌ ์ฝ”๋“œ ์ƒ์„ฑ ๋‹จ๊ณ„๊ฐ€ ํ•„์š” ์—†์–ด ํ…Œ์ŠคํŠธ ์„ค์ •์ด ๋น ๋ฆ…๋‹ˆ๋‹ค. when() / verify() API๋Š” ์ž์—ฐ์Šค๋Ÿฝ๊ฒŒ ์ฝํžˆ๋ฉฐ ๋Œ€๋ถ€๋ถ„์˜ ์˜์กด์„ฑ ์ฃผ์ž… ์‹œ๋‚˜๋ฆฌ์˜ค๋ฅผ ๋‹ค๋ฃน๋‹ˆ๋‹ค.

Mocktail์„ ํ™œ์šฉํ•œ ๋ฉด์ ‘ ๋Œ€๋น„ ๋ชจํ‚น ์ „๋žต

๋ฉด์ ‘์—์„œ๋Š” Flutter์˜ ์˜์กด์„ฑ ์ฃผ์ž…๊ณผ ํ…Œ์ŠคํŠธ ๊ฐ€๋Šฅ์„ฑ์— ๋Œ€ํ•œ ์งˆ๋ฌธ์ด ์ž์ฃผ ์ถœ์ œ๋ฉ๋‹ˆ๋‹ค. ์ผ๊ด€๋˜๊ฒŒ ๋†’์€ ํ‰๊ฐ€๋ฅผ ๋ฐ›๋Š” ํŒจํ„ด์€ ์ƒ์„ฑ์ž ์ฃผ์ž…๊ณผ ์ถ”์ƒ ํด๋ž˜์Šค(๋˜๋Š” Dart์˜ ์•”์‹œ์  ์ธํ„ฐํŽ˜์ด์Šค๋ฅผ ํ™œ์šฉํ•œ ์ธํ„ฐํŽ˜์ด์Šค)์˜ ์กฐํ•ฉ์ž…๋‹ˆ๋‹ค.

weather_service.dartdart
abstract class WeatherService {
  Future<Weather> getWeather(String city);
}

// real implementation
class OpenMeteoWeatherService implements WeatherService {
  final http.Client _client;

  OpenMeteoWeatherService(this._client);

  
  Future<Weather> getWeather(String city) async {
    final response = await _client.get(
      Uri.parse('https://api.open-meteo.com/v1/forecast?city=$city'),
    );
    return Weather.fromJson(jsonDecode(response.body));
  }
}
weather_service_test.dartdart
class MockHttpClient extends Mock implements http.Client {}

void main() {
  late MockHttpClient mockClient;
  late OpenMeteoWeatherService service;

  setUp(() {
    mockClient = MockHttpClient();
    service = OpenMeteoWeatherService(mockClient);
  });

  test('parses weather JSON correctly', () async {
    // Arrange: return a known JSON payload
    when(() => mockClient.get(any())).thenAnswer(
      (_) async => http.Response(
        '{"temperature": 22.5, "condition": "sunny"}',
        200,
      ),
    );

    // Act
    final weather = await service.getWeather('Paris');

    // Assert
    expect(weather.temperature, 22.5);
    expect(weather.condition, 'sunny');
  });
}

์ด ํŒจํ„ด โ€• ์™ธ๋ถ€ ์˜์กด์„ฑ์„ ์ธํ„ฐํŽ˜์ด์Šค๋กœ ์ถ”์ƒํ™”ํ•˜๊ณ  ๋ชจํ‚น์„ ์ฃผ์ž…ํ•˜๋Š” ๋ฐฉ์‹ โ€• ์€ ๋ฆฌํฌ์ง€ํ† ๋ฆฌ, ๋ถ„์„ ์„œ๋น„์Šค, ํ”Œ๋žซํผ ์ฑ„๋„์— ๋™์ผํ•˜๊ฒŒ ์ ์šฉ๋ฉ๋‹ˆ๋‹ค. ๋ฉด์ ‘๊ด€์€ ์ด๋Ÿฌํ•œ ๋ถ„๋ฆฌ๋ฅผ ํ™•์ธํ•จ์œผ๋กœ์จ SOLID ์›์น™๊ณผ ํ”„๋กœ๋•์…˜ ์ˆ˜์ค€ ์•„ํ‚คํ…์ฒ˜์— ๋Œ€ํ•œ ์ดํ•ด๋„๋ฅผ ํ‰๊ฐ€ํ•ฉ๋‹ˆ๋‹ค.

integration_test ํŒจํ‚ค์ง€๋ฅผ ํ™œ์šฉํ•œ ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ

ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ๋Š” ์—ฌ๋Ÿฌ ์ปดํฌ๋„ŒํŠธ๊ฐ€ ์‹ค์ œ ๋””๋ฐ”์ด์Šค ๋˜๋Š” ์—๋ฎฌ๋ ˆ์ดํ„ฐ์—์„œ ํ•จ๊ป˜ ๋™์ž‘ํ•˜๋Š”์ง€ ๊ฒ€์ฆํ•ฉ๋‹ˆ๋‹ค. ๋‚ด์žฅ๋œ integration_test ํŒจํ‚ค์ง€๋Š” flutter_test์˜ API์™€ ๋””๋ฐ”์ด์Šค ์ˆ˜์ค€ ์‹คํ–‰์„ ์—ฐ๊ฒฐํ•ฉ๋‹ˆ๋‹ค.

integration_test/login_flow_test.dartdart
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import 'package:my_app/main.dart' as app;

void main() {
  IntegrationTestWidgetsFlutterBinding.ensureInitialized();

  testWidgets('complete login flow', (tester) async {
    // Launch the full app
    app.main();
    await tester.pumpAndSettle();

    // Navigate to login screen
    await tester.tap(find.text('Sign In'));
    await tester.pumpAndSettle();

    // Fill in credentials
    await tester.enterText(
      find.byKey(const Key('email_field')),
      'test@example.com',
    );
    await tester.enterText(
      find.byKey(const Key('password_field')),
      'securePassword123',
    );

    // Submit the form
    await tester.tap(find.byKey(const Key('login_button')));
    await tester.pumpAndSettle();

    // Verify navigation to dashboard
    expect(find.text('Dashboard'), findsOneWidget);
  });
}

ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ๋Š” flutter test integration_test/๋กœ ์‹คํ–‰ํ•ฉ๋‹ˆ๋‹ค. ๋””๋ฐ”์ด์Šค์—์„œ ์‹คํ–‰๋˜๋ฏ€๋กœ ์œ„์ ฏ ํ…Œ์ŠคํŠธ๋ณด๋‹ค ์‹œ๊ฐ„์ด ์˜ค๋ž˜ ๊ฑธ๋ฆฝ๋‹ˆ๋‹ค. ๊ถŒ์žฅ ์ ‘๊ทผ ๋ฐฉ์‹์€ ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ๋ฅผ ์ค‘์š”ํ•œ ์‚ฌ์šฉ์ž ํ”Œ๋กœ์šฐ(๋กœ๊ทธ์ธ, ๊ฒฐ์ œ, ์˜จ๋ณด๋”ฉ)์— ํ•œ์ •ํ•˜๊ณ , ๊ฐœ๋ณ„ ํ™”๋ฉด ๋™์ž‘์€ ์œ„์ ฏ ํ…Œ์ŠคํŠธ์— ์˜์กดํ•˜๋Š” ๊ฒƒ์ž…๋‹ˆ๋‹ค.

๋„ค์ดํ‹ฐ๋ธŒ ํ”Œ๋žซํผ UI(๊ถŒํ•œ ๋‹ค์ด์–ผ๋กœ๊ทธ, ํ‘ธ์‹œ ์•Œ๋ฆผ, ์ƒ์ฒด ์ธ์ฆ ํ”„๋กฌํ”„ํŠธ)์™€ ์ƒํ˜ธ์ž‘์šฉํ•˜๋Š” ์•ฑ์˜ ๊ฒฝ์šฐ Patrol ํŒจํ‚ค์ง€๊ฐ€ Flutter์˜ ํ…Œ์ŠคํŠธ ๊ธฐ๋Šฅ์„ ๋„ค์ดํ‹ฐ๋ธŒ ์ž๋™ํ™” ์ง€์›์œผ๋กœ ํ™•์žฅํ•ฉ๋‹ˆ๋‹ค.

Flutter ๋ฉด์ ‘ ์ค€๋น„๊ฐ€ ๋˜์…จ๋‚˜์š”?

์ธํ„ฐ๋ž™ํ‹ฐ๋ธŒ ์‹œ๋ฎฌ๋ ˆ์ดํ„ฐ, flashcards, ๊ธฐ์ˆ  ํ…Œ์ŠคํŠธ๋กœ ์—ฐ์Šตํ•˜์„ธ์š”.

๊ณจ๋“  ํ…Œ์ŠคํŠธ๋ฅผ ํ™œ์šฉํ•œ ์‹œ๊ฐ์  ๋ฆฌ๊ทธ๋ ˆ์…˜ ๊ฐ์ง€

๊ณจ๋“  ํ…Œ์ŠคํŠธ๋Š” ๋ Œ๋”๋ง๋œ ์œ„์ ฏ์˜ ์Šคํฌ๋ฆฐ์ƒท์„ ์บก์ฒ˜ํ•˜์—ฌ ๊ธฐ์ค€ ์ด๋ฏธ์ง€์™€ ๋น„๊ตํ•ฉ๋‹ˆ๋‹ค. ํ”ฝ์…€ ์ˆ˜์ค€์˜ ์ฐจ์ด๊ฐ€ ๋ฐœ๊ฒฌ๋˜๋ฉด ํ…Œ์ŠคํŠธ๊ฐ€ ์‹คํŒจํ•˜์—ฌ ์˜๋„ํ•˜์ง€ ์•Š์€ ์‹œ๊ฐ์  ๋ฆฌ๊ทธ๋ ˆ์…˜์„ ์ถœ์‹œ ์ „์— ๊ฐ์ง€ํ•ฉ๋‹ˆ๋‹ค.

Flutter 3.29+์—๋Š” ๋‚ด์žฅ ๊ณจ๋“  ํ…Œ์ŠคํŠธ ์ง€์›์ด ํฌํ•จ๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค. ๋‹ค์–‘ํ•œ ํ™”๋ฉด ํฌ๊ธฐ, ํ…Œ๋งˆ, ๋กœ์ผ€์ผ ๋“ฑ ๊ณ ๊ธ‰ ์‹œ๋‚˜๋ฆฌ์˜ค์˜ ๊ฒฝ์šฐ Alchemist๊ฐ€ ์ค‘๋‹จ๋œ golden_toolkit์„ ๋Œ€์ฒดํ•˜๋Š” ๊ตฌ์กฐํ™”๋œ API๋ฅผ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค.

button_golden_test.dartdart
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:my_app/primary_button.dart';

void main() {
  testWidgets('PrimaryButton matches golden file', (tester) async {
    await tester.pumpWidget(
      MaterialApp(
        home: Scaffold(
          body: Center(
            child: PrimaryButton(
              label: 'Submit',
              onPressed: () {},
            ),
          ),
        ),
      ),
    );

    // Compare against baseline image
    await expectLater(
      find.byType(PrimaryButton),
      matchesGoldenFile('goldens/primary_button.png'),
    );
  });
}

๊ธฐ์ค€ ์ด๋ฏธ์ง€๋Š” flutter test --update-goldens๋กœ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. ๊ณจ๋“  ํŒŒ์ผ์€ ๋ฒ„์ „ ๊ด€๋ฆฌ์— ์ €์žฅํ•ฉ๋‹ˆ๋‹ค. CI ํŒŒ์ดํ”„๋ผ์ธ์—์„œ๋Š” ํฌ๋กœ์Šค ํ”Œ๋žซํผ ํ”ฝ์…€ ์ฐจ์ด๋ฅผ ๋ฐฉ์ง€ํ•˜๊ธฐ ์œ„ํ•ด ์ผ๊ด€๋œ ํ”Œ๋žซํผ(Linux ์ปจํ…Œ์ด๋„ˆ๊ฐ€ ํ‘œ์ค€)์—์„œ ๊ณจ๋“  ํ…Œ์ŠคํŠธ๋ฅผ ์‹คํ–‰ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

ํ™•์žฅ ๊ฐ€๋Šฅํ•œ Flutter ํ…Œ์ŠคํŠธ ์Šค์œ„ํŠธ ๊ตฌ์„ฑ

์ฝ”๋“œ๋ฒ ์ด์Šค๊ฐ€ ์ปค์งˆ์ˆ˜๋ก ํ…Œ์ŠคํŠธ ๊ตฌ์„ฑ์ด ์ค‘์š”ํ•ด์ง‘๋‹ˆ๋‹ค. ํ™•์žฅ ๊ฐ€๋Šฅํ•œ ํŒจํ„ด์€ lib/ ๋””๋ ‰ํ† ๋ฆฌ ๊ตฌ์กฐ๋ฅผ test/ ๋‚ด๋ถ€์— ๋ฏธ๋Ÿฌ๋งํ•˜๊ณ  ์œ ๋‹›, ์œ„์ ฏ, ํ†ตํ•ฉ ๋ ˆ์ด์–ด๋ฅผ ๋ช…ํ™•ํžˆ ๋ถ„๋ฆฌํ•˜๋Š” ๊ฒƒ์ž…๋‹ˆ๋‹ค.

text
test/
  unit/
    services/
      weather_service_test.dart
    models/
      user_test.dart
  widget/
    screens/
      login_screen_test.dart
      dashboard_test.dart
    components/
      primary_button_test.dart
  goldens/
    primary_button.png
integration_test/
  login_flow_test.dart
  checkout_flow_test.dart

๊ณตํ†ต ์„ค์ • ์ฝ”๋“œ(๋ชจํ‚น ์ธ์Šคํ„ด์Šค, ํ…Œ์ŠคํŠธ ๋ฐ์ดํ„ฐ ํŒฉํ† ๋ฆฌ, ์ปค์Šคํ…€ ๋งค์ฒ˜)๋Š” test/helpers/ ๋””๋ ‰ํ† ๋ฆฌ์— ๋ฐฐ์น˜ํ•ฉ๋‹ˆ๋‹ค. ํ…Œ์ŠคํŠธ ํŒŒ์ผ ๊ฐ„์— ์ด๋ฅผ ๊ณต์œ ํ•˜๋ฉด ๋ฐ€๊ฒฐํ•ฉ ์—†์ด ์ค‘๋ณต์„ ์ค„์ผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

Flutter์˜ ์ปค๋ฒ„๋ฆฌ์ง€ ๋„๊ตฌ๋กœ ์ถ”์ ํ•ด์•ผ ํ•  ์ฃผ์š” ์ง€ํ‘œ๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™์Šต๋‹ˆ๋‹ค:

  • ๋ผ์ธ ์ปค๋ฒ„๋ฆฌ์ง€: ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง 80% ์ด์ƒ, UI ์ฝ”๋“œ 60% ์ด์ƒ ๋ชฉํ‘œ
  • ๋ถ„๊ธฐ ์ปค๋ฒ„๋ฆฌ์ง€: ์กฐ๊ฑด๋ถ€ ๊ฒฝ๋กœ๊ฐ€ ํ…Œ์ŠคํŠธ๋˜์—ˆ๋Š”์ง€ ํ™•์ธ
  • ํ…Œ์ŠคํŠธ ์‹คํ–‰ ์‹œ๊ฐ„: ์œ ๋‹› + ์œ„์ ฏ ํ…Œ์ŠคํŠธ 5๋ถ„ ์ด๋‚ด, ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ 30๋ถ„ ์ด๋‚ด

flutter test --coverage๋ฅผ ์‹คํ–‰ํ•˜๋ฉด lcov.info ํŒŒ์ผ์ด ์ƒ์„ฑ๋ฉ๋‹ˆ๋‹ค. CI ํŒŒ์ดํ”„๋ผ์ธ์—์„œ lcov ๋˜๋Š” Codecov๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ์ปค๋ฒ„๋ฆฌ์ง€ ์ถ”์ด๋ฅผ ์‹œ๊ฐํ™”ํ•˜๊ณ  ์ตœ์†Œ ์ž„๊ณ„๊ฐ’์„ ์ ์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

Flutter ๋ฉด์ ‘์—์„œ ์ž์ฃผ ๋‚˜์˜ค๋Š” ํ…Œ์ŠคํŠธ ์งˆ๋ฌธ

2026๋…„ ๊ธฐ์ˆ  ๋ฉด์ ‘์—์„œ๋Š” Flutter ํ…Œ์ŠคํŠธ ์‹œ๋‚˜๋ฆฌ์˜ค๊ฐ€ ์ •๊ธฐ์ ์œผ๋กœ ์ถœ์ œ๋ฉ๋‹ˆ๋‹ค. ๋‹ค์Œ์€ Flutter ๊ฐœ๋ฐœ์ž๋“ค์˜ ์‹ค์ œ ๋ฉด์ ‘ ํ”ผ๋“œ๋ฐฑ์„ ๊ธฐ๋ฐ˜์œผ๋กœ ๊ฐ€์žฅ ์ž์ฃผ ๋‹ค๋ค„์ง€๋Š” ํŒจํ„ด์ž…๋‹ˆ๋‹ค.

"pump()๊ณผ pumpAndSettle()์˜ ์ฐจ์ด์ ์€ ๋ฌด์—‡์ž…๋‹ˆ๊นŒ?"

pump()์€ ์ •ํ™•ํžˆ ํ•œ ํ”„๋ ˆ์ž„์˜ ๋ฆฌ๋นŒ๋“œ๋ฅผ ํŠธ๋ฆฌ๊ฑฐํ•ฉ๋‹ˆ๋‹ค. pumpAndSettle()์€ ๋” ์ด์ƒ ์Šค์ผ€์ค„๋œ ํ”„๋ ˆ์ž„์ด ์—†์„ ๋•Œ๊นŒ์ง€ pump()์„ ๋ฐ˜๋ณต์ ์œผ๋กœ ํ˜ธ์ถœํ•ฉ๋‹ˆ๋‹ค โ€• ์• ๋‹ˆ๋ฉ”์ด์…˜๊ณผ ๋ผ์šฐํŠธ ์ „ํ™˜์— ์œ ์šฉํ•ฉ๋‹ˆ๋‹ค. ์ฃผ์˜ํ•  ์ ์€ pumpAndSettle()์ด ๋ฌดํ•œ ์• ๋‹ˆ๋ฉ”์ด์…˜(๋กœ๋”ฉ ์Šคํ”ผ๋„ˆ, ์‹œ๋จธ ํšจ๊ณผ)์ด ์žˆ๋Š” ์œ„์ ฏ์—์„œ ํƒ€์ž„์•„์›ƒ๋œ๋‹ค๋Š” ๊ฒƒ์ž…๋‹ˆ๋‹ค. ๊ทธ๋Ÿฌํ•œ ๊ฒฝ์šฐ์—๋Š” pump(Duration)์„ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค.

"์œ„์ ฏ ํ…Œ์ŠคํŠธ์™€ ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ์˜ ์ฐจ์ด์ ์€?"

์œ„์ ฏ ํ…Œ์ŠคํŠธ๋Š” ๋””๋ฐ”์ด์Šค ์—†์ด ํ—ค๋“œ๋ฆฌ์Šค ํ™˜๊ฒฝ์—์„œ ์‹คํ–‰๋˜๋ฉฐ ์ปดํฌ๋„ŒํŠธ๋ฅผ ๋…๋ฆฝ์ ์œผ๋กœ ํ…Œ์ŠคํŠธํ•ฉ๋‹ˆ๋‹ค. ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ๋Š” ์‹ค์ œ ๋””๋ฐ”์ด์Šค ๋˜๋Š” ์—๋ฎฌ๋ ˆ์ดํ„ฐ์—์„œ ์‹คํ–‰๋˜๋ฉฐ ์ „์ฒด ์•ฑ ๋˜๋Š” ํฐ ๋‹จ์œ„๋ฅผ ํ…Œ์ŠคํŠธํ•ฉ๋‹ˆ๋‹ค. ์œ„์ ฏ ํ…Œ์ŠคํŠธ๋Š” ๋น ๋ฅด๊ณ (๋ฐ€๋ฆฌ์ดˆ ๋‹จ์œ„) ๋งค ์ปค๋ฐ‹๋งˆ๋‹ค CI์—์„œ ์‹คํ–‰๋ฉ๋‹ˆ๋‹ค. ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ๋Š” ๋А๋ฆฌ๋ฉฐ(๋ถ„ ๋‹จ์œ„) ์ผ๋ฐ˜์ ์œผ๋กœ ๋จธ์ง€ ์‹œ ๋˜๋Š” ์•ผ๊ฐ„ ๋นŒ๋“œ์—์„œ ์‹คํ–‰๋ฉ๋‹ˆ๋‹ค.

"Provider๋‚˜ Riverpod์— ์˜์กดํ•˜๋Š” ์œ„์ ฏ์€ ์–ด๋–ป๊ฒŒ ํ…Œ์ŠคํŠธํ•ฉ๋‹ˆ๊นŒ?"

ํ…Œ์ŠคํŠธ ๋Œ€์ƒ ์œ„์ ฏ์„ ์ ์ ˆํ•œ ํ”„๋กœ๋ฐ”์ด๋” ์Šค์ฝ”ํ”„๋กœ ๊ฐ์‹ธ๊ณ  ํ…Œ์ŠคํŠธ ์ „์šฉ ์˜ค๋ฒ„๋ผ์ด๋“œ๋ฅผ ์ฃผ์ž…ํ•ฉ๋‹ˆ๋‹ค:

dart
// testing a Riverpod-dependent widget
import 'package:flutter_riverpod/flutter_riverpod.dart';

testWidgets('CartWidget shows item count', (tester) async {
  await tester.pumpWidget(
    ProviderScope(
      overrides: [
        // Override the cart provider with test data
        cartProvider.overrideWith(
          (ref) => CartNotifier(initialItems: [
            CartItem(name: 'Widget Book', price: 29.99),
          ]),
        ),
      ],
      child: const MaterialApp(home: CartWidget()),
    ),
  );

  expect(find.text('1 item'), findsOneWidget);
  expect(find.text('\$29.99'), findsOneWidget);
});

์ด ์ ‘๊ทผ ๋ฐฉ์‹์€ ๊ธ€๋กœ๋ฒŒ ์ƒํƒœ์— ๋Œ€ํ•œ ์˜์กด์„ ํ”ผํ•˜๊ณ  ๊ฐ ํ…Œ์ŠคํŠธ๋ฅผ ๊ฒฐ์ •๋ก ์ ์œผ๋กœ ์œ ์ง€ํ•ฉ๋‹ˆ๋‹ค. ์ถ”์ƒ์ ์ธ ์„ค๋ช…์ด ์•„๋‹Œ ๊ตฌ์ฒด์ ์ธ ์ฝ”๋“œ ์˜ˆ์ œ๋ฅผ ํฌํ•จํ•œ ๋‹ต๋ณ€์„ ์ค€๋น„ํ•˜๋Š” ๊ฒƒ์ด Flutter ๋ฉด์ ‘์—์„œ ์ผ๊ด€๋˜๊ฒŒ ์ข‹์€ ๊ฒฐ๊ณผ๋ฅผ ๊ฐ€์ ธ์˜ต๋‹ˆ๋‹ค. Flutter ๋ฉด์ ‘ ์ค€๋น„๋ฅผ ๋” ์ง„ํ–‰ํ•˜๋ ค๋ฉด SharpSkill์˜ ์œ„์ ฏ ํ…Œ์ŠคํŠธ ๋ชจ๋“ˆ๊ณผ ์œ ๋‹› ํ…Œ์ŠคํŠธ ๋ชจ๋“ˆ์„ ์ฐธ๊ณ ํ•˜์‹œ๊ธฐ ๋ฐ”๋ž๋‹ˆ๋‹ค.

์—ฐ์Šต์„ ์‹œ์ž‘ํ•˜์„ธ์š”!

๋ฉด์ ‘ ์‹œ๋ฎฌ๋ ˆ์ดํ„ฐ์™€ ๊ธฐ์ˆ  ํ…Œ์ŠคํŠธ๋กœ ์ง€์‹์„ ํ…Œ์ŠคํŠธํ•˜์„ธ์š”.

๊ฒฐ๋ก 

  • ์œ„์ ฏ ํ…Œ์ŠคํŠธ๋Š” Flutter ์•ฑ์—์„œ ๊ฐ€์žฅ ๋†’์€ ROI๋ฅผ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค โ€• testWidgets()์™€ WidgetTester๋ฅผ ํ†ตํ•œ ๋น ๋ฅธ ์‹คํ–‰, ๋””๋ฐ”์ด์Šค ๋ถˆํ•„์š”, ์ง์ ‘์ ์ธ UI ๊ฒ€์ฆ
  • ์˜์กด์„ฑ ๋ชจํ‚น์—๋Š” Mocktail์„ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค: ์ฝ”๋“œ ์ƒ์„ฑ ๋ถˆํ•„์š”, ๊น”๋”ํ•œ when()/verify() API, Dart์˜ ์•”์‹œ์  ์ธํ„ฐํŽ˜์ด์Šค ์‹œ์Šคํ…œ๊ณผ ์™„๋ฒฝํ•œ ํ˜ธํ™˜์„ฑ
  • ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ๋Š” ํ•ต์‹ฌ ์‚ฌ์šฉ์ž ํ”Œ๋กœ์šฐ(๋กœ๊ทธ์ธ, ๊ฒฐ์ œ, ๊ฒฐ์ œ ์ฒ˜๋ฆฌ)์—๋งŒ ํ•œ์ •ํ•ฉ๋‹ˆ๋‹ค โ€• ์ดˆ์ ์„ ์œ ์ง€ํ•˜๊ณ  ๋จธ์ง€ ์‹œ ๋˜๋Š” ์•ผ๊ฐ„ CI ํŒŒ์ดํ”„๋ผ์ธ์—์„œ ์‹คํ–‰ํ•ฉ๋‹ˆ๋‹ค
  • ๊ณจ๋“  ํ…Œ์ŠคํŠธ๋Š” ์‹œ๊ฐ์  ๋ฆฌ๊ทธ๋ ˆ์…˜์„ ์ž๋™์œผ๋กœ ๊ฐ์ง€ํ•ฉ๋‹ˆ๋‹ค. ๊ธฐ์ค€ ์ด๋ฏธ์ง€๋ฅผ ๋ฒ„์ „ ๊ด€๋ฆฌ์— ์ €์žฅํ•˜๊ณ  ํฌ๋กœ์Šค OS ํ”ฝ์…€ ์ฐจ์ด๋ฅผ ๋ฐฉ์ง€ํ•˜๊ธฐ ์œ„ํ•ด ์ผ๊ด€๋œ CI ํ”Œ๋žซํผ์—์„œ ์‹คํ–‰ํ•ฉ๋‹ˆ๋‹ค
  • lib/ ๊ตฌ์กฐ๋ฅผ test/ ๋””๋ ‰ํ† ๋ฆฌ์— ๋ฏธ๋Ÿฌ๋งํ•˜๊ณ , flutter test --coverage๋กœ ์ปค๋ฒ„๋ฆฌ์ง€๋ฅผ ์ถ”์ ํ•˜์—ฌ ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง์— 80% ์ด์ƒ์˜ ์ž„๊ณ„๊ฐ’์„ ์ ์šฉํ•ฉ๋‹ˆ๋‹ค
  • ๋ฉด์ ‘ ์ค€๋น„: pump()๊ณผ pumpAndSettle()์˜ ์ฐจ์ด, Provider/Riverpod ํ…Œ์ŠคํŠธ ์˜ค๋ฒ„๋ผ์ด๋“œ, ์ƒ์„ฑ์ž ์ฃผ์ž… ํŒจํ„ด์„ ๊ตฌ์ฒด์ ์ธ ์ฝ”๋“œ ์˜ˆ์ œ๋กœ ์—ฐ์Šตํ•ฉ๋‹ˆ๋‹ค

์—ฐ์Šต์„ ์‹œ์ž‘ํ•˜์„ธ์š”!

๋ฉด์ ‘ ์‹œ๋ฎฌ๋ ˆ์ดํ„ฐ์™€ ๊ธฐ์ˆ  ํ…Œ์ŠคํŠธ๋กœ ์ง€์‹์„ ํ…Œ์ŠคํŠธํ•˜์„ธ์š”.

Anthony Fillion-Maillet

์ž‘์„ฑ์ž

Anthony Fillion-Maillet

ํ’€์Šคํƒ ๊ฐœ๋ฐœ์ž, SharpSkill ์ฐฝ์—…์ž

10๋…„ ์ด์ƒ ํ’€์Šคํƒ ๊ฐœ๋ฐœ์„ ํ•ด์™”์Šต๋‹ˆ๋‹ค. SharpSkill์„ ์šด์˜ํ•˜๋ฉฐ ์ด๊ณณ์— ๊ฒŒ์‹œ๋˜๋Š” ๋ชจ๋“  ๋‚ด์šฉ์— ์ฑ…์ž„์„ ์ง‘๋‹ˆ๋‹ค.

2026๋…„ 5์›” 14์ผ ์—…๋ฐ์ดํŠธ

ํƒœ๊ทธ

#flutter
#testing
#widget-test
#integration-test
#interview

๊ณต์œ 

๊ด€๋ จ ๊ธฐ์‚ฌ

๋ชจ๋ฐ”์ผ ๊ฐœ๋ฐœ์ž๋ฅผ ์œ„ํ•œ Flutter ๋ฉด์ ‘ ์งˆ๋ฌธ ๊ฐ€์ด๋“œ

๋ชจ๋ฐ”์ผ ๊ฐœ๋ฐœ์ž๋ฅผ ์œ„ํ•œ Flutter ๋ฉด์ ‘ ์งˆ๋ฌธ 20์„ 

Flutter ๋ฉด์ ‘์—์„œ ๊ฐ€์žฅ ์ž์ฃผ ์ถœ์ œ๋˜๋Š” 20๊ฐ€์ง€ ์งˆ๋ฌธ์„ ์ค€๋น„ํ•˜์‹ญ์‹œ์˜ค. Widget, ์ƒํƒœ ๊ด€๋ฆฌ, Dart, ์•„ํ‚คํ…์ฒ˜, ๋ชจ๋ฒ” ์‚ฌ๋ก€๋ฅผ ์ƒ์„ธํ•œ ์ฝ”๋“œ ์˜ˆ์ œ์™€ ํ•จ๊ป˜ ์„ค๋ช…ํ•ฉ๋‹ˆ๋‹ค.

Dart 3 records, patterns and sealed classes for Flutter development

Flutter์™€ Dart 3: ๋ ˆ์ฝ”๋“œ, ํŒจํ„ด ๋งค์นญ, ๊ณ ๊ธ‰ ๋ฉด์ ‘ ์งˆ๋ฌธ ์™„๋ฒฝ ๊ฐ€์ด๋“œ

Dart 3์˜ ๋ ˆ์ฝ”๋“œ, ํŒจํ„ด ๋งค์นญ, ์‹ค๋“œ ํด๋ž˜์Šค๋ฅผ Flutter ์ฝ”๋“œ ์˜ˆ์ œ์™€ ํ•จ๊ป˜ ์ƒ์„ธํžˆ ์„ค๋ช…ํ•ฉ๋‹ˆ๋‹ค. ์™„์ „ ๋งค์นญ, ์ƒํƒœ ๋ชจ๋ธ๋ง, ๊ธฐ์ˆ  ๋ฉด์ ‘ ๋Œ€๋น„ ํ•ต์‹ฌ ์งˆ๋ฌธ์„ ๋‹ค๋ฃน๋‹ˆ๋‹ค.

Flutter ์ƒํƒœ ๊ด€๋ฆฌ 2026: Riverpod, Bloc, GetX ๋น„๊ต ๋‹ค์ด์–ด๊ทธ๋žจ

2026๋…„ Flutter ์ƒํƒœ ๊ด€๋ฆฌ ์™„๋ฒฝ ๊ฐ€์ด๋“œ: Riverpod vs Bloc vs GetX ๋น„๊ต ๋ถ„์„

Riverpod 3.0, Bloc 9.0, GetX ์„ธ ๊ฐ€์ง€ Flutter ์ƒํƒœ ๊ด€๋ฆฌ ์†”๋ฃจ์…˜์„ ์ฝ”๋“œ ์˜ˆ์ œ, ์„ฑ๋Šฅ ๋ถ„์„, ํ…Œ์ŠคํŠธ ์ „๋žต ๊ด€์ ์—์„œ ๋น„๊ต ๋ถ„์„ํ•ฉ๋‹ˆ๋‹ค.