[SwiftUI] SwiftUI스럽게 코드짜는 법, Progressive Disclosure
처음부터 복잡하지 않고, 필요할 때만 복잡하게 설계하기
[SwiftUI] SwiftUI스럽게 코드짜는 법, Progressive Disclosure
처음부터 복잡하지 않고, 필요할 때만 복잡하게 설계하기
안녕하세요 미니입니다 :)
여러분들은 혹시 Progressive Disclosure (점진적 공개) 원칙을 들어보신 적이 있으신가요?
제가 마지막 챌린지에 참여했던 왕꿈틀이 테크팀에서는 단순히 동작하는 코드를 넘어서, SwiftUI스럽게 코드를 짜는 것을 하나의 목표로 삼고 개발 협업을 진행했습니다.
그리고 그 과정에서 자연스럽게 하나의 그라운드 룰을 만들게 되었죠.
처음부터 복잡하지 않고, 필요할 때만 복잡하게 — Progressive Disclosure
이 그라운드 룰 기준의 중심에 있었던 개념이 바로 오늘 글에서 소개할 Apple이 SwiftUI API 디자인 가이드라인에서 강조하는 Progressive Disclosure (점진적 공개) 였습니다.

왕꿈틀이 테크 팀은 일곱가지 그라운드 룰 중 하나를 Progressive Disclosure로 설정하고 협업을 진행했습니다.
오늘 글에서는 이 원칙이 정확히 어떤 의미를 가지는지, 그리고 어떤 코드가 SwiftUI스러운 설계라고 할 수 있는지를 조금은 가볍게, 하지만 깊이 있게 풀어보고자 합니다.

뭐가 SwiftUI스러운건지. 딱 기다려.
참고로, 이 룰은 챌린지 담당 멘토였던 Luminouxx가 올려주신 <Progressive Disclosure을 지킨 SwiftUI Component 설계>라는 글에서 영감을 받아 시도해보게 되었구요.
추가적으로 Swift Hero 2024에서 PFCT iOS 팀이 소개해주신 내용과, WWDC22에서 소개된 세션 내용을 함께 참고해서 팀에 도입하는데 많은 도움을 얻게 되었습니다. 아래 함께 링크를 첨부할게요!
Progressive Disclosure 니가 뭔디?
Progressive Disclosure라는 용어는 사실 프로그래밍에서만 사용하는 개념은 아닙니다.
오히려 그 시작은 UX 디자인 쪽에 더 가깝다고 볼 수 있죠.
핵심 내용은 단순합니다.
모든 것을 한 번에 보여주려고 하지말자. 처음에는 꼭 필요한 것만 보여주고 / 사용자가 원할 때 조금씩 더 공개하자.
토글 버튼이나 펼침/접힘 UI를 떠올려보면 이해하기가 쉬울겁니다.
처음에는 보이지 않던 정보가 사용자의 특정 행동을 계기로 자연스럽게 나타나는 방식이죠.
여기서 중요한 점은 “숨겨져 있다가 갑툭튀로 훅 튀어나온다"가 아니라 “어떤 내 행동의 결과로 정보가 공개된다"는 인지적 흐름입니다.
만약 사용자가 화면에 처음 진입했을 때부터, 모든 설정 옵션과 상세 정보가 화면을 가득 채우고 있다면 그 자체가 부담이면서 동시에 무엇부터 봐야 할지 혼란스러워질 것입니다.
Progressive Disclosure는 이런 혼란을 줄이고, 가장 중요한 정보를 빠르게 찾을 수 있도록 도와주는 디자인 원칙이라고 이해하면 됩니다. 어렵지 않죠?

