본문으로 건너뛰기

Typography

머리말

커피 팩토리 파운데이션의 핵심 요소 중 하나인 Typography 에 대해 작성합니다. 글을 작성하다보니, 원론적으로 되어가는 것 같아 제작에 초점을 맞춰 작성하였습니다. 좀 더 텍스트와 타이포그래피에 초점을 맞춘 내용이 궁금하신 분들을 여기[1]

정보 전달의 기본인 텍스트

오늘 날 정보 전달을 위해 수 많은 컨텐츠가 생성되고 소비되고 있다. 텍스트는 이러한 컨텐츠를 구성하는 기본적인 요소이다. 텍스트는 정보 전달을 목적으로 하기에, 효과적인 정보 전달을 위해 다양한 위계와 스타일이 사용된다. 파운데이션 타이포그래피는 이러한 텍스트의 위계와 스타일에 초점을 맞춰 작성되었다.

System Font

Font Family

Pretendard Font

시스템의 기본적인 폰트 패밀리는 pretendard 를 사용하였다. 프리텐다드는 SF Pro와 산돌 고딕 Neo 1 을 기반으로 한 플랫폼에 구애받지 않는 폰트 패밀리 셋이다. 커피팩토리는 스타일의 일관성을 추구한다. 이에, 플랫폼 별 시스템 폰트를 프리텐다드로 통합하고자 pretendard 를 적용하였다.

Font Scale

커피팩토리의 폰트 스케일은 HIG(Human Interface Guidelines) 스케일을 영향을 받아 작성되었다.

Display: HeroBanner 와 같은 대문에서 사용
LargeTitle: 페이지 내 가장 큰 제목을 표현할 때 일반적으로 사용 as <h1>
Title: 제목 컨텐츠를 표현할 때 일반적으로 사용 as <h2>
Title2: 제목 컨텐츠를 표현할 때 일반적으로 사용
Tilte3: 제목 컨텐츠를 표현할 때 일반적으로 사용 as <h3>
SubTitle: 보조적인 제목을 표현할 때 일반적으로 사용
Headline: 보조적인 제목을 표현할 때 일반적으로 사용 as <h4>
Body: 본문 사용 시 일반적으로 사용 as <p>, <h5>
Caption: 보조적인 설명을 위해 사용 as <h6>
Caption2: 보조적인 설명을 위해 사용

제작 과정 - 피그마

피그마의 폰트 스타일을 이용하면 쉽게 텍스트의 스타일을 위한 폰트 시스템을 작성할 수 있다. 붉은색으로 박스 쳐져 있는 요소들이 스타일을 구성하는 원자들 중 핵심요소들이다.

    interface FontStyle {
name: String // 폰트의 이름
fontFamily: String // 폰트 패밀리
fontWeight: "light" | "medium" | "bold" // 폰트 굵기
fontSize: Float // 폰트 크기
lineHeight: Float // 행간 크기
alignment: "left" | "center" | "right" // 텍스트 정렬 방법
}

추가한 폰트 스타일은 디자인 탭의 텍스트 스페이스에서 원하는 스타일을 선택해 사용할 수 있다. 해당 스타일 사용 시, 설정한 패밀리, 굵기, 크기, 행간은 인스턴스 안에서 변경할 수 없지만, 정렬 방법 및 리사이징, 리스트 스타일은 수정할 수 있다.

제작 과정 - 스위프트 패키지

SwiftUIViewModifier 조합으로 뷰를 작성한다. 기본적으로 Text 컴포넌트를 제공하고 있으므로, 이를 랩핑한 커스텀 컴포넌트를 만들 수도 있지만, extension 과 ViewModifier 를 활용해 좀 더 SwiftUI 스럽게 텍스트 스타일을 구현한다.

protocol CFFontDescriptor {
associatedtype FontEnum

var fontWeight: FontEnum { get }
var fontSize: CGFloat { get }
var lineHeight: CGFloat { get }
var letterSpacing: CGFloat { get }
var relativeTo: Font.TypographyStyle { get }
}

public enum Pretendard: String, CaseIterable {
case black = "Pretendard-Black"
case regular = "Pretendard-Regular"
case bold = "Pretendard-Bold"
case medium = "Pretendard-Medium"
case light = "Pretendard-Light"
case semiBold = "Pretendard-SemiBold"


public enum FontScale: String, CFFontDescriptor {
case caption = "Caption"
case title3 = "Title3"
case headline = "Headline"
case body = "Body"
case display = "Display"
case title2 = "Title2"
case subTitle = "SubTitle"
case caption2 = "Caption2"
case largeTitle = "LargeTitle"
case title = "Title"

var fontWeight: Pretendard {
switch self {
case .caption:
.medium
case .title3:
.medium
case .headline:
.medium
case .body:
.regular
case .display:
.bold
case .title2:
.semiBold
case .subTitle:
.semiBold
case .caption2:
.medium
case .largeTitle:
.bold
case .title:
.bold
}
}
...
}
}

