SwiftUI로 Design System 구축하기

goddoro
12 min readJun 11, 2023

--

안녕하세요 개발자 doro입니다.

Duckie의 iOS 제품은create project버튼을 누를 때부터 SwiftUI만을 사용해서 구현하고 싶다는 계획이 있었습니다. 선언형으로 UI코드를 작성하면 코드의 재사용성도 좋아지고 도메인 지식을 적용하기가 편해집니다.

개발하기에 앞서 서비스 전체의 아키텍쳐부터 구상하고 객체지향적인 코드를 작성하기 위해 꽤나 많은 고민을 했었습니다.

Duckie iOS Architecture

위의 아키텍쳐에서는 크게 :app모듈에서 서버로부터 데이터를 가져오는 방식화면을 그리기 위한 컴포넌트를 가져오는 방식 두 가지를 나타내고 있습니다.

Data Layer에서는 Network EngineService가 참조해 Repository가 가져다 쓰는 구조이고, UI Layer는 크게 appduckie-componentdesign-system의 구조로 나누어 서로 간의 다른 역할을 가질 수 있게 설계했습니다.

이 때, 가장 신경썼던 부분은 디자인 시스템을 적용해서 공통 컴포넌트를 다른 레이어에서 관리할 수 있게 분리했다는 점입니다. 제품에 디자인 시스템을 적용해서 얻을 수 있는 장점은 간단명료합니다.

관리가 쉬워지고 유지보수에 용이하다.

입니다. 완벽한 관심사 분리가 가능해지고 응집도를 높여서 구현할 수 밖에 없기 때문인데요. 이는 단순히 개발자가 편하다로 끝나는 문제를 넘어서 Quality Assurance 기간도 줄어들게 되고, UI가 변화에 유연하기 때문에 기획적으로도 다양한 시도를 해볼 수 있게 되는것이죠.

결국에 디자인 시스템은 팀 내에서 제품을 이용해 빠르게 테스트하고 빠르게 결과를 얻어낼 수 있다는 점에서 큰 의의가 있습니다. 이를 위해서 Duckie의 디자인팀은 디자인 시스템부터 체계적으로 서비스를 디자인했고, 그 덕에 이 화면 저 화면의 컴포넌트들의 스펙이 뒤죽박죽 섞이는 일이 없었습니다.

User Interface Architecture

iOS의 디자인 시스템을 설계하면서 가장 신경을 썼던 부분은

디자인 시스템은 시스템의 역할만 해야한다

였습니다. 어떠한 도메인 지식도 들어가면 안되고, 단순히 Domain Layer에서 이를 가져다 쓰는 설계여야 했습니다. 그래야 Domain Layer에 존재하는 여러 도메인 컴포넌트들이 시스템 컴포넌트를 오해없이 재사용 할 수 있습니다.

디자인 시스템의 이름은 quack-quack 이라고 지었고, 이 디자인 시스템만 보고서는 어떤 도메인의 서비스에서 사용되는지 가늠조차 할 수 없게 설계를 했습니다.

혜진님이 만들어주신 Design System

디자인 시스템에서는 Typo, Image, Icon, Tab 등 서비스에서 사용되는 컴포넌트들의 스펙을 먼저 정의합니다. 일종의 재료를 먼저 손질하는 과정이라고 볼 수 있는데요. 화면 구성을 독립적으로 하기보다는 서비스 내에서 공통으로 사용되는 텍스트는 무엇인지, 아이콘은 어떤것인지 등 정리를 한다면 같은 스펙을 가진 컴포넌트를 두 번 구현할 필요는 없겠죠.

제가 정의한 디자인 시스템들의 컴포넌트는 도메인 지식이 없는 Headline1 , Subtitle2 와 같은 것들입니다. 이를 이용해서 도메인 지식을 넣는 컴포넌트를 만든다면 HomeTitle 과 같은 텍스트로 추상화해서 사용할 수 있게되는것이죠.

HomeTitleProfileTitle 이 같은 Headline1 이라는 시스템을 이용하고 있다면 응집도가 높게 관리가 될 수 있으니 보다 객체지향적인 코드를 작성할 수 있게 됩니다.

예시를 들어 설명해볼까요.

QuackIconTextField

위 컴포넌트의 이름은 QuackIconTextField입니다. placeholder를 적을 수 있고 우측 중단에 아이콘을 배치해 TextField와 관련된 이벤트를 정의할 수 있는 컴포넌트입다. System Layer에 속합니다.

이 컴포넌트에 도메인 지식을 입혀보겠습니다.