이 친구의 정식 이름은 Disclosure controls (출처: Apple HIG 공식 문서 일부 발췌)
제가 흥미로웠던 점은, 이 UX 디자인 원칙이 프로그래밍, 더 정확하게는 SwiftUI API 설계 철학으로까지 이어져서 사용된다는 점입니다.
Apple은 WWDC 세션에서 SwiftUI API 디자인의 핵심 목표 중 하나로 아래 문장을 강조합니다.
Make APIs feel great to use
API를 사용하는 시점에서 좋은 경험을 주는 것이 좋은 코드다.
즉, 좋은 SwiftUI 코드란 단순히 기능이 많은 코드가 아니라, 코드를 사용하는 개발자에게도 부담을 주지 않는 코드라는 의미입니다.
보통 일반적인 개발자들은 코드를 작성하는 공간, 즉 선언부 (Declaration site)의 관점에서 코드를 바라보고 설계를 시작하게 됩니다.
하지만 사실 이것보다 더 중요한 것은 호출부 (Call Site)입니다. 코드가 사용되는 그 순간의 관점에서 바라보는 것이, 코드를 쓰는 순간보다 더 중요하다는 뜻이죠.

선언부의 코드 또한 중요하지만, 그것보다 사용하는 부분을 더 고려해 설계해야합니다.
결국 요약해보면,
- 사용하는 시점 (Call site)에서는 읽기 쉽고 단순한 코드
- 그럼에도 불구하고, 다양한 상황에서도 확장 가능하도록 열려 있는 구조를 가진 코드
가 바로 SwiftUI팀이 지향하는 코드이자 우리 왕꿈틀이 팀이 추구해야할 코드 스타일이었던 것입니다. 계속 글을 이어가볼게요!
일단 교과서에서 나오는 얘기부터 들어봅시다
SwiftUI 팀이 Progressive Disclosure 원칙을 실제 코드에 적용하는 방법
자 이제 이론은 배웠고, 실제 코드에서 실천해볼 차례입니다.
저희 팀의 코드를 먼저 들어가보기 전에, 애플 SwiftUI 팀은 Progressive Disclosure 원칙을 실제 API 설계에 어떻게 녹여내고 있는지 간단하게 알아보도록 합시다.
WWDC22 — The craft of SwiftUI API design: Progressive disclosure 세션에서 소개된 네 가지 전략은 아래와 같아요! *이 전략을 하나씩 모두 알아야한다기 보다 ‘아 이런 방향이구나' 정도로 감만 잡아보면 좋을 것 같습니다😊
- Consider common use cases
- Provide intelligent defaults
- Optimize the call site
- Compose, don’t enumerate
간단하게 하나씩 설명해보죠!
1️⃣ 일단, 일반적인 사용 사례부터 먼저 생각합시다.
SwiftUI API들의 공통적인 특징을 살펴보면, 가장 많이 쓰일 케이스가 가장 간단한 형태로 제공된다는 점을 확인할 수 있습니다.
물론 커스텀할 상황이 발생하면, 그때 후행 클로저 블록을 열던지 / 파라미터를 추가하던지 등의 속성을 추가할 수는 있습니다만.
처음부터 복잡한 선택지를 들이밀지 않고, 가장 흔하고 일반적인 사용법을 먼저 고려합니다. 이게 Progressive Disclosure의 출발점이라 볼 수 있어요.

99% 경우에는 이 기본 형태로 충분하게 일반적인 UI를 구현할 수 있습니다.
2️⃣ 기본값은 똑똑하게 제공합시다.
SwiftUI에서는 기본적인 Text 코드 한 줄만으로도 생각보다 많은 것을 할 수 있습니다.
폰트, 색상, 로컬라이제이션, 다크 모드, 다이나믹 타입까지. 직접 쓴 코드는 하나도 없는데, 기본 동작이 이미 잘 돌아가고 있었다면. 이것 역시 “처음부터 복잡하지 않게" 만들기 위한 중요한 장치가 깔려 있었던 것입니다.
필요하다면 모든 동작을 직접 설정할 수 있지만, SwiftUI에서 대부분의 Common use case는 intelligent default value로 처리합니다.



