디자인 시스템 구성하기
머리말
이번 글에서는 커피팩토리 디자인 시스템을 구성하기 위해 어떻게 접근하고 적용하였는지를 정리한다.
디자인 시스템 구성
구성요소 소개
커피팩토리 디자인 시스템 이하 커피팩토리는 크게 디자인과 개발 파트로 구성되어 있다. 각 파트에서 메인으로 사용되는 구성요소는 피그마와 스위프트 패키지다. 개발 파트에는 CLI 가 포함되어 있는데, 이는 피그마와 패키지를 연결시켜 주는 다리 역할을 한다.
피그마
커피팩토리를 설계하고 관리하기 위한 도구로, 커피팩토리에 사용되는 모든 구성요소를 작성한다. 개발 시 원하는 특정 패키지(모듈)을 추가하는 것처럼, 다른 디자인 파일에서 라이브러리로 적용해 디자인 시 활용할 수 있다.
스위프트 패키지
커피팩토리는 SwiftUI 개발 시 UI 패키지로 활용할 수 있도록 작성된다. 해당 패키지를 스위프트 패키지 매니저를 통해 가져온 후 UI 개발 시 활용할 수 있다.
스위프트 CLI
커피팩토리는 UI에 사용되는 값에 대한 소스를 피그마로 관리한다. 이를 스위프트 패키지로 전달하기 위해 스위프트 CLI가 활용된다. 피그마는 변경사항을 Variables를 통해 CLI에 전달하고, CLI는 github Action을 활용해 스위프트 패키지를 배포한다.
아토믹 디자인

