나는 이렇게 논다/Flutter 로 간단한 노트 앱을 만들어보자

Flutter로 멋진 노트 앱을 만들어보자 ③ - 컬러 추가와 테마 적용하기

daco2020 2024. 11. 16. 22:30
반응형

'간단한 노트 앱을 만들자' 시리즈 첫 번째 글에서 우리가 컬러 팔레트를 정의했던 걸 기억하시나요?

글 내용 일부

 

당시에는 기본적인 색상 몇 가지만 정의를 했었습니다만, 이제는 다양한 요소들이 추가되었기 때문에 더 상세한 컬러들을 팔레트에 정의하고 테마로서 쉽게 view 화면에 적용할 수 있도록 구현해보겠습니다.

 

 

목차

1. 컬러 팔레트 정의하기

2. 테마 정의하기

3. 테마를 앱 전체에 적용하기

4. 테마 적용 UI 컴포넌트 예시

 

 

 

 

1. 컬러 팔레트 정의하기

컬러 팔레트는 앱의 색상 설계를 한 곳에서 관리하도록 미리 정의해둔 파일입니다. 이번 글에서는 색상을 카테고리별로 나누어 추가하여 재사용성을 높여보겠습니다.

 

color_palette.dart

import 'package:flutter/material.dart';

class ColorPalette {
  // 기본 색상
  static const Color background = Color(0xFFF5F6F7); // 배경색
  static const Color surface = Color(0xFFFFFFFF); // 카드 등의 표면 색상
  static const Color primary = Color(0xFF2196F3); // 주요 브랜드 색상
  static const Color secondary = Color(0xFF03DAC6); // 보조 색상

  // 텍스트 색상
  static const Color textPrimary = Color(0xFF1C1C1E); // 주요 텍스트
  static const Color textSecondary = Color(0xFF6C757D); // 보조 텍스트
  static const Color textWhite = Color(0xFFFFFFFF); // 밝은 배경의 텍스트

  // 노트 라벨 색상
  static const Color noteYellow = Color(0xFFFFC107); // 진한 노란색
  static const Color noteBlue = Color(0xFF42A5F5); // 부드러운 파란색
  static const Color noteGreen = Color(0xFF66BB6A); // 부드러운 초록색
  static const Color notePink = Color(0xFFDF5683); // 부드러운 분홍색

  // 기능성 색상
  static const Color error = Color(0xFFB00020); // 에러/삭제
  static const Color success = Color(0xFF4CAF50); // 성공
  static const Color divider = Color(0xFFE0E0E0); // 구분선
}

 

 

기본 색상 뿐만 아니라, 텍스트, 노트 라벨, 기능성 등 카테고리를 주석으로 표기해두었습니다.

 

 

2. 테마 정의하기

테마를 미리 정의해두면 앱 전체 스타일을 통합 관리할 수 있습니다. 테마는 app_theme.dart 파일에 ThemeData를 사용해 정의하겠습니다.

 

app_theme.dart

import 'package:flutter/material.dart';
import 'package:ttingnote/utils/color_palette.dart';

// 커스텀 색상을 위한 확장
extension CustomColorScheme on ColorScheme {
  Color get noteYellow => ColorPalette.noteYellow;
  Color get noteBlue => ColorPalette.noteBlue;
  Color get noteGreen => ColorPalette.noteGreen;
  Color get notePink => ColorPalette.notePink;
  Color get success => ColorPalette.success;
}