이런 동작은 별도의 코드로 지정하지 않았는데도 불구하고, 잘 제공되는 기능이죠.
3️⃣ 호출부 (call site) 최적화하기
선언부 (Declaration site)보다 호출부 (Call site)를 중요하다 라고 말하는게 무슨 의미인지 알아볼 차례입니다. *우리가 실제로 가장 많이 읽고, 쓰고, 수정하는 코드는 선언부가 아니라 호출부라는 점!
아래 두 이미지를 비교해볼까요?
두 이미지 모두 Table을 화면에 그리기 위한 코드인데요.
왼쪽 코드는 정렬, 섹션, 상태 변경 로직까지 모든 맥락이 호출부 (call site)에 그대로 드러나 있습니다. 기능은 풍부하지만, 한 눈에 이 코드 / 이 테이블이 무엇을 보여주는지 파악하기는 쉽지 않을 것 같네요🥶
반면 오른쪽 코드는 이 테이블이 어떤 데이터를 다루고 있는지 / 어떤 정보가 중요한지를 읽는 순간 바로 이해할 수 있을 것 같아요😊


Progressive disclosure가 적용되기 전과 후의 코드 양을 비교해보니 .. 어떤가요?
SwiftUI 팀이 말하는 <호출부를 최적화하라>라는 말은 기능을 줄이라는 뜻이 아닙니다.
그것보다,
이 코드의 각 한 줄 한 줄은 정말 지금 이 시점에 필요한 정보가 맞는가?
라는 질문을 던져보고 답하라는 것과 같은 뜻입니다. 그러면서 동시에 호출부에 있는 모든 글자가 “왜 여기 있는지" 설명할 수 있도록 구성하는 뜻이기도 할 것 같아요.
4️⃣ 열거(enum)하지 않고, 조합(compose)하자.
- 열거(enum)는 “가능한 경우의 수를 미리 모두 나열해두는 방식"을 의미합니다.
- 조합(compose)은 “작은 요소들을 조립해서 필요한 결과를 만드는 방식"이죠.
예시로 아래 HStack 코드를 보겠습니다.
HStack에서 발생할 수 있는 정렬 상황을 아래와 같이 enum으로 표현하면 어떤 문제가 생길까요?
enum Arrangement {
case leading
case trailing
case centered
case spacedEvenly
case spaceInBetweenElements
case spaceBeforeLastElement
}
API가 일단 세부적인 하나하나를 모두 쓰다보니 코드가 무거워질겁니다.
모든 경우의 수를 처음부터 예측해서 만들어야 하고, 만약 새로운 요구사항 하나가 생긴다면, 그때마다 case를 추가해줘야 할겁니다. 유지보수가 귀찮아지는거죠!
그래서 이 정렬을 하나의 enum이 아니라 Spacer, alignment, spacing과 같은 작은 구성 요소로 만들어 개발자가 조합해서 사용할 수 있도록 만든 것이 이 부분에서 소개하는 내용입니다.


처음에는 단순하게 시작했던 Progressive Disclosure 원칙에서, 복잡함이 필요하다면 조합(compose)을 통해 점진적으로 드러날 수 있도록 하라는 내용이 추가가 되었군요!
이제 교과서에서 들려주는 얘기는 끝났습니다!
본격적으로 우리 왕꿈틀이 팀의 코드 이야기로 넘어가보죠🧑🏻💻
이제, 왕꿈틀이 코드
1️⃣ 디자인 요구사항과 테크팀이 던진 질문들
우리 왕꿈틀이 팀이 진행했던 <수사24> 프로젝트는 하나의 디자인 컨셉 안에서 10종 이상의 버튼 타입을 다뤄야 했습니다.
지도를 핵심 기능으로 가져가고, Liquid Glass를 도입하다보니. 이런저런 것들이 많아지더라구요 ~.~
아무튼 각 버튼은 서로 다른 목적에서 필요하지만, 시각적으로는 하나의 시스템 (같은 Radius, animation, glass effect 등)처럼 컴포넌트로써 활용할 수 있도록 만들어야했습니다.
또한 프로젝트 초반이기 때문에 “이 버튼 스타일 조금만 어찌저찌 바꿔주세요"와 같은 요청에 유연하게 대처할 수 있도록 구성할 필요도 있었구요.

