2026
BrianUI
SwiftUI의 사용 흐름을 유지하면서, 테마와 컴포넌트 스타일을 유연하게 설정할 수 있는 디자인 시스템

BrianUI는 SwiftUI 앱에서 공통으로 사용할 수 있도록 만든 테마와 UI 컴포넌트 라이브러리입니다. 앱의 디자인 기준을 한곳에서 관리하면서도, 화면이나 컴포넌트에 필요한 차이는 유연하게 표현할 수 있도록 설계했습니다.
설계 과정에서 중요하게 생각한 것은 두 가지였습니다. 여러 화면에서 재사용할 수 있는 구조, 그리고 SwiftUI를 사용하는 개발자에게 익숙한 API입니다. 컴포넌트의 외형뿐 아니라 테마를 전달하고 스타일을 조정하는 방식까지 라이브러리의 사용 경험으로 보았습니다.
재사용 가능한 UI 라이브러리 설계
공통 UI를 만들 때는 어디까지 공유하고 어디에서 차이를 허용할지 정해야 합니다. 모든 값을 컴포넌트 안에 고정하면 앱의 디자인을 바꾸기 어렵고, 모든 값을 사용할 때마다 지정하면 같은 설정을 반복하게 됩니다.
BrianUI에서는 앱 전반의 디자인 기준을 담는 테마와, 개별 컴포넌트의 표현을 결정하는 설정을 분리했습니다. 색상과 간격 같은 공통 기준은 테마에서 관리하고, 버튼의 크기나 색상 역할은 컴포넌트 설정으로 조정하는 구조입니다.
테마를 통해 공유하는 디자인 기준
BrianTheme은 색상, 그라디언트, 간격, 모서리, 글꼴, 그림자 토큰을 담습니다. 이 테마를 SwiftUI의 환경값으로 전달해 하위 뷰가 같은 기준을 사용할 수 있도록 했습니다.
앱의 루트에 테마를 주입하면 공통 디자인을 적용할 수 있고, 특정 화면에 주입하면 해당 범위의 디자인을 조정할 수 있습니다. BrianUI 컴포넌트뿐 아니라 앱에서 직접 만든 뷰도 같은 환경값을 읽어 디자인 기준을 공유할 수 있습니다.
예를 들어 아래 화면은 기본 테마의 간격 하나를 변경한 뒤, 하위 뷰에서 그 값을 읽어 레이아웃에 사용합니다.
import SwiftUI
import BrianUI
struct ThemeExample: View {
private var theme: BrianTheme {
var theme = BrianTheme()
theme.spacing.medium = 12
return theme
}
var body: some View {
ThemeContent()
.environment(\.theme, theme)
}
}
private struct ThemeContent: View {
@Environment(\.theme) private var theme
var body: some View {
VStack(spacing: theme.spacing.medium) {
Text("디자인을 함께 사용하는 방법")
.typography(theme.typography.title.medium)
.foregroundStyle(theme.color.foreground.primary)
Button("확인") {}
.buttonStyle(.solid)
}
.padding(theme.spacing.large)
}
}
이 구조에서는 화면마다 숫자와 색상을 따로 정하는 대신, 의미에 맞는 토큰을 선택해 사용할 수 있습니다. 디자인을 수정할 때도 공통으로 바꿀 기준과 특정 화면에만 적용할 설정을 구분할 수 있습니다.
개발자 사용성을 고려한 API
재사용할 수 있는 컴포넌트라도 사용하는 방식이 낯설면 도입할 때 배워야 할 것이 늘어납니다. BrianUI는 SwiftUI의 기존 사용 방식과 자연스럽게 연결되도록 설계했습니다.
버튼은 SwiftUI의 Button을 그대로 사용하고, buttonStyle로 BrianUI의 스타일을 적용합니다. 액션과 레이블을 작성하는 방식은 익숙한 형태를 유지하고, 디자인에 필요한 설정을 수정자로 덧붙입니다.
공통 설정을 상속하고 필요한 속성만 변경하기
컴포넌트 설정은 부모 컨테이너에 적용할 수도 있고, 개별 컴포넌트에 적용할 수도 있습니다. 여러 버튼이 공유하는 크기와 색상 역할은 부모에서 정하고, 다른 표현이 필요한 버튼에서는 해당 속성만 변경할 수 있습니다.
import SwiftUI
import BrianUI
struct ButtonStylesExample: View {
var body: some View {
VStack {
Button("저장") {}
.buttonStyle(.solid)
Button("삭제") {}
.buttonStyle(.outline)
.buttonTone(.critical)
}
.buttonSize(.large)
.buttonTone(.brand)
}
}
두 버튼은 부모에서 지정한 큰 크기를 공유합니다. 저장 버튼은 브랜드 색상 역할을 사용하고, 삭제 버튼은 가까운 위치에서 지정한 critical 역할을 사용합니다. 삭제 버튼의 색상을 바꾸기 위해 크기까지 다시 지정할 필요가 없습니다. 예제의 액션은 스타일 설정에 집중하기 위해 비워 두었습니다.
전체 설정을 교체하는 API와 일부 속성만 바꾸는 API의 역할도 구분했습니다. buttonProperty(_:)는 버튼 설정 전체를 교체하고, buttonSize(_:)와 buttonTone(_:)은 각각의 필드만 변경합니다. 사용자는 필요한 변경 범위에 맞춰 API를 선택할 수 있습니다.
다만 같은 뷰에서 전체 설정 교체와 개별 필드 변경을 함께 사용하면 수정자 순서에 따라 결과가 달라질 수 있습니다. 이 차이를 문서에 명시하고, 가능한 한 한 가지 방식으로 설정하도록 안내했습니다.
컴포넌트와 앱의 책임 나누기
BrianUI는 버튼 외에도 텍스트 입력, 체크박스, 스위치, 칩, 수량 선택, 배지, 로딩 표시 등의 UI를 제공합니다. 입력과 선택 상태는 바인딩으로 연결하고, 앱은 그 상태를 바탕으로 실제 동작을 구현합니다.
로딩도 같은 기준으로 나누었습니다. isLoading(true)는 로딩 환경값을 전달하고 상호작용을 비활성화하며, 이를 읽는 BrianUI 버튼 스타일은 로딩 표시를 제공합니다. 비동기 작업의 실행과 종료, 오류 처리, 로딩 상태를 바꾸는 책임은 앱에 둡니다.
UI 라이브러리가 표현에 필요한 기능을 제공하고 앱이 동작을 결정하도록 경계를 두어, 같은 컴포넌트를 서로 다른 기능과 화면에서 사용할 수 있도록 했습니다.
사용 방법까지 포함한 라이브러리
API를 제공하는 것과 사용자가 그 API를 이해하는 것은 별개의 일입니다. BrianUI에는 시작하기, 테마와 스타일, 입력과 선택 상태, 알림과 바텀 시트 등을 설명하는 DocC 문서를 함께 구성했습니다.
특히 부모 설정의 적용 범위, 전체 프로퍼티 교체와 개별 속성 변경의 차이처럼 코드의 결과에 영향을 주는 규칙을 설명하는 데 신경 썼습니다. 컴포넌트 목록과 예제 코드를 통해 필요한 UI와 설정 방법을 찾아볼 수 있도록 했습니다.
이 글의 API 예제는 BrianUI 0.2.1 문서를 기준으로 작성했습니다.
BrianUI에서 중요하게 생각한 것
BrianUI를 만들며 집중한 것은 공통 디자인을 재사용하면서도 화면에 필요한 차이를 표현할 수 있는 구조였습니다. 테마로 앱의 기준을 공유하고, 스타일과 환경값으로 컴포넌트를 설정하며, 필요한 속성만 선택적으로 변경하도록 했습니다.
그 구조를 사용하는 코드에서도 설계 의도가 드러나길 바랐습니다. SwiftUI의 버튼, 스타일, 환경값, 바인딩을 활용한 이유도 개발자가 이미 익숙한 방식 안에서 디자인 시스템을 사용할 수 있도록 하기 위해서입니다.