class AppTheme {
  static ThemeData get lightTheme {
    return ThemeData(
      scaffoldBackgroundColor: ColorPalette.background,
      appBarTheme: AppBarTheme(
        backgroundColor: ColorPalette.surface,
        elevation: 0,
        titleTextStyle: const TextStyle(
          color: ColorPalette.textPrimary,
          fontSize: 18,
          fontWeight: FontWeight.w500,
        ),
        shape: const Border(
          bottom: BorderSide(
            color: ColorPalette.divider,
            width: 0.5,
          ),
        ),
        shadowColor: Colors.black.withOpacity(0.05),
        surfaceTintColor: Colors.transparent,
        scrolledUnderElevation: 8,
      ),

      // Material 3 TextTheme 설정
      textTheme: const TextTheme(
        // 본문 텍스트 스타일
        bodyLarge: TextStyle(
          color: ColorPalette.textPrimary,
          fontSize: 16,
          height: 1.5,
        ),
        bodyMedium: TextStyle(
          color: ColorPalette.textPrimary,
          fontSize: 14,
          height: 1.4,
        ),
        bodySmall: TextStyle(
          color: ColorPalette.textSecondary,
          fontSize: 12,
          height: 1.3,
        ),

        // 제목 텍스트 스타일
        titleLarge: TextStyle(
          color: ColorPalette.textPrimary,
          fontSize: 22,
          fontWeight: FontWeight.w600,
        ),
        titleMedium: TextStyle(
          color: ColorPalette.textPrimary,
          fontSize: 18,
          fontWeight: FontWeight.w500,
        ),
        titleSmall: TextStyle(
          color: ColorPalette.textPrimary,
          fontSize: 16,
          fontWeight: FontWeight.w500,
        ),
      ),

      // ColorScheme 설정
      colorScheme: ColorScheme.light(
        // 기본 색상
        primary: ColorPalette.primary,
        secondary: ColorPalette.secondary,
        error: ColorPalette.error,
        background: ColorPalette.background,
        surface: ColorPalette.surface,
        surfaceVariant: ColorPalette.background.withOpacity(0.95),

        // 텍스트/아이콘 색상
        onPrimary: ColorPalette.textWhite,
        onSecondary: ColorPalette.textWhite,
        onBackground: ColorPalette.textPrimary,
        onSurface: ColorPalette.textPrimary,
        onSurfaceVariant: ColorPalette.textSecondary,
        onError: ColorPalette.textWhite,

        // 구분선 및 기타
        outline: ColorPalette.divider,
        outlineVariant: ColorPalette.divider.withOpacity(0.5),
      ),
    );
  }
}

 

1번에서 정의해둔 컬러 파레트의 색상들을 사용하여 테마를 정의합니다.

 

 

 

TextTheme는 테마의 기본적인 텍스트 스타일을 정의합니다.

 

본문 텍스트

  • bodyLarge: 본문에서 큰 텍스트에 사용. (폰트 크기: 16)
  • bodyMedium: 본문에서 기본 크기 텍스트에 사용. (폰트 크기: 14)
  • bodySmall: 본문에서 작은 크기 텍스트에 사용. (폰트 크기: 12)

제목 텍스트

  • titleLarge: 페이지나 섹션의 주요 제목에 사용. (폰트 크기: 22)
  • titleMedium: 중간 크기의 제목에 사용. (폰트 크기: 18)
  • titleSmall: 작은 제목이나 강조 텍스트에 사용. (폰트 크기: 16)

 

텍스트 스타일을 정의하면 텍스트마다 별도로 스타일을 지정하지 않아도 기본 스타일을 적용할 수 있습니다.

 

추가로, 특정 텍스트를 커스터마이징하려면 copyWith 메서드를 사용하여 변화를 줄 수 있습니다.

 

 

 

ColorScheme는 앱의 주요 색상을 정의합니다. 

 

기본 색상

  • primary: 주요 색상.
  • secondary: 보조 색상.
  • error: 오류 표시 색상.
  • background: 기본 배경 색상.
  • surface: 카드 등 표면 색상.

텍스트/아이콘 색상

  • onPrimary: primary 배경 위 텍스트/아이콘 색상.
  • onSecondary: secondary 배경 위 텍스트/아이콘 색상.
  • onBackground: background 위 텍스트 색상.
  • onSurface: surface 위 텍스트 색상.

기타 색상

  • outline: 구분선 색상.
  • outlineVariant: 구분선의 투명도가 더 낮은 색상.

 

 

 

3. 테마를 앱 전체에 적용하기

main.dart에서 AppTheme.lightTheme을 MaterialApp.router의 theme 속성에 연결합니다. 

 

main.dart

import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:go_router/go_router.dart';
import 'package:ttingnote/theme/app_theme.dart';
import 'services/note_service.dart';

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

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MultiProvider(
      providers: [
        ChangeNotifierProvider(create: (_) => NoteService()),
      ],
      child: MaterialApp.router(
        routerConfig: _router,
        theme: AppTheme.lightTheme, // 테마 적용
        debugShowCheckedModeBanner: false,
      ),
    );
  }
}

 

이렇게 하면 모든 화면에 테마를 적용할 수 있습니다.

 

 

4. 테마 적용 UI 컴포넌트 예시

변경 전