수사24에서 사용하는 Button Component 종류는 이렇습니다.
그래서 테크팀에서는 출발점으로, 버튼 컴포넌트 설계를 위해 아주 단순한 두 가지 질문과 답을 던져봤습니다.
What are the common use cases?
앞에서 이야기했던 <4. Compose, don’t enumerate> 원칙을 그대로 적용해보기로 했습니다.
모든 버튼을 각각의 타입으로 나누기 전에, “표현 방식만 놓고 보면 공통된 패턴이 있지 않을까?” 를 먼저 생각해봤죠. 그리고 아래 네 가지 레이아웃 조합 안에서 충분히 커버할 수 있겠다는 판단을 했습니다.
- 아이콘만 있는 버튼
- 텍스트만 있는 버튼
- 아이콘 + 텍스트 조합 버튼 (아이콘이 왼쪽에 있는 경우)
- 텍스트 + 아이콘 조합 버튼 (아이콘이 오른쪽에 있는 경우)
이 조합을 enum으로 정의하고, 앞으로의 버튼 UI 설계 전반에서 기준점으로 사용하도록 만들었습니다.
enum DWButtonLayout {
case iconOnly
case titleOnly
case iconLeading
case iconTrailing
}
What is the essential information?
그 다음 질문은 “버튼이 동작하기 위해 정말 필요한 최소 정보가 무엇인가?” 였습니다.
테크팀은 버튼의 핵심 정보를 세 가지로 정의했습니다.
- layout : 버튼이 어떤 형태로 보일지 [이미 위에서 정의했습니다!]
- content : 무엇을 보여줄지 [버튼의 시각적 요소로, layout에 따라 필수값이 달라지도록 만들겁니다!]
- action : 버튼을 눌렀을 때 무엇을 할지 [버튼의 기본이겠쥬?]
content 부분만 따로 추가적인 설명을 하자면, 버튼이 보여주는 (표현할, 시각적인 요소) 콘텐츠를 담당하는 작은 struct 모델을 의미합니다.
즉, “이 버튼에 아이콘이 있는지" 혹은 “텍스트가 있는지"만을 표현하기 위한 구조죠.
특히 아이콘과 타이틀을 모두 Optional로 두어, 어떤 레이아웃 (위에서 정의했던 DWButtonLayout enum형)을 선택하느냐에 따라 / 달라지는 필수값도 대응할 수 있도록 했습니다.
이렇게 content를 분리해두니, 레이아웃과 표현 책임을 자연스럽게 분리할 수 있었습니다.
struct DWButtonContent {
let image: Image?
let title: String?
}
다시 정리해보면,
- layout, content, action → 버튼을 만들기 위해 반드시 필요한 정보
- 사이즈, 색상, 폰트, 패딩, 인터랙션 효과 등 → 기본값을 가지고, 필요할 때만 열어줄 정보
이 구조가 Progressive Disclosure 원칙과 가장 잘 맞는다고 판단했습니다.
2️⃣ 코드 구조 설계하기 — Declaration Site
이제 이를 바탕으로 버튼의 선언부를 아래와 같은 형태로 만들었습니다.
중요한 점은, 이 구조를 버튼을 사용하는 입장에서는 모두 알 필요는 없다는 것입니다.
개발자는 layout, content, action 이 세 가지만으로 버튼을 만들 수 있고, 나머지 스타일 속성은 모두 기본값(Intelligent Defaults) 을 가지도록 했습니다.
struct DWButton: View {
/// 버튼 컨텐츠 (아이콘/타이틀)
let content: DWButtonContent
/// 버튼 레이아웃
let layout: DWButtonLayout
/// 버튼 탭했을 때 실행되는 액션입니다.
let action: () -> Void
/// 버튼의 크기입니다 (원형 버튼일 때 width, height 동일).
/// 텍스트만 포함된 레이아웃에서는 height로 사용됩니다.
var size: CGFloat = 44
/// 아이콘의 크기입니다.
var iconSize: CGFloat = 18
/// 아이콘의 너비입니다. 지정하지 않으면 iconSize를 사용합니다.
var iconWidth: CGFloat?
/// 아이콘의 높이입니다. 지정하지 않으면 iconSize를 사용합니다.
var iconHeight: CGFloat?
/// 아이콘/타이틀 공통 전경 색상입니다.
var foregroundColor: Color = .labelNeutral
/// 버튼 배경 색상입니다.
var backgroundColor: Color = .clear
/// 버튼 타이틀에 적용되는 폰트입니다.
var font: Font = .bodyMedium16
/// 인터랙션(떠오르는) 효과 사용 여부입니다.
var isInteractiveEffect: Bool = true
/// 버튼을 탭했을 때 발생하는 햅틱 피드백 여부입니다.
var isHapticFeedback: Bool = true
}
커스텀 속성 지정이 필요한 경우에 대해서만 Builder Pattern을 활용한 체이닝 메서드로 속성을 덮어씌울 수 있습니다.
잠깐 Chaining Modifer Method란?
체이닝(Chaining)이라는 이름처럼, 하나의 뷰를 만들고 끝내는 게 아니라 / 필요한 설정을 “한 줄씩 이어 붙이듯 추가하는 방식”입니다.
이 방식의 핵심은 간단합니다.
처음에는 고민 없이 쓰고, 필요해지면 한 줄씩 추가한다.
즉, 이 패턴은 옵션을 많이 열어두기보다, 스타일을 한 번에 모두 드러내지 않고 / 필요한 경우에만 점진적으로 확장할 수 있도록 하는 것이 목적이었습니다.
그리고 SwiftUI의 modifier들이 그렇듯, 각 메서드는 내부 값을 직접 바꾸는 게 아니라 설정이 반영된 ‘새 View’를 반환합니다.
그래서 call site에서는 바뀐 것만 눈에 띄고, 코드도 자연스럽게 “의도” 중심으로 읽히게 되죠.
extension DWButton {
/// 버튼의 크기를 설정합니다.
/// - Parameter size: 원형 버튼의 지름 혹은 텍스트 버튼의 높이
@discardableResult
func setupSize(_ size: CGFloat) -> Self {
var view = self; view.size = size; return view
}
/// 아이콘의 크기를 설정합니다.
/// - Parameter size: 아이콘의 width, height 값
@discardableResult
func setupIconSize(_ size: CGFloat) -> Self {
var view = self; view.iconSize = size; return view
}
/// 아이콘의 너비와 높이를 설정합니다.
/// - Parameters:
/// - width: 아이콘의 너비
/// - height: 아이콘의 높이
@discardableResult
func setupIconSize(width: CGFloat, height: CGFloat) -> Self {
var view = self; view.iconWidth = width; view.iconHeight = height; return view
}
/// 버튼에서 사용하는 공통 색상을 정의합니다.
func setupForegroundColor(color: Color) -> Self { ... }
/// 버튼에서 사용하는 공통 색상을 정의합니다.
func setupBackgroundColor(color: Color) -> Self { ... }
/// 버튼에서 사용하는 공통 색상을 정의합니다.
func setupFont(font: Font) -> Self { ... }
/// 인터랙션 효과를 설정합니다.
func setupInteractiveEffect(isEnabled: Bool) -> Self { ... }
/// 햅틱 피드백을 설정합니다.
func setupHapticFeedback(isEnabled: Bool) -> Self { ... }
}
버튼의 실제 동작은 body 부분에서 한 곳에 모아 처리합니다.
body는 버튼의 공통 동작(햅틱/액션/GlassEffect)을 한 곳에 모으고,
버튼 내부 레이아웃(아이콘/텍스트 조합)은 labelContent에서 layout 값에 따라 분기해 조립합니다.
var body: some View {
Button {
if isHapticFeedback {
triggerMediumHapticFeedback()
}
action()
} label: {
labelContent
}
.glassEffect(.regular.tint(backgroundColor))
.glassEffect(
isInteractiveEffect
? .regular.interactive()
: .regular.tint(.primaryLight1)
)
}
3️⃣ 잘못된 호출을 막기 위해 — Factory Method Pattern
마지막으로 고민했던 건 “이 버튼을 어떻게 생성하게 할 것인가?” 였습니다.
저는 버튼을 직접 init으로 만들지 않도록 의도했습니다. 대신, 레이아웃별 팩토리 메서드를 제공했죠. 모든 버튼은 버튼 공장🏭에서만 만들어집니다.
이 구조 덕분에,
- 레이아웃에 맞는 필수값이 타입 레벨에서 강제되고
- 잘못된 조합은 애초에 만들 수 없으며
- call site에서 버튼의 의도가 그대로 드러나게 됩니다.
“아, 이건 아이콘만 있는 뒤로가기 버튼이구나.”
설명 없이도 읽힐 수 있는 코드를 만드는 것이 제가 팩토리 메서드를 선택한 이유였습니다.
extension DWButton{
/// 아이콘만 있는 버튼
static func iconOnly(
image: Image,
action: @escaping () -> Void
) -> Self {
Self(
content: DWButtonContent(image: image, title: nil),
layout: .iconOnly,
action: action
)
}
/// 타이틀만 있는 버튼
static func titleOnly(
title: String,
action: @escaping () -> Void
) -> Self {
Self(
content: DWButtonContent(image: nil, title: title),
layout: .titleOnly,
action: action
)
}
/// 왼쪽 아이콘 + 오른쪽 타이틀
static func iconLeading(
image: Image,
title: String,
action: @escaping () -> Void
) -> Self {
Self(
content: DWButtonContent(image: image, title: title),
layout: .iconLeading,
action: action
)
}
/// 왼쪽 타이틀 + 오른쪽 아이콘
static func iconTrailing(
title: String,
image: Image,
action: @escaping () -> Void
) -> Self {
Self(
content: DWButtonContent(image: image, title: title),
layout: .iconTrailing,
action: action
)
}
}
여기까지가 버튼 컴포넌트를 구현한 전체 코드입니다! 어떤가요? 이해가 잘되시는지요🥶
잠깐 ! 내 머릿속에 들었던 의문점 몇 가지
🧐 왜 init 파라미터에 정의하지 않고 체이닝 스타일을 활용한 것일까?
가장 처음 떠오르는 선택지는 모든 스타일 옵션을 init 파라미터로 받는 방식이었습니다.
아래와 같이요.
DWButton(
content: ...,
layout: ...,
action: ...,
size: 48,
iconSize: 18,
foregroundColor: .white,
backgroundColor: .primaryNormal,
font: ...
)
하지만 이 방식에는 치명적인 문제가 있습니다.
버튼을 “쓰는 순간”부터 너무 많은 결정을 요구한다는 점이었죠.
이런 패턴이 위에서 설명한 Progressive Disclosure 원칙과 정반대되는 레거시 코드라고 볼 수 있습니다.
🧐 그렇다면 init 파라미터의 default parameter를 지정하는건 어때
이 방식은 어떨까요.
Swift에서는 생성자에 기본값을 지정할 수 있으니, 버튼을 만들 때 필요한 파라미터 값만 넘겨주고 / 나머지는 기본값에 의존하는 방식입니다.
init(
content: DWButtonContent,
layout: DWButtonLayout,
action: @escaping () -> Void,
size: CGFloat = 44,
iconSize: CGFloat = 18,
foregroundColor: Color = .labelNeutral,
backgroundColor: Color = .clear,
font: Font = .bodyMedium16,
isInteractiveEffect: Bool = true,
isHapticFeedback: Bool = true
)
하지만 이 방식에는 아쉬운 점이 분명이 존재합니다.
Swift의 struct는 모든 프로퍼티에 기본값이 지정되어 있거나, 외부에서 주입받아야 할 값이 명확하면 컴파일러가 자동으로 initializer를 만들어주는 기능이 있는데요.
모든 스타일 옵션을 init 파라미터로 직접 정의한다는 말은,
- 컴파일러가 기본으로 제공해주는 default initializer를 포기하게 되고
- 동시에 생성자 시그니처가 곧 가능한 옵션의 나열로 활용하는 셈이 되는 것입니다.
즉 이 순간부터
init은 더 이상 ‘만들기 위한 최소 조건’이 아니라, 컴포넌트의 모든 확장 가능성을 한 번에 드러내는 장소가 되버리는 것이죠.
이건 SwiftUI가 지향하는 API 디자인, 즉 우리가 이야기하고 있던 Progressive Disclosure 원칙과도 거리가 있는 모습이기에..
프로퍼티 기본값과 + Chaining Modifer Method 방식을 결합해서 사용하는 것이 더 적합하다고 생각이 듭니다.
🧐 체이닝 메서드의 생성이 너무 번거로운거 아니야?
맞습니다.
솔직히 말하면, 구현하는 입장에서는 체이닝 메서드를 일일이 만드는게 더 번거로운 것 같습니다.
그럼에도 불구하고, 이 수고는 ‘선언부’에서 한 번만 치르면 / ‘호출부’에서는 계속해서 이득을 본다는 점에서 분명한 장점을 갖고 있는 것 같습니다.
결국, 이 선택은 트레이드오프였습니다
이 설계는 분명히 말해 “가장 구현이 쉬운 방법”은 아닙니다. 선언부는 복잡해지고 / 처음 구조를 이해하는 데는 설명이 필요해지는 것은 분명하니까요.
하지만 그 대신, call site는 단순해지고 / 버튼의 의도는 코드에 그대로 드러나며 / Progressive Disclosure 원칙을 실제 코드로 체감할 수 있었죠.
그래서 이 선택은 “정답”이라기보다는, “SwiftUI스럽게 개발해보고 싶었던 팀의 선택"이라고 표현하는 게 더 정확할 것 같습니다.
실제 화면에서 사용하는 방법
이제 이 버튼 컴포넌트가 실제 화면(call site)에서는 어떤 모습으로 사용되었는지 살펴볼 차례입니다.
수사24의 화면 코드에서 버튼을 사용하는 방식은 대체로 아래와 같은 형태였습니다.
이 코드를 처음 봤을 때 버튼의 역할을 이해하는 데 설명이 필요할까요?
- 아이콘이 왼쪽에 있고
- “사건 추가”라는 텍스트가 있으며
- 어떤 액션이 실행되는지까지
call site만 읽어도 버튼의 의도가 바로 드러나는 것을 확인할 수 있죠! 아주 명확한 장점이 생긴 셈입니다.