텍스트를 구성하는 폰트패밀리를 정의하고, 폰트 별 스타일을 설정한다. 이 때 프로토콜과 이넘을 사용하면 규칙적은 스타일 정의에 도움이 된다.

extension Font {
public static func pretendard(_ pretendard: Pretendard, size: CGFloat, relativeTo: Font.TypographyStyle = .body) -> Font {
.custom(pretendard.rawValue, size: size, relativeTo: relativeTo)
}

public static func pretendard(_ fontScale: Pretendard.FontScale) -> Font {
.custom(fontScale.fontWeight.rawValue, size: fontScale.fontSize, relativeTo: fontScale.relativeTo)
}

public static func pretendard(_ fontScale: Pretendard.FontScale, weight: Pretendard?) -> Font {
guard let weight = weight else {
return .custom(fontScale.fontWeight.rawValue, size: fontScale.fontSize, relativeTo: fontScale.relativeTo)
}
return .custom(weight.rawValue, size: fontScale.fontSize, relativeTo: fontScale.relativeTo)
}
}

작성한 폰트 패밀리 enum을 사용해 Font 타입을 확장한다. 이렇게 하면, Font의 타입 프로퍼티의 정의 된 메서드를 통해 정의한 스타일의 폰트를 사용할 수 있다. 폰트를 사용하는 방법을 다양하게 정의해 용도 별로 사용할 수 있게 했다. 작성된 폰트는 다음과 같이 사용할 수 있다.

    Text("HELLO")
.font(.pretendard(.largeTitle))
Text("WORLD")
.font(.pretendard(.body, weight: .bold))

위 폰트 모디파이어에서 한 가지 아쉬운 점은 SwiftUI 의 디자인 시 폰트 설정 시 일반적으로 함께 설정하는 lineHeight 는 폰트만으로 선언할 수 없다는 점이다. 따라서 위 font 만으로는 lineHeight 를 디자인대로 작성할 수 없다.

extension View {
public func pretendard(_ scale: Pretendard.FontScale, weight: Pretendard? = nil) -> some View {
modifier(PretendardModifier(scale: scale, weight: weight))
}
}

struct PretendardModifier: ViewModifier {
var scale: Pretendard.FontScale
var weight: Pretendard?

func body(content: Content) -> some View {
content
.font(
.custom(
weight?.rawValue ?? scale.fontWeight.rawValue,
size: scale.fontSize,
relativeTo: scale.relativeTo
)
)
.lineSpacing(.pretendardLineSpacing(scale))
}
}

이를 해결하기 위해, ViewModifier 를 활용할 수 있다. fontlineSpacing 를 함께 적용한 뷰 모디파이어를 작성하고, 이를 폰트 타입에서와 같이 뷰 타입을 확장시키면 뷰에서 자유롭게 닷 구문으로 폰트를 적용시킬 수 있다.

    VStack {
Typography("HELLO")
Typography("WORLD")
}
.pretendard(.largeTitle)
.padding(24)

폰트 등록하기

위처럼 모디파이어를 사용하기 전 사전 작업이 하나 있다. 바로 폰트를 앱 시스템에 등록하는 과정이다. 프리텐다드 폰트는 앱이 설치되어 있는 폰트가 아니므로, 임베디드 해야 한다. 먼저 아래와 같이 사용할 폰트를 리소스에 추가하고 다음 코드를 작성한다.

public struct CoffeeFactoryFont {
public static func registerFonts() {
Pretendard.allCases.forEach { registerFont(bundle: .module, fontName: $0.rawValue, fonTypographyension: "otf") }
}

fileprivate static func registerFont(bundle: Bundle, fontName: String, fonTypographyension: String) {
guard let fontURL = bundle.url(forResource: fontName, withExtension: fonTypographyension),
let fontDataProvider = CGDataProvider(url: fontURL as CFURL),
let font = CGFont(fontDataProvider) else {
fatalError("Could't create font from filename: \(fontName) with extension \(fonTypographyension)")
}
var error: Unmanaged<CFError>?
CTFontManagerRegisterGraphicsFont(font, &error)
}
}


위 코드는 폰트를 등록하는 메서드다. 패키지를 앱에 추가한 이후, 앱 실행 시, 위 메서드를 실행하면, 앱에 폰트가 등록되어 사용할 수 있게 된다.

CoffeeFactoryFont.registerFonts() // 폰트 등록

맺음말

지금까지 텍스트를 구성하는 Typography 파운데이션이 어떻게 구성되어 있는지 살펴보았다. 텍스트를 표현하는 Typography 는 수많은 상위 컴포넌트의 기본이 된다. 다음은 텍스트와 마찬가지로 정보 전달 시 큰 역할을 하는 요소 중 하나인 Color 를 살펴보자.


[1] 타이포그래피 - HIG 정리