커피팩토리는 각각의 모듈 제작 시 아토믹 디자인에 기반한 설계를 적용하였다. 아토믹 디자인이란, 브래드 프로스트가 제안한 화학적 관점에서 영감을 받은 디자인 접근 방법론이다. 모든 것은 모든 것은 atom(원자)으로 구성되어있고 atom(원자)들이 서로 결합하여 molecule(분자)이 되고, molecule는 더 복잡한 organism(유기체)으로 결합하여 궁극적으로 모든 물질을 생성한다는 관점으로 접근하여 작은 단위부터 점차 확장해나가는 방식의 디자인 시스템을 구성한다.[1]
아토믹 디자인을 차용했지만 아토믹 디자인의 구성요소를 그대로 적용하기보단, 아토믹 디자인의 개념 크게 두가지 원칙을 받아들여 디자인 시스템 개발에 적용하려 하였다.
-
모든 컴포넌트를 독립적인 의미를 갖는 원자 단위로 분해한다.
-
각 원자 단위를 조합해 구성한 셀을 중심으로 디자인 시스템을 확장시킨다.
아토믹 디자인 사례
아토믹 디자인의 관점으로 버튼 UI를 살펴보자. 무언가를 선택하기 위해 사용되는 버튼은 그 역할을 설명하기 위해 다양한 스타일을 적용하고 있다. 이 버튼의 스타일을 분해해보자.
버튼의 스타일을 분해해보면 다양한 스타일들이 적용되어 있음을 알 수 있다. 디자인 및 개발 경험이 있다면 다른 UI들도 버튼과 동일한 스타일들이 적용되어 있음을 알 수 있고, 분해해 나가다보면 이 스타일들이 공통된 요소라는 것도 알 수 있다.
아토믹 디자인을 적용하면, 위와 같이 스타일에서 동일한 역할을 하는, 의미를 갖는 요소들을 묶어 원자
(Atom)로 설정한다. 설정한 각 원자들을 구분하는 단위를 Foundation (파운데이션)으로 묶어 정의할 수 있다.
파운데이션은 각 요소들을 구분하지만, 파운데이션 만으로는 요소 간의 구분 이상의 역할을 수행할 수 없다.
Component (컴포넌트)는 특정한 역할을 수행하는 요소를 구분하기 위한 단위다. 컴포넌트는 독립적이고,
개별적인 역할을 포함하고 있으며, 그 역할을 잘 수행하기 위해, 여러 다양한 상태를 나타낼 수 있다. 이 상태를
표현하기 위해 파운데이션 요소들을 사용하며 파운데이션의 조합을 통해 특정한 컴포넌트의 상태를 표현한다. 즉
파운데이션의 조합으로 컴포넌트를 표현할 수 있다. 이러한 방식으로 점차 하위 컴포넌트에서 상위
컴포넌트로 확장해나가는 방식을 아토믹 디자인이라 한다.
피그마로 디자인 시스템 구성하기
각 구성요소는 개별적으로 살펴볼 것이기에, 이번 글에서는 전반적으로 피그마를 활용한 방법에 대해 소개하고자 한다.
피그마는 UI 디자인을 위한 훌륭한 도구다. 디자인을 할 줄 모르는 사람도 누구나 쉽고 편하게 사용할 수 있도록 도와준다. 피그마는 디자인을 위한 다양한 기능들을 제공하고 있다. 이 기능들은 커피팩토리의 각 구성 단계에서 다양하게 활용되고 있다.
패키지 레이어 구조에서 볼 수 있듯, 커피팩토리는 크게 4개의 레이어로 구분되어 있다. 컴포저블 컴포넌트, 시스템 컴포넌트, 코어 컴포넌트, 파운데이션이 각각의 레이어다. 더 크게 구분하자면, 위에 아토믹 디자인에서 설명했듯 파운데이션과 컴포넌트로 구분할 수 있다.
파운데이션은 피그마의 Styles 와 Variables 를 주로 활용하여 작성하고 관리하며, 컴포넌트는 Component
와 Variant 가 주로 활용된다.
Figma Variables
파운데이션 작성 시 활용되는 Variables 는 그 이름대로, 개발 시 변수에 해당된다. 이를 활용해 작성한
컴포넌트를 DevMode로 살펴보면 다음과 같이 적용 된 코드를 확인할 수 있다.
display: inline-flex;
height: var(--Frame-Height-xlarge, 48px);
min-width: var(--Frame-Width-minWidth, 64px);
padding: 0px var(--spacing-xxxsmall-1, 1px);
flex-direction: column;
justify-content: center;
align-items: center;
flex-shrink: 0;
VStack(alignment: .center, spacing: 0) { ... }
.padding(.horizontal, Constants.SpacingXxxsmall1)
.padding(.vertical, 0)
.frame(height: Constants.FrameHeightLarge, alignment: .center)
.cornerRadius(Constants.RadiusSquare0)
struct Constants {
static let RadiusSquare0: CGFloat = 0
static let StrokeXsmall1: CGFloat = 1
static let FrameHeightLarge: CGFloat = 40
static let SpacingXxxsmall1: CGFloat = 1
}
작성된 코드를 확인하면, Variables 로 작성된 요소가 css에는 css Variable 로, swift 에는 구조체의
Type Property 로 작성되는 모습을 확인할 수 있다. 즉 Variables 는 코드 작성 시 재사용 될 요소로
여겨진다.
enum 이었다면 더 좋지 않았을까?
그러나 Variables 에 모든 것을 저장하고 사용할 수 없다. Variables 의 사용될 수 있는 값은 Color,
Number, String, Bool 이다. Effect 나 Font Scale 같은 경우, Styles 를 활용해야 한다.
스타일, 컴포넌트 및 베리언트를 사용하는 방법이 궁금하다면 아래 자료들을 참조해보자.
- Create color, text, effect and layout grid styles
- Create components to reuse in designs
- Create and use variants
스위프트 패키지로 디자인 시스템 구성하기
피그마를 통해 작성한 디자인을 실제로 적용하기 위해선, 코드로 변환해야 한다. 변환 된 코드를 개발할 앱 안에 직접 작성할 수도 있지만, 그렇게 되면 디자인이 업데이트 될 때마다 개발할 앱들을 모두 수정해야 한다. 이를 최소화하고, 다양한 앱에서 동일한 코드를 사용할 수 있게 만들기 위해 패키지로 모듈화 시킬 수 있다.
스위프트 패키지 매니저는 위의 목적으로 만든 스위프트 패키지(모듈)를 관리할 수 있게 도와주는 도구다. 이 패키지는는 피그마의 디자인을 원 소스로 하기에, 피그마에 적용 된 디자인 상태를 동일하게 따른다.
let package = Package(
name: "CoffeeFactorySwift",
platforms: [.iOS(.v16), .macCatalyst(.v16), .macOS(.v13)],
products: [
// Products define the executables and libraries a package produces, and make them visible to other packages.
.library(
name: "coffee-factory-swift",
targets: ["CoffeeFactorySwift"]),
],
dependencies: [
// Dependencies declare other packages that this package depends on.
// .package(url: /* package url */, from: "1.0.0"),
],
targets: [
// Targets are the basic building blocks of a package. A target can define a module or a test suite.
// Targets can depend on other targets in this package, and on products in packages this package depends on.
.target(
name: "CoffeeFactorySwift",
dependencies: [],
resources: [.process("Resources")]
),
.testTarget(
name: "coffee-factory-swift-tests",
dependencies: ["CoffeeFactorySwift"]),
],
swiftLanguageVersions: [.v5]
)
CoffeeFactorySwift
- Component
- Core
...
- Foundation
- Color
- Layout
- Typography
...
- Resource
...
이 패키지에 작성된 코드는 github를 통해 배포되고, 다음과 같이 xcode에서 패키지로 가져와 사용할 수 있다.
// 1. module import
import CoffeeFactorySwift
struct MyView: View {
var body: some View {
VStack {
// 2. use Component
CFButton(label: "MyButton", size: .xlarge, type: .block) {
print("Hello CoffeeFactory")
}
// 3. use Modifier
Text("Welcome")
.pretendard(.largeTitle, weight: .bold)
}
}
}
피그마에서 파운데이션, 컴포넌트 별로 주로 활용한 기능들과 마찬가지로 스위프트 패키지에서도 스위프트의 여러
기능들을 활용하여 작성되었다. type extension 을 필두로 파운데이션은 enum 과 modifier 가 주로 활용
되었으며, 컴포넌트는 protocol 과 struct 가 주로 활용되었다.
extension CGFloat {
/**
View의 CornerRadius 값을 설정할 때 사용됩니다.
⚠️ Radius 값을 제외한 컨텍스트에서 사용하지 마세요.
- View 간 간격 조정에 사용될 값이 필요한 경우(padding, spacing) : `CFSpacing`
- View frame에 고정된 크기의 값이 필요한 경우(width, height) : `CFFrame`
---
Text("Sample")
.cornerRadius(.CFCornerRadius.xlarge) /// 16.0
---
- Authors: Coffee
- Date: 23.09.02
- Version: 0.1.0
*/
struct CFCornerRadius {
// MARK: - CornerRadius
private init(){}
/// rawValue: 2.0
static let xxsmall = FoundationSpaceRaduis.xxsmall.rawValue
/// rawValue: 4.0
static let xsmall = FoundationSpaceRaduis.xsmall.rawValue
/// rawValue: 6.0
static let small = FoundationSpaceRaduis.small.rawValue
/// rawValue: 8.0
static let medium = FoundationSpaceRaduis.medium.rawValue
/// rawValue: 12.0
static let large = FoundationSpaceRaduis.large.rawValue
/// rawValue: 16.0
static let xlarge = FoundationSpaceRaduis.xlarge.rawValue
/// rawValue: 20.0
static let xxlarge = FoundationSpaceRaduis.xxlarge.rawValue
/// rawValue: 24.0
static let xxxlarge = FoundationSpaceRaduis.xxxlarge.rawValue
/// rawValue: 100.0
static let round = FoundationSpaceRaduis.round.rawValue
}
}
protocol StyledLabelStyleEssential {
var alignStyle: LabelAlignStyle { get }
}
struct CFLabel: View, StyleEssential, StyledLabelStyleEssential {
var label: String
var icon: String = "plus.square.fill"
var type: LabelType = .blockFill
var size: LabelSize = .small
var color: Color = .CFSecondary.orange
var fontStyle: FoundationTypoSystemFont = .footnote
var alignStyle: LabelAlignStyle = .textOnly
var body: some View {
Label(label, systemImage: icon)
.labelStyle(CFLabelStyle(
type: type,
size: size,
color: color,
alignStyle: alignStyle
)
)
.systemFont(fontStyle)
}
}
스위프트 CLI로 피그마와 패키지 연결하기
위와 같이 디자인과 코드를 작성해나가다보면, 점점 변경사항이 많아지게 된다. 모든 변경 사항을 일일히 변환하는 작업은 시간과 비용이 많이 들 뿐더러, 오류의 가능성도 높다. 스위프트 CLI는 이러한 어려움을 해결해 주기 위한 도구다. CLI는 피그마의 Styles와 Variables 를 활용해 스위프트 패키지의 파운데이션을 비롯한 파일들을 생성하여, 피그마와 개발코드를 동기화 시키는 작업을 한다.
Figma API 활용하기
CLI가 동작하기 위해서 필요한 작업이 몇 가지 있는데, 첫 번째는 Figma API를 활용해야 한다는 점이다. 피그마는 사용자에게 다양한 API를 제공하고 있는데, 그 기능 중 사용자가 작성한 Styles 와 Variables 에 접근할 수 있는 API도 포함되어 있다.
문제는 Variables 가 체이닝으로 엮여 있는 경우, 해당 API를 사용해 Variables를 원하는 방식으로 사용하려면 엔터프라이즈 계정이 필요하다는 점이다.
위 내용은 2024년 2월을 기준이다. 솔직히 좀 아ㅣㄴ꼽다
이를 대체하기 위해
variables2json 라는
플러그인을 사용할 수 있다. 피그마 엔터프라이즈로 제공하는 API 기능은 피그마 플러그인 개발 시 일반 사용자도
사용할 수 있는데, 위 플러그인이 이를 활용하면, Variables 와 Styles 를 JSON 파일로 다운로드 받을 수
있다.
훌륭한 플러그인을 만들어 주신 Mark Mooibroek 감사해하자.
위 플러그인을 사용해 JSON으로 변환 된 파일을 살펴보면 다음과 같은 유형의 코드를 확인할 수 있다.
{
"version": "1.0.4",
"metadata": {},
"collections": [
{
"name": "A_ColorSystem",
"modes": [
{
"name": "light",
"variables": [
{
"name": "PrimaryScale/Primary/lightness",
"type": "color",
"isAlias": false,
"value": "#8B593F"
} ... ]
} ... ]
} ... ]
}
이를 활용해, 피그마의 업데이트 시 마다 해당 JSON의 값을 활용해 CLI를 통해 스위프트 코드를 만들어주면, 피그마와 스위프트 패키지 간의 업데이트 동기화가 간편해진다.
github Action 활용하기
github Action은 깃허브에서 제공하는 CI · CD 도구다. 이를 활용하면 깃허브 레포를 활용한 배포를 자동화를 비롯한 다양한 작업들을 수행할 수 있다. 이를 활용해 CLI 배포 시 마다 코드를 생성하고, 커피팩토리 레포지토리를 업데이트 시킨다.
import Foundation
import ArgumentParser
import SwiftyJSON
@main
struct CoffeeFactoryCLI: ParsableCommand {
static let configuration: CommandConfiguration = CommandConfiguration(
abstract: "Figma로 관리되는 variables를 쉽게 Swift Code로 추출할 수 있습니다.",
usage: "work <subcommand>",
subcommands: [All.self]
)
@Argument(help: "Path to the input file.")
var filePath: String = "\(FileManager.default.currentDirectoryPath)/Sources/Resources/variables.json"
public func run() throws {
let fileURL = URL(fileURLWithPath: filePath)
let rootJSON = JSON(try Data(contentsOf: fileURL))
let collections = rootJSON["collections"]
let foundation = collections
.filter { JSONManager.shared.search(name: $0.1["name"].string!, type: .foundation) }
.compactMap { $1 }
let typographyCollection = foundation
.filter { $0["name"].stringValue == FoundationCollection.typography.rawValue }.first
let colorCollection = foundation
.filter { $0["name"].stringValue == FoundationCollection.color.rawValue }.first
let layoutCollection = foundation
.filter { $0["name"].stringValue == FoundationCollection.layout.rawValue }.first
createDirectory()
if let typographyCollection = typographyCollection {
JSONManager.shared.makeFiles(json: typographyCollection, type: .typography)
}
if let colorCollection = colorCollection {
JSONManager.shared.makeFiles(json: colorCollection, type: .color)
}
if let layoutCollection = layoutCollection {
JSONManager.shared.makeFiles(json: layoutCollection, type: .layout)
}
}
}
정리
커피팩토리는 위의 방법들을 활용해 최종적으로 앱 제작 시 UI 디자인과 개발 시 모두 활용할 수 있는 각각의 모듈을 제공하여 제품 제작의 효율성과 일관성을 증대시킨다. 아직 작성 초기 단계라, 각 구성요소 별 작업 진행율도 낮고 변경도 자주 일어나지만, 큰 구조는 동일하다.
맺음말
지금까지 커피팩토리를 작성하는 구성요소들에 대해 간단하게 알아보았다. 틀을 설명하다보니, 생략된 내용이 많은 점에 양해부탁드린다. 이후 부터는 각 파운데이션 컴포넌트 별로 어떻게 작성하였는지를 다루겠다.
[1] Atomic Design Methodology - 디자인 시스템 작성을 고려한다면 해당 문서들을 참고해보자.