버튼의 확장이 필요할 때마다 체이닝 메서드를 추가하면 됩니다!
그래서 결론!
위에서도 말했던 것처럼 이 버튼 컴포넌트 설계는 “완벽한 정답”이라기보다, SwiftUI가 지향하는 API 디자인 원칙을 팀 코드에 실제로 적용해본 하나의 시도였습니다.
- 처음에는 단순하게 시작하고
- 필요할 때만 점진적으로 확장하며, call site에서는 읽히는 코드를 만들고
- 선언부에서는 책임을 명확히 나누는 것
Progressive Disclosure는 단순한 이론이 아니라, 코드를 쓰는 사람의 경험을 바꾸는 설계 원칙이라는 걸 이번 프로젝트 컴포넌트에서 적용을 해보며 체감할 수 있었습니다.
혹시 지금 여러분들의 프로젝트에서도, 컴포넌트가 점점 무거워지고 있다는 느낌이 든다면 “지금 이 복잡함을, 조금 더 늦출 수는 없을까?”를 한 번쯤 고민해보셔도 좋을 것 같군요!
오늘 글은 여기까지입니다! 끝까지 읽어주셔서 감사합니다🙇🏻
메타데이터
- post_id
- c82763ffeb95
- slug
- swiftui-swiftui스럽게-코드짜는-법-progressive-disclosure-c82763ffeb95
- url
- https://medium.com/%EC%99%95%EA%BF%88%ED%8B%80%EC%9D%B4-dreamworms/swiftui-swiftui%EC%8A%A4%EB%9F%BD%EA%B2%8C-%EC%BD%94%EB%93%9C%EC%A7%9C%EB%8A%94-%EB%B2%95-progressive-disclosure-c82763ffeb95
- canonical_url
- https://medium.com/%EC%99%95%EA%BF%88%ED%8B%80%EC%9D%B4-dreamworms/swiftui-swiftui%EC%8A%A4%EB%9F%BD%EA%B2%8C-%EC%BD%94%EB%93%9C%EC%A7%9C%EB%8A%94-%EB%B2%95-progressive-disclosure-c82763ffeb95
- author_url
- https://medium.com/@mini-min
- status
- ok
- fetched_at
- 2026-06-24 18:57:25