🤖 이 글은 Claude Code(AI)가 작성합니다. | 시리즈 목차 | 이전: [18편] 리스트·내비게이션·시트
재사용 가능한 뷰 컴포넌트
앱을 만들다 보면 같은 스타일의 버튼이나 카드 UI를 여러 곳에서 사용하게 됩니다. 매번 같은 수정자를 반복 작성하는 대신, 재사용 가능한 커스텀 뷰를 만들면 코드가 훨씬 깔끔해집니다.
커스텀 뷰 컴포넌트 만들기
기본 패턴
// 재사용 가능한 카드 뷰
struct CardView: View {
let title: String
let subtitle: String
let color: Color
var body: some View {
VStack(alignment: .leading, spacing: 8) {
Text(title)
.font(.headline)
.foregroundColor(.white)
Text(subtitle)
.font(.subheadline)
.foregroundColor(.white.opacity(0.8))
}
.padding()
.frame(maxWidth: .infinity, alignment: .leading)
.background(color)
.cornerRadius(12)
}
}
// 사용 예시
struct ContentView: View {
var body: some View {
VStack(spacing: 12) {
CardView(title: "Swift", subtitle: "Apple의 시스템 언어", color: .orange)
CardView(title: "SwiftUI", subtitle: "선언형 UI 프레임워크", color: .blue)
CardView(title: "Xcode", subtitle: "Apple 공식 IDE", color: .purple)
}
.padding()
}
}
뷰를 파라미터로 받기 (ViewBuilder)
내용이 다양한 컨테이너 뷰를 만들 때는 @ViewBuilder를 사용합니다.
struct PanelView<Content: View>: View {
let title: String
@ViewBuilder let content: Content
var body: some View {
VStack(alignment: .leading, spacing: 12) {
Text(title)
.font(.headline)
.padding(.horizontal)
.padding(.top)
Divider()
content
.padding([.horizontal, .bottom])
}
.background(Color(.systemBackground))
.cornerRadius(16)
.shadow(color: .black.opacity(0.08), radius: 8, y: 2)
}
}
// 사용 예시 — 어떤 뷰든 들어갈 수 있음
struct ExampleView: View {
var body: some View {
VStack(spacing: 16) {
PanelView(title: "프로필") {
HStack {
Image(systemName: "person.circle.fill")
.font(.largeTitle)
Text("홍길동")
.font(.title2)
}
}
PanelView(title: "통계") {
HStack(spacing: 24) {
StatItem(label: "게시글", value: 42)
StatItem(label: "팔로워", value: 1200)
StatItem(label: "팔로잉", value: 300)
}
}
}
.padding()
}
}
커스텀 수정자(ViewModifier)
자주 쓰는 수정자 조합을 하나로 묶을 수 있습니다.
struct CardBackground: ViewModifier {
var color: Color = .white
func body(content: Content) -> some View {
content
.padding()
.background(color)
.cornerRadius(12)
.shadow(color: .black.opacity(0.1), radius: 6, y: 2)
}
}
// View 확장으로 편하게 사용
extension View {
func cardStyle(color: Color = .white) -> some View {
modifier(CardBackground(color: color))
}
}
// 이제 이렇게 사용 가능
struct UsageView: View {
var body: some View {
VStack {
Text("안녕하세요")
.cardStyle()
Image(systemName: "star.fill")
.font(.largeTitle)
.cardStyle(color: Color.yellow.opacity(0.2))
}
}
}
애니메이션 기초
SwiftUI 애니메이션은 매우 간단합니다. 상태가 바뀔 때 withAnimation으로 감싸거나 .animation 수정자를 추가하면 됩니다.
기본 애니메이션
struct BasicAnimation: View {
@State private var isExpanded = false
var body: some View {
VStack(spacing: 20) {
RoundedRectangle(cornerRadius: 12)
.fill(Color.blue)
.frame(
width: isExpanded ? 300 : 100,
height: isExpanded ? 200 : 100
)
.animation(.spring(response: 0.4, dampingFraction: 0.6), value: isExpanded)
Button(isExpanded ? "줄이기" : "늘리기") {
isExpanded.toggle()
}
.buttonStyle(.borderedProminent)
}
}
}
.animation(_:value:) 수정자는 value가 바뀔 때마다 해당 수정자가 적용된 뷰를 애니메이션합니다.
주요 애니메이션 종류
// 선형 — 일정한 속도
.animation(.linear(duration: 0.3), value: isExpanded)
// 이징 — 천천히 시작해 빠르게 (기본)
.animation(.easeInOut(duration: 0.3), value: isExpanded)
// 스프링 — 탄성 있는 움직임
.animation(.spring(response: 0.4, dampingFraction: 0.7), value: isExpanded)
// 바운스 — iOS 17+
.animation(.bouncy, value: isExpanded)
// 딜레이
.animation(.easeOut.delay(0.1), value: isExpanded)
withAnimation
struct WithAnimationExample: View {
@State private var offset: CGFloat = 0
@State private var opacity = 1.0
var body: some View {
VStack(spacing: 30) {
Circle()
.fill(Color.orange)
.frame(width: 80)
.offset(x: offset)
.opacity(opacity)
Button("움직이기") {
withAnimation(.spring(response: 0.5, dampingFraction: 0.5)) {
offset = offset == 0 ? 100 : 0
opacity = opacity == 1.0 ? 0.3 : 1.0
}
}
.buttonStyle(.bordered)
}
}
}
withAnimation 클로저 안에서 상태를 바꾸면 그 상태 변화에 의한 UI 업데이트 전체에 애니메이션이 적용됩니다.
전환(Transition)
뷰가 나타나거나 사라질 때 적용할 전환 효과입니다.
struct TransitionExample: View {
@State private var showCard = false
var body: some View {
VStack(spacing: 20) {
Button(showCard ? "카드 숨기기" : "카드 보이기") {
withAnimation(.spring()) {
showCard.toggle()
}
}
.buttonStyle(.borderedProminent)
if showCard {
RoundedRectangle(cornerRadius: 16)
.fill(Color.blue.gradient)
.frame(height: 120)
.overlay(
Text("✨ 카드!")
.font(.title)
.foregroundColor(.white)
)
.transition(.asymmetric(
insertion: .move(edge: .top).combined(with: .opacity),
removal: .scale.combined(with: .opacity)
))
}
}
.padding()
}
}
주요 전환 종류
.opacity: 서서히 나타나고 사라짐.scale: 크기가 변하며 나타나고 사라짐.slide: 옆에서 밀려 들어옴.move(edge:): 특정 방향에서 이동.combined(with:): 두 전환 조합.asymmetric: 나타날 때와 사라질 때 다른 효과
matchedGeometryEffect — 히어로 애니메이션
두 뷰 사이에 자연스러운 전환을 만드는 강력한 도구입니다. 목록에서 상세 화면으로 확장되는 애니메이션을 구현할 때 유용합니다.
struct HeroAnimation: View {
@State private var isExpanded = false
@Namespace private var animation
var body: some View {
if isExpanded {
// 확장된 상태
VStack {
RoundedRectangle(cornerRadius: 20)
.fill(Color.blue.gradient)
.matchedGeometryEffect(id: "card", in: animation)
.frame(height: 300)
.onTapGesture {
withAnimation(.spring()) { isExpanded = false }
}
.overlay(
Text("상세 정보")
.font(.title)
.foregroundColor(.white)
)
}
.padding()
} else {
// 축소된 상태 (목록 아이템)
HStack {
RoundedRectangle(cornerRadius: 12)
.fill(Color.blue.gradient)
.matchedGeometryEffect(id: "card", in: animation)
.frame(width: 60, height: 60)
Text("탭해서 확장")
.font(.headline)
Spacer()
}
.padding()
.onTapGesture {
withAnimation(.spring()) { isExpanded = true }
}
}
}
}
같은 id와 namespace를 가진 두 뷰 사이를 SwiftUI가 자동으로 보간(interpolate)해 매끄러운 전환을 만들어줍니다.
그라디언트와 도형
struct ShapeExamples: View {
var body: some View {
VStack(spacing: 16) {
// 그라디언트
LinearGradient(
colors: [.blue, .purple],
startPoint: .topLeading,
endPoint: .bottomTrailing
)
.frame(height: 100)
.cornerRadius(16)
// 커스텀 도형
HStack(spacing: 12) {
Circle()
.fill(Color.orange.gradient)
.frame(width: 60)
RoundedRectangle(cornerRadius: 8)
.fill(Color.green.gradient)
.frame(width: 80, height: 60)
Capsule()
.fill(Color.pink.gradient)
.frame(width: 80, height: 40)
}
}
.padding()
}
}
커스텀 도형 (Shape 프로토콜)
struct Triangle: Shape {
func path(in rect: CGRect) -> Path {
var path = Path()
path.move(to: CGPoint(x: rect.midX, y: rect.minY))
path.addLine(to: CGPoint(x: rect.maxX, y: rect.maxY))
path.addLine(to: CGPoint(x: rect.minX, y: rect.maxY))
path.closeSubpath()
return path
}
}
// 사용
Triangle()
.fill(Color.teal.gradient)
.frame(width: 100, height: 80)
.animation(.spring(), value: someState)
Shape 프로토콜을 구현하면 SwiftUI의 모든 수정자와 애니메이션을 그대로 사용할 수 있습니다.
핵심 요약
- 커스텀 뷰:
View프로토콜을 채택한 구조체로 재사용 가능한 컴포넌트 제작 - @ViewBuilder: 뷰를 파라미터로 받는 제네릭 컨테이너 뷰 구현
- ViewModifier: 반복되는 수정자 묶음을 재사용 가능한 단위로 추출
- .animation(_:value:): 특정 값 변화에 애니메이션 적용
- withAnimation: 블록 안 상태 변화 전체에 애니메이션 적용
- .transition: 뷰가 나타나고 사라질 때의 효과
- matchedGeometryEffect: 두 뷰 간 히어로 전환 효과
- Shape 프로토콜: 커스텀 도형 제작, 애니메이션 지원
3부 SwiftUI가 완결됩니다. 다음 편부터는 4부 macOS 앱 개발로 넘어가 NSWindow, MenuBarExtra, AppKit 통합 등 macOS 특화 기능을 다룹니다.
🤖 Generated with Claude Code