일본어 폰트로 LINE Seed JP를 쓰고 있었는데, 특정 화면에서 한자가 깨지거나 시스템 기본 글꼴로 표시되는 문제가 있었다. 원인은 LINE Seed JP 같은 브랜드 폰트가 상용 한자(常用漢字) 글리프를 전부 포함하지 않는 경우가 많다는 점이었다.
브랜드 폰트가 못 그리는 한자만 자동으로 대체 렌더링되게 했다.
static List<String> get _jpKanjiFallback =>
isJapan ? const [FontFamily.pretendardJP] : const [FontFamily.pretendard];
TextStyle(
fontFamily: _resolveFont(fontFamily),
fontFamilyFallback: _jpKanjiFallback,
...
);
Pretendard 오리지널은 한국어(한글) 최적화 폰트라 한자 글리프셋이 일본에서 기대하는 자형(신자체, JIS 자형)과 다르게 그려질 수 있다. PretendardJP는 이 문제를 겨냥해 일본어 환경에 맞는 한자 자형으로 별도 빌드된 버전이라, 일본 flavor에서는 fallback뿐 아니라 기본 폰트 자체를 PretendardJP로 교체했다.
static String _resolveFont(String? fontFamily, {String defaultFont = FontFamily.pretendard}) {
var requested = fontFamily ?? defaultFont;
if (isJapan) {
if (requested == FontFamily.pretendard) requested = FontFamily.pretendardJP;
if (!_jpAllowedFonts.contains(requested)) return FontFamily.pretendardJP;
return requested;
}
if (!isKorean && !_glAllowedFonts.contains(requested)) return FontFamily.pretendard;
return requested;
}
플레이버(지역)별로 허용 폰트 목록(_jpAllowedFonts, _glAllowedFonts)을 두고, 목록에 없는 폰트 요청이 들어오면 안전한 기본 폰트로 강제 치환하는 구조다. 디자인 스타일 유틸을 호출하는 쪽에서 실수로 안 맞는 폰트를 지정해도 최종 렌더링은 항상 안전망을 거치게 된다.
기존에는 두께별로 정적(static) 폰트 파일을 weight마다 하나씩 등록했다.
# Before: weight별 static 파일 8개
- family: Pretendard
fonts:
- asset: fonts/Pretendard-ExtraLight.ttf
weight: 200
- asset: fonts/Pretendard-Light.ttf
weight: 300
- asset: fonts/Pretendard-Regular.ttf
weight: 400
# ... Medium/SemiBold/Bold/ExtraBold/Black까지 총 8개, 약 21MB
Variable font는 하나의 파일 안에 weight 축(axis)을 연속값으로 담고 있어서 파일 하나로 전체 두께 범위를 커버한다.
# After: variable 파일 1개
- family: Pretendard
fonts:
- asset: fonts/PretendardVariable.ttf # 약 6.5MB, weight 100~900 전부 커버
같은 폰트 패밀리 기준 약 21MB → 6.5MB로 번들 크기가 줄었다.
주의
fontWeight만 지정하면 Flutter가 내부적으로 가장 가까운 static instance로 스냅하려 하고 variable font의 진짜 이점(임의 weight 보간)을 못 살린다.
fontVariationsaxis를 명시적으로 줘야 실제 weight 값대로 렌더링된다.
extension _FontVariations on TextStyle {
TextStyle withVariations() {
final w = (fontWeight ?? FontWeight.w400).value.toDouble();
return copyWith(fontVariations: [FontVariation('wght', w)]);
}
}
모든 텍스트 스타일 생성부에서 .withVariations()를 체이닝해서, fontWeight로 지정한 값이 실제로 variable font의 wght 축에 반영되도록 강제했다.
폰트 파일 추가/삭제가 잦다 보니 'Pretendard', 'PretendardJP' 같은 문자열 리터럴이 여기저기 흩어져 있으면 오타나 리네이밍 누락이 나기 쉬웠다. flutter_gen으로 생성한 FontFamily 상수 클래스로 전부 교체해서, 폰트 family 이름이 컴파일 타임에 검증되도록 했다.
static const _jpAllowedFonts = {
FontFamily.pretendardJP,
FontFamily.lINESeedJP,
FontFamily.rajdhani,
FontFamily.barlowCondensed,
};
fontFamilyFallback으로 안전망을 깐다.fontWeight만으로는 부족하고 fontVariations(wght axis)를 직접 지정해야 의도한 두께가 그대로 나온다.주의
문자열 리터럴로 폰트 이름, 이미지 경로를 직접 타이핑하면 오타가 나도 컴파일은 통과하고 런타임에야 터진다.
flutter_gen이pubspec.yaml의assets:/fonts:선언을 읽어 타입 세이프한 Dart 코드를 만들어준다.
dev_dependencies:
build_runner: ^2.15.0
flutter_gen_runner: ^5.14.1
flutter_gen:
output: lib/gen/
line_length: 120
integrations:
flutter_svg: true
lottie: true
assets:
outputs:
class_name: Assets
style: dot-delimiter
exclude:
- assets/localizable/**
- assets/fonts/**
- assets/dummy/**
assets.exclude는 Assets 클래스(이미지/아이콘/lottie) 생성 대상에서만 특정 경로를 뺀다.
폰트는 assets: 목록이 아니라 flutter: 하위 fonts: 블록을 따로 스캔해서 FontFamily 클래스를 만들기 때문에, assets.fonts/** exclude와 별개로 항상 생성된다.
flutter pub run build_runner build --delete-conflicting-outputs
프로젝트에선 easy_localization 키 생성까지 묶어서 스크립트로 실행한다.
#!/bin/bash
set -e
flutter pub run build_runner build --delete-conflicting-outputs
flutter pub run easy_localization:generate \
-S assets/localizable -s ko-KR.json \
-O lib/gen -o locale_keys.g.dart -f keys
결과물은 lib/gen/ 밑에 생성되고 git에 커밋되는 방식(코드 생성 시점마다 diff로 추적)이다.
lib/gen/assets.gen.dart
lib/gen/fonts.gen.dart
lib/gen/locale_keys.g.dart
// lib/gen/fonts.gen.dart
class FontFamily {
FontFamily._();
static const String pretendard = 'Pretendard';
static const String pretendardJP = 'PretendardJP';
static const String lINESeedJP = 'LINESeedJP';
static const String rajdhani = 'Rajdhani';
static const String barlowCondensed = 'BarlowCondensed';
static const String gmarketSans = 'GmarketSans';
}
// lib/gen/assets.gen.dart
class $AssetsIconsGen {
const $AssetsIconsGen();
SvgGenImage get icoAlertOn => const SvgGenImage('assets/icons/Ico_alert_on.svg');
AssetGenImage get appLogo => const AssetGenImage('assets/icons/app_logo.png');
}
문자열 대신 상수 접근이라 오타는 컴파일 에러로, 파일 리네이밍/삭제는 IDE Find Usages로 잡힌다.
// before
Text('제목', style: TextStyle(fontFamily: 'Pretendard'));
Image.asset('assets/icons/app_logo.png');
// after
Text('제목', style: TextStyle(fontFamily: FontFamily.pretendard));
Assets.icons.appLogo.image();
앞서 다룬 AppTextStyles._jpAllowedFonts, _resolveFont의 FontFamily.pretendard / FontFamily.pretendardJP 비교도 전부 이 생성 코드를 참조한다.
폰트 파일을 추가/교체(static → variable 전환 같은 작업)해도 pubspec.yaml만 고치고 재생성하면 참조하는 쪽 코드는 그대로 컴파일된다.