Theme과 다크모드 — ThemeData로 앱 전체 스타일 관리

앱의 색상, 폰트, 버튼 스타일 등을 일관되게 관리하려면 Theme을 활용해야 합니다. 특히 다크모드 지원은 이제 거의 필수 기능이 되었습니다.


ThemeData 기본 설정

DART
MaterialApp(
  theme: ThemeData(
    // Material 3 활성화
    useMaterial3: true,
    // 시드 컬러로 전체 색상 자동 생성
    colorSchemeSeed: Colors.blue,
    // 밝기
    brightness: Brightness.light,
  ),
  darkTheme: ThemeData(
    useMaterial3: true,
    colorSchemeSeed: Colors.blue,
    brightness: Brightness.dark,
  ),
  // 시스템 설정에 따라 자동 전환
  themeMode: ThemeMode.system,
  home: const HomeScreen(),
)

ColorScheme 직접 정의

DART
ThemeData(
  colorScheme: ColorScheme.fromSeed(
    seedColor: const Color(0xFF6750A4),
    brightness: Brightness.light,
  ),
)

// 또는 완전 커스텀
ThemeData(
  colorScheme: const ColorScheme(
    brightness: Brightness.light,
    primary: Color(0xFF1976D2),
    onPrimary: Colors.white,
    secondary: Color(0xFF03DAC6),
    onSecondary: Colors.black,
    error: Color(0xFFB00020),
    onError: Colors.white,
    surface: Colors.white,
    onSurface: Colors.black,
  ),
)

Theme에서 색상 사용하기

DART
@override
Widget build(BuildContext context) {
  final colorScheme = Theme.of(context).colorScheme;

  return Container(
    color: colorScheme.surface,
    child: Text(
      '테마 색상 사용',
      style: TextStyle(color: colorScheme.onSurface),
    ),
  );
}

ColorScheme의 주요 색상

속성용도
primary주요 컴포넌트 (버튼, FAB 등)
onPrimaryprimary 위의 텍스트/아이콘
secondary보조 강조
surface카드, 시트 등 표면
error에러 상태
outline테두리

위젯별 테마 커스터마이징

DART
ThemeData(
  useMaterial3: true,
  colorSchemeSeed: Colors.blue,

  // AppBar 테마
  appBarTheme: const AppBarTheme(
    centerTitle: true,
    elevation: 0,
    scrolledUnderElevation: 1,
  ),

  // 카드 테마
  cardTheme: CardTheme(
    elevation: 2,
    shape: RoundedRectangleBorder(
      borderRadius: BorderRadius.circular(12),
    ),
  ),

  // 버튼 테마
  elevatedButtonTheme: ElevatedButtonThemeData(
    style: ElevatedButton.styleFrom(
      padding: const EdgeInsets.symmetric(
        horizontal: 24,
        vertical: 12,
      ),
      shape: RoundedRectangleBorder(
        borderRadius: BorderRadius.circular(8),
      ),
    ),
  ),

  // 입력 필드 테마
  inputDecorationTheme: InputDecorationTheme(
    border: OutlineInputBorder(
      borderRadius: BorderRadius.circular(8),
    ),
    filled: true,
    contentPadding: const EdgeInsets.symmetric(
      horizontal: 16,
      vertical: 12,
    ),
  ),

  // 텍스트 테마
  textTheme: const TextTheme(
    displayLarge: TextStyle(fontWeight: FontWeight.bold),
    titleLarge: TextStyle(fontWeight: FontWeight.w600),
  ),
)

다크모드 구현

시스템 설정 자동 감지

DART
MaterialApp(
  theme: _lightTheme(),
  darkTheme: _darkTheme(),
  themeMode: ThemeMode.system,  // 시스템 설정 따름
)

ThemeData _lightTheme() {
  return ThemeData(
    useMaterial3: true,
    colorSchemeSeed: Colors.blue,
    brightness: Brightness.light,
  );
}

ThemeData _darkTheme() {
  return ThemeData(
    useMaterial3: true,
    colorSchemeSeed: Colors.blue,
    brightness: Brightness.dark,
  );
}

수동 전환 구현

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

  @override
  State<MyApp> createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> {
  ThemeMode _themeMode = ThemeMode.system;

  void _toggleTheme() {
    setState(() {
      _themeMode = _themeMode == ThemeMode.light
          ? ThemeMode.dark
          : ThemeMode.light;
    });
  }

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      theme: _lightTheme(),
      darkTheme: _darkTheme(),
      themeMode: _themeMode,
      home: HomeScreen(onToggleTheme: _toggleTheme),
    );
  }
}

// 토글 버튼
IconButton(
  icon: Icon(
    Theme.of(context).brightness == Brightness.dark
        ? Icons.light_mode
        : Icons.dark_mode,
  ),
  onPressed: widget.onToggleTheme,
)

Theme 부분 오버라이드

특정 위젯 트리에만 다른 테마를 적용할 수 있습니다.

DART
Theme(
  data: Theme.of(context).copyWith(
    colorScheme: Theme.of(context).colorScheme.copyWith(
      primary: Colors.red,
    ),
  ),
  child: ElevatedButton(
    onPressed: () {},
    child: const Text('빨간 버튼'),  // 이 버튼만 빨간색
  ),
)

현재 테마 정보 확인

DART
@override
Widget build(BuildContext context) {
  // 현재 밝기
  final isDark = Theme.of(context).brightness == Brightness.dark;

  // 시스템 밝기 직접 확인
  final platformBrightness = MediaQuery.platformBrightnessOf(context);

  return Container(
    color: isDark ? Colors.grey.shade900 : Colors.white,
    child: Text(isDark ? '다크 모드' : '라이트 모드'),
  );
}

테마 관리 패턴

DART
// 테마 상수를 별도 파일로 분리
// lib/theme/app_theme.dart
abstract class AppTheme {
  static ThemeData light() {
    return ThemeData(
      useMaterial3: true,
      colorSchemeSeed: _seedColor,
      brightness: Brightness.light,
      appBarTheme: _appBarTheme,
      cardTheme: _cardTheme,
    );
  }

  static ThemeData dark() {
    return ThemeData(
      useMaterial3: true,
      colorSchemeSeed: _seedColor,
      brightness: Brightness.dark,
      appBarTheme: _appBarTheme,
      cardTheme: _cardTheme,
    );
  }

  static const _seedColor = Color(0xFF6750A4);

  static const _appBarTheme = AppBarTheme(
    centerTitle: true,
    elevation: 0,
  );

  static final _cardTheme = CardTheme(
    elevation: 1,
    shape: RoundedRectangleBorder(
      borderRadius: BorderRadius.circular(12),
    ),
  );
}

// 사용
MaterialApp(
  theme: AppTheme.light(),
  darkTheme: AppTheme.dark(),
  themeMode: ThemeMode.system,
)

정리

  • ThemeData로 앱 전체의 스타일을 한 곳에서 관리합니다
  • Material 3에서는 colorSchemeSeed로 시드 컬러만 지정하면 전체 색상 팔레트가 자동 생성됩니다
  • theme + darkTheme + themeMode로 다크모드를 간편하게 구현할 수 있습니다
  • Theme.of(context)로 현재 테마 정보에 접근합니다
  • 테마 설정은 별도 파일로 분리하면 유지보수가 편합니다
  • 하드코딩된 색상 대신 colorScheme의 속성을 사용하는 습관을 들이세요
댓글 로딩 중...