Flutter 튜토리얼 23편: 커스텀 폰트와 타이포그래피
요약
핵심 요지
- 문제 정의: 기본 시스템 폰트만으로는 브랜드 아이덴티티를 표현하기 어렵고, 텍스트 스타일을 일관되게 관리하기 번거롭다.
- 핵심 주장:
pubspec.yaml에 폰트를 등록하거나google_fonts1 패키지를 사용하면 커스텀 폰트를 쉽게 적용할 수 있다. - 주요 근거:
TextTheme2의 15가지 텍스트 스타일(displayLarge~bodySmall)을 정의하면 앱 전체에서 일관된 타이포그래피를 유지할 수 있다. - 실무 기준:
google_fonts패키지를 사용하면 1,000개 이상의 오픈소스 폰트를 별도 파일 없이 사용할 수 있다. - 한계: 가변 폰트(Variable Font)의 모든 축(axis)을 지원하지 않을 수 있다.
문서가 설명하는 범위
- 로컬 폰트 파일 등록과 사용법
- google_fonts 패키지 활용
- TextTheme으로 타이포그래피 시스템 구축
- 폰트 웨이트와 스타일 설정
읽는 시간: 12분 | 난이도: 초급
참고 자료
- Use a custom font - Flutter 공식 커스텀 폰트 가이드
- Typography - Flutter 타이포그래피 가이드
- google_fonts - Google Fonts 패키지
문제 상황
앱에서 텍스트 스타일을 관리할 때 여러 어려움이 있습니다.
폰트와 타이포그래피 관리의 어려움
문제 1: 기본 폰트로는 브랜드 아이덴티티 표현이 어려움문제 2: 텍스트 크기, 굵기를 각 위젯에서 개별 지정 → 일관성 유지 어려움문제 3: 폰트 파일 관리 → 웨이트별로 여러 파일 필요문제 4: 다국어 지원 → 언어별로 다른 폰트가 필요할 수 있음해결 방법
Flutter는 커스텀 폰트와 체계적인 타이포그래피 시스템을 지원합니다. 로컬 파일이나 Google Fonts를 통해 폰트를 적용하고, TextTheme으로 일관된 스타일을 관리합니다.
챕터 1: 로컬 폰트 파일 등록하기
Why
NOTE브랜드 전용 폰트나 라이선스가 있는 상용 폰트를 사용하려면 폰트 파일을 직접 프로젝트에 포함해야 합니다.
pubspec.yaml에 폰트를 등록하면 앱 번들에 포함됩니다.폰트 파일 준비 → pubspec.yaml 등록 → TextStyle에서 fontFamily 지정
What
NOTEFlutter는 TrueType(
.ttf), OpenType(.otf), OpenType Collection(.ttc) 폰트를 지원합니다.
웹 전용 포맷(.woff,.woff2)은 데스크톱 플랫폼에서 지원하지 않습니다.
How
TIP프로젝트 구조
my_app/├── fonts/│ ├── Pretendard-Regular.ttf│ ├── Pretendard-Medium.ttf│ ├── Pretendard-SemiBold.ttf│ ├── Pretendard-Bold.ttf│ └── Pretendard-ExtraBold.ttf├── lib/│ └── main.dart└── pubspec.yamlpubspec.yaml 설정
flutter:fonts:- family: Pretendardfonts:- asset: fonts/Pretendard-Regular.ttfweight: 400- asset: fonts/Pretendard-Medium.ttfweight: 500- asset: fonts/Pretendard-SemiBold.ttfweight: 600- asset: fonts/Pretendard-Bold.ttfweight: 700- asset: fonts/Pretendard-ExtraBold.ttfweight: 800앱 전체 기본 폰트 설정
MaterialApp(theme: ThemeData(fontFamily: 'Pretendard',),)특정 위젯에서 사용
Text('커스텀 폰트 텍스트',style: TextStyle(fontFamily: 'Pretendard',fontWeight: FontWeight.w600,),)
Watch out
WARNING
fontWeight에 해당하는 폰트 파일이 없으면 Flutter가 가장 가까운 웨이트를 합성(simulate)합니다.
합성된 폰트는 품질이 떨어지므로 필요한 웨이트의 파일을 모두 포함하세요.# 잘못된 예시 - w500 요청 시 w400을 합성flutter:fonts:- family: MyFontfonts:- asset: fonts/MyFont-Regular.ttf # w400만 있음# 올바른 예시 - 필요한 웨이트 모두 포함flutter:fonts:- family: MyFontfonts:- asset: fonts/MyFont-Regular.ttfweight: 400- asset: fonts/MyFont-Medium.ttfweight: 500- asset: fonts/MyFont-Bold.ttfweight: 700
결론: pubspec.yaml에 폰트 파일을 등록하고 weight 속성으로 웨이트를 매핑합니다.
챕터 2: Google Fonts 패키지 사용하기
Why
NOTE폰트 파일을 직접 관리하는 것은 번거롭습니다.
google_fonts패키지를 사용하면 1,000개 이상의 오픈소스 폰트를 네트워크에서 자동으로 다운로드해서 사용할 수 있습니다.graph LR A[GoogleFonts.lato] --> B[네트워크 다운로드] B --> C[로컬 캐시 저장] C --> D[TextStyle 반환]
What
NOTE
google_fonts패키지는 Google Fonts에서 제공하는 모든 폰트를 런타임에 다운로드하고 캐싱합니다.
폰트 파일을 프로젝트에 포함할 필요가 없어 앱 크기를 줄일 수 있습니다.
How
TIP패키지 설치
Terminal window flutter pub add google_fonts기본 사용법
import 'package:google_fonts/google_fonts.dart';Text('Hello, Google Fonts!',style: GoogleFonts.lato(),)스타일 커스터마이징
Text('커스텀 스타일',style: GoogleFonts.notoSansKr(fontSize: 24,fontWeight: FontWeight.w700,fontStyle: FontStyle.normal,letterSpacing: -0.5,),)기존 TextStyle과 결합
Text('테마 스타일과 결합',style: GoogleFonts.lato(textStyle: Theme.of(context).textTheme.displayLarge,fontWeight: FontWeight.bold,),)동적으로 폰트 선택
// 폰트 이름을 문자열로 지정Text('동적 폰트',style: GoogleFonts.getFont('Noto Sans KR'),)앱 전체 기본 폰트로 설정
MaterialApp(theme: ThemeData(textTheme: GoogleFonts.notoSansKrTextTheme(),),)
Watch out
WARNINGGoogle Fonts는 첫 사용 시 네트워크에서 다운로드합니다.
오프라인 환경에서는 미리 캐시되지 않은 폰트를 사용할 수 없습니다.// 프로덕션에서는 폰트를 미리 번들링하는 것을 권장// pubspec.yaml에 에셋으로 폰트 파일 포함 후:GoogleFonts.config.allowRuntimeFetching = false;앱 크기가 중요하다면 Google Fonts를, 오프라인 지원이 중요하다면 로컬 폰트를 선택하세요.
결론: google_fonts 패키지로 다양한 폰트를 쉽게 사용할 수 있지만, 오프라인 지원이 필요하면 로컬 폰트를 사용합니다.
챕터 3: TextTheme으로 타이포그래피 시스템 구축
Why
NOTE앱 전체에서 일관된 텍스트 스타일을 유지하려면 체계적인 타이포그래피 시스템이 필요합니다.
TextTheme은 15가지 텍스트 스타일을 정의해서 용도별로 사용할 수 있게 합니다.displayLarge/Medium/Small → 큰 제목headlineLarge/Medium/Small → 섹션 제목titleLarge/Medium/Small → 카드 제목 등bodyLarge/Medium/Small → 본문labelLarge/Medium/Small → 버튼, 레이블
What
NOTEMaterial Design 3의
TextTheme은 5개 카테고리(Display, Headline, Title, Body, Label)와 3개 크기(Large, Medium, Small)로 구성됩니다.
총 15가지 스타일 조합을 제공합니다.
How
TIP커스텀 TextTheme 정의
MaterialApp(theme: ThemeData(textTheme: const TextTheme(displayLarge: TextStyle(fontSize: 57,fontWeight: FontWeight.w400,letterSpacing: -0.25,height: 1.12,),displayMedium: TextStyle(fontSize: 45,fontWeight: FontWeight.w400,letterSpacing: 0,height: 1.16,),displaySmall: TextStyle(fontSize: 36,fontWeight: FontWeight.w400,letterSpacing: 0,height: 1.22,),headlineLarge: TextStyle(fontSize: 32,fontWeight: FontWeight.w600,letterSpacing: 0,height: 1.25,),headlineMedium: TextStyle(fontSize: 28,fontWeight: FontWeight.w600,letterSpacing: 0,height: 1.29,),headlineSmall: TextStyle(fontSize: 24,fontWeight: FontWeight.w600,letterSpacing: 0,height: 1.33,),titleLarge: TextStyle(fontSize: 22,fontWeight: FontWeight.w500,letterSpacing: 0,height: 1.27,),titleMedium: TextStyle(fontSize: 16,fontWeight: FontWeight.w500,letterSpacing: 0.15,height: 1.50,),titleSmall: TextStyle(fontSize: 14,fontWeight: FontWeight.w500,letterSpacing: 0.1,height: 1.43,),bodyLarge: TextStyle(fontSize: 16,fontWeight: FontWeight.w400,letterSpacing: 0.5,height: 1.50,),bodyMedium: TextStyle(fontSize: 14,fontWeight: FontWeight.w400,letterSpacing: 0.25,height: 1.43,),bodySmall: TextStyle(fontSize: 12,fontWeight: FontWeight.w400,letterSpacing: 0.4,height: 1.33,),labelLarge: TextStyle(fontSize: 14,fontWeight: FontWeight.w500,letterSpacing: 0.1,height: 1.43,),labelMedium: TextStyle(fontSize: 12,fontWeight: FontWeight.w500,letterSpacing: 0.5,height: 1.33,),labelSmall: TextStyle(fontSize: 11,fontWeight: FontWeight.w500,letterSpacing: 0.5,height: 1.45,),),),)위젯에서 TextTheme 사용
class ArticlePage extends StatelessWidget {const ArticlePage({super.key});@overrideWidget build(BuildContext context) {final textTheme = Theme.of(context).textTheme;return Column(crossAxisAlignment: CrossAxisAlignment.start,children: [Text('Flutter 튜토리얼', style: textTheme.displaySmall),const SizedBox(height: 8),Text('커스텀 폰트 가이드', style: textTheme.headlineMedium),const SizedBox(height: 16),Text('이 문서에서는 Flutter에서 커스텀 폰트를 사용하는 방법을 설명합니다.',style: textTheme.bodyLarge,),const SizedBox(height: 24),ElevatedButton(onPressed: () {},child: Text('시작하기', style: textTheme.labelLarge),),],);}}
Watch out
WARNINGTextTheme의 모든 스타일을 사용할 필요는 없습니다.
앱에 필요한 스타일만 정의하고 일관되게 사용하세요.// 권장: 용도에 맞는 스타일 사용Text('페이지 제목', style: textTheme.headlineLarge)Text('본문 내용', style: textTheme.bodyMedium)// 비권장: 임의로 폰트 크기 지정Text('페이지 제목', style: TextStyle(fontSize: 28))Text('본문 내용', style: TextStyle(fontSize: 14))
결론: TextTheme으로 타이포그래피를 체계화하면 앱 전체에서 일관된 텍스트 스타일을 유지할 수 있습니다.
챕터 4: Google Fonts로 TextTheme 생성
Why
NOTEGoogle Fonts와 TextTheme을 결합하면 커스텀 폰트로 전체 타이포그래피 시스템을 쉽게 구축할 수 있습니다.
GoogleFonts.xxxTextTheme()메서드가 이 작업을 간단하게 만들어줍니다.GoogleFonts.notoSansKrTextTheme() → 15가지 스타일 모두 Noto Sans KR 적용
What
NOTE각 Google Font에는 해당 폰트가 적용된 전체 TextTheme을 반환하는 메서드가 있습니다.
기존 TextTheme을 기반으로 폰트만 교체할 수도 있습니다.
How
TIP전체 TextTheme을 Google Font로 설정
MaterialApp(theme: ThemeData(textTheme: GoogleFonts.notoSansKrTextTheme(),),)기존 테마에 Google Font 병합
MaterialApp(theme: ThemeData(textTheme: GoogleFonts.notoSansKrTextTheme(Theme.of(context).textTheme,),),)제목과 본문에 다른 폰트 적용
final baseTextTheme = Theme.of(context).textTheme;MaterialApp(theme: ThemeData(textTheme: TextTheme(// Display, Headline은 Playfair DisplaydisplayLarge: GoogleFonts.playfairDisplay(textStyle: baseTextTheme.displayLarge,),displayMedium: GoogleFonts.playfairDisplay(textStyle: baseTextTheme.displayMedium,),headlineLarge: GoogleFonts.playfairDisplay(textStyle: baseTextTheme.headlineLarge,),// Body, Label은 Noto SansbodyLarge: GoogleFonts.notoSans(textStyle: baseTextTheme.bodyLarge,),bodyMedium: GoogleFonts.notoSans(textStyle: baseTextTheme.bodyMedium,),labelLarge: GoogleFonts.notoSans(textStyle: baseTextTheme.labelLarge,),),),)다크 모드용 TextTheme
ThemeData(brightness: Brightness.dark,textTheme: GoogleFonts.notoSansKrTextTheme(ThemeData.dark().textTheme,),)
Watch out
WARNING
GoogleFonts.xxxTextTheme()은 기본 Material TextTheme을 사용합니다.
커스텀 크기나 높이가 필요하면 별도로 정의해야 합니다.// 기본 크기 사용GoogleFonts.latoTextTheme()// 커스텀 크기 필요 시GoogleFonts.latoTextTheme().copyWith(displayLarge: GoogleFonts.lato(fontSize: 60, // 기본값 57 대신 60fontWeight: FontWeight.bold,),)
결론: GoogleFonts.xxxTextTheme()으로 전체 타이포그래피 시스템에 Google Font를 쉽게 적용할 수 있습니다.
챕터 5: 폰트 웨이트와 스타일 활용
Why
NOTE같은 폰트라도 웨이트(굵기)와 스타일(이탤릭)을 조합하면 다양한 표현이 가능합니다.
적절한 웨이트 활용은 시각적 계층 구조를 만드는 데 중요합니다.본문: w400 (Regular)강조: w500 (Medium) 또는 w600 (SemiBold)제목: w700 (Bold)
What
NOTE
FontWeight클래스는 100부터 900까지 9단계 웨이트를 제공합니다.
FontStyle은normal과italic두 가지를 지원합니다.
How
TIPFontWeight 상수
FontWeight.w100 // ThinFontWeight.w200 // ExtraLightFontWeight.w300 // LightFontWeight.w400 // Regular (기본값)FontWeight.w500 // MediumFontWeight.w600 // SemiBoldFontWeight.w700 // BoldFontWeight.w800 // ExtraBoldFontWeight.w900 // Black// 별칭FontWeight.normal // w400FontWeight.bold // w700웨이트와 스타일 조합
// RegularTextStyle(fontWeight: FontWeight.w400)// BoldTextStyle(fontWeight: FontWeight.w700)// ItalicTextStyle(fontStyle: FontStyle.italic)// Bold ItalicTextStyle(fontWeight: FontWeight.w700,fontStyle: FontStyle.italic,)시각적 계층 예시
class Typography extends StatelessWidget {const Typography({super.key});@overrideWidget build(BuildContext context) {return Column(crossAxisAlignment: CrossAxisAlignment.start,children: [// 메인 제목 - ExtraBoldText('Welcome',style: GoogleFonts.notoSansKr(fontSize: 32,fontWeight: FontWeight.w800,),),// 부제목 - SemiBoldText('시작해봅시다',style: GoogleFonts.notoSansKr(fontSize: 20,fontWeight: FontWeight.w600,),),// 본문 - RegularText('이 앱은 Flutter로 만들어졌습니다. 다양한 기능을 제공합니다.',style: GoogleFonts.notoSansKr(fontSize: 16,fontWeight: FontWeight.w400,),),// 캡션 - LightText('버전 1.0.0',style: GoogleFonts.notoSansKr(fontSize: 12,fontWeight: FontWeight.w300,color: Colors.grey,),),],);}}
Watch out
WARNING모든 폰트가 9단계 웨이트를 지원하지는 않습니다.
폰트가 지원하지 않는 웨이트를 요청하면 가장 가까운 웨이트로 대체됩니다.// Roboto는 9단계 모두 지원GoogleFonts.roboto(fontWeight: FontWeight.w500) // Medium 지원// 일부 폰트는 제한된 웨이트만 지원// 예: w400, w700만 있는 폰트에서 w500 요청 시 w400으로 대체Google Fonts 웹사이트에서 각 폰트의 지원 웨이트를 확인하세요.
결론: 폰트 웨이트를 적절히 활용해서 텍스트의 시각적 계층 구조를 만듭니다.
챕터 6: 가변 폰트와 고급 기능
Why
NOTE가변 폰트(Variable Font)는 하나의 파일에 여러 웨이트와 스타일을 포함합니다.
FontVariation을 사용하면 웨이트를 연속적인 값으로 지정할 수 있습니다.일반 폰트: w400, w500, w600 등 불연속 값가변 폰트: 100~900 사이의 모든 값 (예: w450)
What
NOTE가변 폰트는 OpenType 규격의 디자인 축(axis)을 지원합니다.
웨이트(wght), 너비(wdth), 기울기(slnt) 등을 연속적으로 조절할 수 있습니다.
How
TIPFontVariation 사용
Text('가변 폰트 텍스트',style: TextStyle(fontFamily: 'RobotoFlex', // 가변 폰트fontVariations: const [FontVariation('wght', 450), // 웨이트 450 (불연속 값 사이)FontVariation('wdth', 80), // 너비 80%FontVariation('slnt', -12), // 기울기 -12도],),)가변 폰트 등록 (pubspec.yaml)
flutter:fonts:- family: RobotoFlexfonts:- asset: fonts/RobotoFlex-VariableFont.ttfFontFeature로 글리프 선택
Text('fi fl', // 합자(ligature) 테스트style: TextStyle(fontFamily: 'Fira Code',fontFeatures: const [FontFeature.enable('liga'), // 합자 활성화FontFeature.enable('calt'), // 문맥 대체 활성화],),)숫자 스타일 변경
Text('0123456789',style: TextStyle(fontFeatures: const [FontFeature.oldstyleFigures(), // 올드스타일 숫자// 또는FontFeature.tabularFigures(), // 등폭 숫자],),)
Watch out
WARNING모든 폰트가 FontFeature와 FontVariation을 지원하지는 않습니다.
폰트 파일이 해당 기능을 포함해야 작동합니다.// 지원하지 않는 기능을 설정해도 에러는 없지만 효과도 없음Text('Text',style: TextStyle(fontFamily: 'BasicFont', // FontFeature 미지원 폰트fontFeatures: [FontFeature.enable('smcp')], // 무시됨),)가변 폰트를 사용하기 전에 해당 폰트의 지원 축(axis)을 확인하세요.
결론: 가변 폰트와 FontFeature를 활용하면 더 세밀한 타이포그래피 제어가 가능합니다.
한계
Flutter의 폰트 시스템은 대부분의 상황을 처리하지만 몇 가지 제약이 있습니다.
- 웹 포맷 미지원:
.woff,.woff2는 데스크톱 플랫폼에서 사용할 수 없습니다. - 일부 OpenType 기능 미지원: 모든 OpenType 기능이 Flutter에서 작동하지는 않습니다.
- 폰트 합성 품질: 존재하지 않는 웨이트를 합성하면 품질이 저하됩니다.
- CJK 폰트 크기: 한중일 폰트는 파일 크기가 커서 앱 용량에 영향을 줍니다.
Footnotes
공유
이 글이 도움이 되었다면 다른 사람과 공유해주세요!