DuckieAddTagTextField

placeholder에 태그 입력하기라는 도메인 지식이 들어갔고, 우화살표 아이콘을 넣어줘서 태그를 추가할 수 있는 이벤트를 정의했습니다. 이름은 DuckieAddTagTextField 가 되었고 이 컴포넌트는 Domain Layer에 속하게 됩니다.

이렇게 Domain Layer에서는 디자인 시스템의 여러 컴포넌트들을 재사용해서 화면을 구성할 때 직접 호출할 수 있는 도메인 컴포넌트들을 관리하는 역할을 합니다. 실제로 Domain Layer에 있는 컴포넌트들을 가공하는 일이 손이 가장 많이 가는 것 같습니다.

실제 제품의 화면에서의 예를 하나 더 들어보겠습니다.

아래 화면은 Duckie에서 유저들의 덕력 순위를 확인할 수 있는 명예의 전당 화면입니다.

덕키의 명예의 전당 UI

UI가 그렇게 복잡해보이지는 않습니다. 하지만 이런 화면도 다른 화면에서 미리 구현해놓은 컴포넌트들을 가져다 쓴다면 훨씬 빠르게 구현할 수 있을 것 같습니다.

구조화 해서 그려보면 어떤식으로 UI 코드를 작성해야하는지 좀 더 쉽게 알 수 있습니다.

명예의전당 화면의 구성요소들

RankingTopBar — 명예의 전당 페이지에 상단바 (빨간색 영역)

RankingMainTab — Duckie에서 UI를 분기해주는 컴포넌트 (주황색 영역)

RankingUserView — 명예의 전당에 등록된 유저들을 보여주는 컴포넌트 (초록색 영역)

RankingUserItemView — 유저의 순위와 정보를 보여주는 컴포넌트 (하늘색 영역)

이렇게 구조화된 도메인 컴포넌트들은 실제로 코드로는 어떻게 구현이 되어있을까요?

// RankingView.swift

var body: some View {
VStack(spacing: 0) {
RankingTopBar()
Spacer().frame(height: 12)
RankingMainTab()
RankingPager(content: { index in
if index == 0 {
UserRankingView()
}
else if index == 1 {
ExamRankingView()
}
})
}
}

꽤나 간단합니다. 실제 UI와 상당히 닮아있는 코드를 확인하실 수 있습니다.단순히 사용하고 있는 도메인 컴포넌트를 호출해주거나 Spacer() 로 컴포넌트간의 간격만 맞춰주고 있습니다. 컴포넌트들은 ViewBuilder로 상단 레이어의 이벤트를 받아서 처리를 해주는 식이죠.

이렇게 나눠진 도메인 컴포넌트들은 각각의 디자인 시스템을 이용해서 구현이 됩니다.

// UserRankingItemView