테마를 사용하지 않으면, 각 UI 컴포넌트에서 필요한 스타일(색상, 텍스트 크기 등)을 개별적으로 지정해야 합니다.

Container(
  color: Colors.white, // 배경 색상 직접 지정
  child: Text(
    'Hello, World!',
    style: TextStyle(
      color: Colors.black, // 텍스트 색상 직접 지정
      fontSize: 18,       // 텍스트 크기 직접 지정
    ),
  ),
);

 

이 방식은 코드가 장황해지고, 스타일을 변경하려면 모든 UI 컴포넌트를 수정해야 하는 단점이 있죠.

 

변경 후

테마를 적용하면 기본 스타일이 설정되기 때문에, 위젯에서 스타일을 생략해도 테마에 따라 자동으로 적용됩니다.

Container(
  child: Text('Hello, World!'), // 테마에 따라 기본 스타일 적용
);

 

테마에서 정의된 TextTheme와 ColorScheme가 적용되어, 스타일 코드 없이도 일관된 디자인을 유지할 수 있는거죠.

 

생략이 가능한 경우

테마의 기본 스타일을 사용하여 생략이 가능한 스타일은 다음과 같습니다.

 

 

1. 텍스트 스타일

테마에 TextTheme이 정의되어 있다면, Text 위젯에서 기본 스타일을 생략할 수 있습니다.

// TextTheme.bodyMedium이 자동 적용
Text('자동 적용된 텍스트 스타일'),

 

2. 색상

테마의 ColorScheme에서 기본 색상(primary, background 등)을 설정했다면, Container, Scaffold, AppBar 등의 배경색을 생략해도 자동 적용됩니다.

// ColorScheme.background가 자동 적용
Scaffold(
  body: Center(child: Text('Hello!')),
),

 

 

 

 

지정이 필요한 경우

이와 달리 생략하지 않고 스타일을 지정해야하는 경우도 있습니다.

 

1. 강조가 필요한 경우

예를 들어, 특정 텍스트를 강조하거나 색상을 커스터마이징할 때에는 Theme.of(context).textTheme.bodyMedium 처럼 미리 정의한 스타일을 불러오고 .copyWith( ... ) 메서드를 사용하여 일부 스타일 속성을 변경할 수 있습니다.

Text(
  '강조 텍스트',
  style: Theme.of(context).textTheme.bodyMedium?.copyWith(
    color: Theme.of(context).colorScheme.primary, // 강조 색상
  ),
),

 

2. 일부 위젯의 고유 속성

특정 위젯의 속성은 테마로 설정되지 않아 직접 지정해야 합니다.

아이콘 크기: Icon.size
버튼 높이: Button.height

Icon(
  Icons.star,
  size: 24, // 아이콘 크기 명시
),

 

 

 

지정 방법

1. 텍스트 스타일

Theme.of(context).textTheme 처럼 TextTheme에서 제공하는 스타일을 사용하면 일관된 스타일을 사용할 수 있습니다.

Text(
  '기본 텍스트',
  style: Theme.of(context).textTheme.bodyMedium, // 테마에서 불러오기
),

 

2. 배경 색상

마찬가지로 배경 색상 또한 ColorScheme의 색상을 활용해 지정할 수 있습니다.

Container(
  color: Theme.of(context).colorScheme.surface, // 표면 색상
),

 

3. 버튼 스타일

버튼에서 색상을 지정할 때에도 테마를 기반으로 스타일을 지정할 수 있습니다.

ElevatedButton(
  onPressed: () {},
  style: ElevatedButton.styleFrom(
    backgroundColor: Theme.of(context).colorScheme.primary, // 버튼 배경색
  ),
  child: Text('테마 적용 버튼'),
),

 

 

마무리

이번 글에서는 컬러 팔레트 정의부터 테마 적용, 그리고 UI 컴포넌트에서 테마를 활용하는 방법까지 단계별로 살펴보았습니다. 테마를 적용하면 스타일 코드의 재사용성을 높이고 일관된 디자인을 유지할 수 있습니다.

 

다음 글 부터는 드디어 앱 출시에 대해 다루어보겠습니다.

 

 

[Flutter로 멋진 노트 앱을 만들어보자] 시리즈는 직접 독학으로 하나씩 만들어나가는 과정이므로 틀리거나 부족한 내용이 있을 수 있습니다. 조언과 피드백을 댓글로 남겨주시면 적극 반영하겠습니다. 감사합니다.
반응형