VStack(spacing: 0){
ZStack {
HStack {
QuackSubtitle1(text: "\(index+1)등")
Spacer()
QuackBody2(text: "\(user.duckPower.tier) | \(user.duckPower.tag.name)", color: Color.Gray1)
}.padding(.horizontal, QuackCommonPadding)

HStack(spacing: RankingUserProfileDiff {
UserProfileView(
user: user,
onClick: onClickUser,
size: LargeUserProfileSize
)
QuackTitle2(text: user.nickName)
}
.frame(maxWidth: .infinity, alignment: .leading)
.padding(.leading, RankingProfileLeadingPadding)
.padding(.vertical, RankingItemSpace)
}
QuackDivider()
}.background(Color.White)

UserRankingItemView 에서는 UserProfileView 라는 또 다른 도메인 컴포넌트를 사용하기도 하고 QuackSubtitle1 , QuackTitle2 등 디자인 시스템을 사용하여 화면을 구성하기도 합니다.

즉, 하나의 화면은 여러 개의 도메인 컴포넌트로 구성이 되어있고, 각 도메인 컴포넌트는 디자인 시스템의 컴포넌트로 구성이 되어있는 구조입니다. 이렇게 되면 결국엔 가장 상위의 UI레이어에서는 도메인 컴포넌트만 참조할뿐 디자인 시스템에 대한 지식이 아예 없는 구조가 됩니다.

이는 완벽한 관심사의 분리를 가져다줄 수 있게 되는 근간이 되는 셈이죠. 관심사의 분리는 각자의 역할을 정확하게 나눠줄 수 있으므로 단일책임원칙을 UI코드에서도 지키게 됩니다.

한치의 오차도 없이

그렇다면 디자인 시스템이 실제로 제품에 영향을 끼치는 장점은 어떤게 있을까요? 여러 가지 있겠지만, 모든 시스템 컴포넌트들의 유지보수를 한 곳에서 관리한다는데 가장 큰 장점이 있습니다. 이는 디자이너가 만들어준 디자인을 한치의 오차도 없이 구현할 수 있는 시도를 해볼 수 있는데요.

디자이너들은 1px에 어색함을 느끼고 RGB hex값이 조금만 틀려도 정확히 잡아내곤 합니다. 그렇게 정성스럽고 정밀하게 디자인해준 시안을 정확히 구현하려고 노력하는 것이 클라이언트 개발자의 기본 소양이 아닐까 생각합니다.

디자이너가 디자인 검수를 할 때, 디자인 시안과 개발된 제품을 겹쳐서 비교하면 꽤나 일치하지 않는 것을 경험해본 적이 있으실겁니다.

명예의 전당 TopBar
홈 TopBar
프로필 화면 TopBar

위의 이미지들은 iOS로 구현한 각 화면들의 TopBar입니다.

피그마 겹치기를 통해 디자인 검수를 제가 직접 해본 결과인데, 처참합니다. 실제 디자인 시안과 제대로 맞는 화면이 없습니다. 제대로 맞추기 위해서는 각 TopBar의 컴포넌트 간격이 제대로 맞는지 확인을 해야합니다.

DuckieProfileTopBar 라고 만들어놨던 컴포넌트의 수정을 우선적으로 해야하겠죠. 컴포넌트간의 간격이나 padding값이 조금 잘못 들어갔던 것을 확인해서 고쳤습니다.

컴포넌트 자체를 수정한 결과

하지만 아직도 제대로 맞지 않는데요.

확인해보니 디자인 시스템단에서 텍스트가 폰트와 크기 등의 값들은 제대로 적용이 되어있지만 행간, 자간 등 디테일한 텍스트 스펙이 제대로 적용이 되어있지 않았습니다. 디테일한 스펙을 맞춰볼까요?

디자인 시스템을 수정한 결과

그런데 자간,행간을 맞추어도 겹치기는 계속 맞고 있지 않았습니다. 이는 피그마와 SwiftUI가 Typography에 대한 스펙을 정의할 때 단위가 픽셀포인트로 서로 다른 방식을 사용했기 때문에 발생하는 문제인데요. 그렇기 때문에 피그마에 적혀져 있는 스펙 그대로 적용을 한다고 해도 미세하게 깨지게 되었습니다.

이를 px에서 포인트로 변환해준다면 (사실 간단하진 않습니다) 아주 깔끔하게 겹치기에 성공한 것을 볼 수 있습니다.

단위 변환후에 두 이미지 겹치기

다른 컴포넌트들은 어떻게 됐을까요? 아까 잘못 설정되어있던 TopBar의 공통 패딩을 고쳐주고, Typography의 스펙이 수정되었기 때문에 컴포넌트간의 간격만 조금 잡아준다면 큰 문제는 없을것으로 보입니다.

개선된 홈 TopBar
개선된 명예의전당 TopBar

와우 너무 깔끔하게 겹쳐졌네요.

깔끔하게 겹치기에 성공한 것을 확인할 수 있었습니다. 모든 화면에 TopBar 기준으로 겹치기에 성공하는데 2시간이 걸리지 않았었는데요. 이제 Headline1 텍스트를 사용하는 곳에서는 모두 정확한 타이포그래피를 구현할 수 있습니다.

각 화면마다 다른 Text 컴포넌트를 사용해 속성을 바꿔가면서 코드를 짰다면, 서비스의 모든 텍스트를 고치는게 엄두가 나지 않았을 것 같습니다. 하지만 디자인 시스템을 적용했었기에 하나의 화면만 수정을 해줘도 나머지 화면까지 정확한 디자인을 구현할 수 있었습니다.

마치며

디자인 시스템은 부분적으로나마 다들 사용하고 계시는 개념일거라고 생각이 드는데요. 이런 UI 컴포넌트들을 싹 정리해서 시스템으로 적용할 수 있다면 추후에 유지보수에 매우 용이한 코드를 작성하실 수 있습니다.

Duckie에서는 위와 같이 디자인 시스템을 이용하여 개발된 iOS 제품입니다.한 번 앱을 사용해보시는것도..?좋겠네요!

감사합니다.

--

--

goddoro
goddoro

Written by goddoro

TVING에서 동영상 플레이어를 개발하고 있습니다

No responses yet