# SwiftUI: iOS를 위한 모던 인터페이스 구축 가이드 > SwiftUI로 모던 UI를 구축하는 방법을 알아봅니다. 선언적 구문, 컴포넌트, 애니메이션, iOS 18 모범 사례를 종합적으로 다룹니다. - Published: 2026-01-05 - Updated: 2026-04-02 - Author: Anthony Fillion-Maillet - Tags: swiftui, ios, swift, ui, apple - Reading time: 12 min --- SwiftUI는 Apple 플랫폼의 인터페이스 개발 방식을 근본적으로 바꾸었습니다. 선언적 구문과 네이티브 통합을 통해 이전보다 훨씬 적은 코드로 세련된 애플리케이션을 구축할 수 있습니다. iOS 18에서는 성능의 대폭적인 향상과 새로운 기능이 도입되었습니다. > **Why SwiftUI in 2026?** > > SwiftUI는 iOS 18에서 완전한 성숙기에 접어들었습니다. 렌더링 최적화, 메모리 관리 개선, UIKit 통합 간소화가 이루어졌으며, 신규 iOS 애플리케이션 개발의 표준으로 자리 잡았습니다. ## 선언적 패러다임 이해하기 코드를 작성하기 전에 SwiftUI의 본질적인 특징을 파악해야 합니다. UIKit(기존 프레임워크)에서는 인터페이스를 단계별로 구축하도록 지시해야 했습니다. "레이블을 생성하고, 여기에 배치하고, 사용자가 클릭하면 색상을 변경하라"는 식입니다. 이것이 **명령형** 패러다임입니다. SwiftUI는 다른 접근 방식을 취합니다. 표시하고자 하는 **내용**을 기술하면 프레임워크가 나머지를 처리합니다. 내비게이션에서 경로를 하나씩 안내하는 것(명령형)과 목적지만 말하는 것(선언형)의 차이와 유사합니다. ### 첫 번째 View 만들기 SwiftUI에서 모든 인터페이스 요소는 `View`입니다. View는 화면에 표시될 내용을 기술하는 struct입니다. 제목, 부제목, 버튼이 포함된 첫 번째 화면을 만들어 보겠습니다. ```swift // ContentView.swift import SwiftUI // Each screen is a struct that implements the View protocol struct ContentView: View { // The "body" property describes what the view displays // "some View" means "a type of View, but Swift figures it out automatically" var body: some View { // VStack = "Vertical Stack": stacks elements vertically // spacing: 20 = 20 points of space between each element VStack(spacing: 20) { // A simple text with style modifiers Text("Welcome to SharpSkill") .font(.largeTitle) // Large title font .fontWeight(.bold) // Bold text Text("Prepare for your iOS interviews") .font(.subheadline) // Smaller font .foregroundColor(.secondary) // Gray secondary color // Button with an action (closure) and a label Button("Get Started") { print("Button tapped!") } .buttonStyle(.borderedProminent) // Filled blue button style } .padding() // Adds margins around the VStack } } ``` 이 구조를 살펴보면, 표시할 요소(텍스트, 버튼)를 선언하고, 세로로 배치하고(VStack), **모디파이어**(`.font()`, `.padding()`)로 스타일을 적용하고 있습니다. > **핵심 포인트**: SwiftUI에서는 UI 객체를 "생성"하는 것이 아니라, 원하는 인터페이스를 **기술**합니다. 실제 요소의 생성, 갱신, 소멸은 SwiftUI가 담당합니다. ### 모디파이어: View 변환하기 모디파이어는 View 뒤에 체이닝하여 변환을 적용하는 메서드입니다. Instagram 필터를 순서대로 적용하는 것과 비슷하다고 생각하면 됩니다. **순서가 중요합니다.** 각 모디파이어가 이전 View를 감싸는 새로운 View를 생성하기 때문입니다. 순서가 왜 중요한지 보여주는 예제를 살펴보겠습니다. ```swift // ModifiersOrder.swift // Example 1: padding THEN background Text("SwiftUI") .padding() // 1. Adds 16pt of space around the text .background(.blue) // 2. Blue background covers text + padding .foregroundColor(.white) // Result: white text on blue rectangle with margins // Example 2: background THEN padding (reversed order!) Text("SwiftUI") .background(.blue) // 1. Tight blue background around text only .padding() // 2. Transparent padding around blue background .foregroundColor(.white) // Result: white text on small blue rectangle, surrounded by empty space ``` 차이점은 명확합니다. 첫 번째 예제에서는 padding이 배경의 "안쪽"에 있고, 두 번째에서는 "바깥쪽"에 있습니다. 미묘한 차이이지만 레이아웃을 정확하게 제어하는 데 필수적인 지식입니다. ## Stack을 활용한 인터페이스 구성 SwiftUI는 View를 구성하기 위한 세 가지 주요 컨테이너를 제공합니다. 콘텐츠를 서로 다른 방식으로 배치하는 상자라고 생각하면 됩니다. ### VStack, HStack, ZStack - **VStack** (Vertical Stack): 요소를 위에서 아래로 쌓습니다 - **HStack** (Horizontal Stack): 요소를 왼쪽에서 오른쪽으로 나열합니다 - **ZStack** (Z축 Stack): 요소를 겹쳐서 배치합니다 세 가지 Stack을 모두 조합하여 사용자 프로필 카드를 구축해 보겠습니다. 사진 위에 인증 배지를 겹치고, 옆에 사용자 이름과 직책을 표시하는 것이 목표입니다. ```swift // ProfileCard.swift struct ProfileCard: View { var body: some View { // Main HStack: photo on left, info on right HStack(spacing: 16) { // ZStack to overlay the badge on the photo ZStack(alignment: .bottomTrailing) { // Profile image (circle) Image("avatar") .resizable() .frame(width: 60, height: 60) .clipShape(Circle()) // Green "verified" badge at bottom right Image(systemName: "checkmark.circle.fill") .foregroundColor(.green) .background(Circle().fill(.white)) // White circle behind } // VStack to stack name and role vertically VStack(alignment: .leading, spacing: 4) { Text("Marie Dupont") .font(.headline) Text("iOS Developer") .font(.subheadline) .foregroundColor(.secondary) } // Spacer pushes everything to the left Spacer() // Chevron on right indicates it's tappable Image(systemName: "chevron.right") .foregroundColor(.gray) } .padding() .background(Color(.systemGray6)) .cornerRadius(12) } } ``` 여기서 핵심은 `Spacer()`입니다. 사용 가능한 모든 공간을 차지하는 보이지 않는 요소입니다. Spacer가 없으면 요소들이 중앙에 배치되지만, Spacer를 사용하면 요소들이 왼쪽으로 밀려나고 셰브론이 오른쪽 끝에 고정됩니다. > **Xcode Tip** > > Xcode에서 임의의 View를 `⌘ + 클릭`하면 비주얼 인스펙터에 접근할 수 있습니다. 코드를 직접 입력하지 않고도 모디파이어를 추가할 수 있습니다. ## @State와 @Binding을 활용한 상태 관리 상태 관리는 SwiftUI의 핵심입니다. 상태란 변경될 수 있으며 인터페이스를 갱신해야 하는 데이터를 의미합니다. 상태가 변경되면 SwiftUI는 영향을 받는 View를 자동으로 재계산합니다. ### @State: View의 로컬 상태 `@State`는 프로퍼티 래퍼로, SwiftUI에 "이 변수를 감시하고, 변경되면 View를 갱신하십시오"라고 전달합니다. 단일 View의 로컬 상태에 적합합니다. 대화형 카운터를 만들어 이 메커니즘을 이해해 보겠습니다. ```swift // CounterView.swift struct CounterView: View { // @State creates a "source of truth" for this view // private because state shouldn't be modified from outside @State private var count = 0 var body: some View { VStack(spacing: 30) { // This Text updates automatically when count changes Text("\(count)") .font(.system(size: 72, weight: .bold)) HStack(spacing: 40) { // Decrement button Button(action: { count -= 1 // Modifies state → view refreshes }) { Image(systemName: "minus.circle.fill") .font(.largeTitle) } // Increment button Button(action: { count += 1 }) { Image(systemName: "plus.circle.fill") .font(.largeTitle) } } } } } ``` 버튼을 탭하면 `count`가 변경됩니다. SwiftUI는 이 변경을 감지하고 `body`를 다시 실행하여 화면을 갱신합니다. 레이블을 수동으로 업데이트할 필요가 없습니다. 모든 것이 자동으로 처리됩니다. ### @Binding: View 간 상태 공유하기 자식 View가 부모 View의 상태를 변경해야 하는 경우가 있습니다. 이때 `@Binding`이 필요합니다. 기존 `@State`에 대한 양방향 연결을 생성합니다. 구체적인 예제로, 실시간 유효성 검사가 포함된 사용자 이름 입력 필드를 살펴보겠습니다. ```swift // UsernameValidation.swift // Parent view: owns the state struct SignupForm: View { @State private var username = "" // Source of truth @State private var isValid = false // Validation state var body: some View { VStack(spacing: 20) { // We pass BINDINGS (with $) to the child view UsernameField(username: $username, isValid: $isValid) Button("Create Account") { // Submit the form } .disabled(!isValid) // Disabled if invalid .buttonStyle(.borderedProminent) } .padding() } } ``` 자식 View는 바인딩을 전달받아 변경할 수 있습니다. ```swift // Child view: receives and modifies state via @Binding struct UsernameField: View { @Binding var username: String // Connection to parent's @State @Binding var isValid: Bool var body: some View { VStack(alignment: .leading, spacing: 8) { TextField("Username", text: $username) .textFieldStyle(.roundedBorder) .onChange(of: username) { oldValue, newValue in // Validation: at least 3 characters isValid = newValue.count >= 3 } // Visual feedback HStack { Image(systemName: isValid ? "checkmark.circle" : "xmark.circle") Text("Minimum 3 characters") } .font(.caption) .foregroundColor(isValid ? .green : .red) } } } ``` 사용자가 TextField에 입력하면 `username`이 바인딩을 통해 변경됩니다. 부모 View는 이 변경 사항을 인식하고 활용할 수 있습니다. 깔끔한 양방향 통신이 구현된 것입니다. > **Warning** > > `@State`는 단순한 로컬 상태에만 사용해야 합니다. 여러 화면에 걸친 데이터나 복잡한 로직에는 `@Observable`(iOS 17 이상) 또는 MVVM 패턴을 사용하는 것을 권장합니다. ## 동적 리스트 표시하기 리스트는 모바일 앱 어디에나 존재합니다. SwiftUI는 네이티브 iOS 스타일링(구분선, 스와이프 액션 등)을 갖춘 데이터 컬렉션 표시를 위해 `List`를 제공합니다. ### 간단한 리스트 만들기 리스트를 표시하려면 데이터와 그것을 식별하는 방법 두 가지가 필요합니다. `Identifiable` 프로토콜을 통해 SwiftUI는 어떤 View가 어떤 데이터에 대응하는지 파악할 수 있습니다. 먼저 데이터 모델을 정의합니다. ```swift // Models/Interview.swift // Identifiable lets SwiftUI track each element struct Interview: Identifiable { let id = UUID() // Auto-generated unique identifier let technology: String let difficulty: String let questionCount: Int } ``` 이제 리스트를 생성합니다. `ForEach`로 데이터를 반복 처리하고 각 요소에 대한 행을 생성하는 방식입니다. ```swift // InterviewListView.swift struct InterviewListView: View { // Data to display (in reality, this would come from an API) @State private var interviews = [ Interview(technology: "iOS", difficulty: "Intermediate", questionCount: 25), Interview(technology: "Android", difficulty: "Advanced", questionCount: 30), Interview(technology: "React", difficulty: "Beginner", questionCount: 20) ] var body: some View { // NavigationStack enables the navigation bar NavigationStack { List { // ForEach iterates over each interview // Thanks to Identifiable, no need to specify id: ForEach(interviews) { interview in InterviewRow(interview: interview) } } .navigationTitle("My Interviews") } } } ``` 각 행의 View를 독립된 컴포넌트로 분리합니다. 가독성과 재사용성이 향상됩니다. ```swift // InterviewRow.swift struct InterviewRow: View { let interview: Interview var body: some View { HStack { // Left column: title and subtitle VStack(alignment: .leading, spacing: 4) { Text(interview.technology) .font(.headline) Text(interview.difficulty) .font(.subheadline) .foregroundColor(.secondary) } Spacer() // Badge with question count Text("\(interview.questionCount) Q") .font(.caption) .fontWeight(.medium) .padding(.horizontal, 8) .padding(.vertical, 4) .background(Color.blue.opacity(0.1)) .cornerRadius(8) } .padding(.vertical, 4) } } ``` `InterviewRow`를 독립된 struct로 분리함으로써 코드의 가독성과 재사용성이 크게 향상됩니다. 이것은 SwiftUI의 모범 사례입니다. ### 액션 추가: 삭제와 재정렬 iOS 리스트는 삭제(왼쪽 스와이프)와 재정렬(드래그 앤 드롭)을 기본적으로 지원합니다. SwiftUI에서는 `.onDelete`와 `.onMove`로 간단하게 구현할 수 있습니다. ```swift // InterviewListView.swift (enhanced version) struct InterviewListView: View { @State private var interviews = [/* ... data ... */] var body: some View { NavigationStack { List { ForEach(interviews) { interview in InterviewRow(interview: interview) } // Swipe to delete .onDelete(perform: deleteInterview) // Drag and drop to reorder .onMove(perform: moveInterview) } .navigationTitle("My Interviews") .toolbar { // "Edit" button that activates edit mode EditButton() } } } // Deletes elements at specified indices private func deleteInterview(at offsets: IndexSet) { interviews.remove(atOffsets: offsets) } // Moves elements from one position to another private func moveInterview(from source: IndexSet, to destination: Int) { interviews.move(fromOffsets: source, toOffset: destination) } } ``` 단 4줄의 추가 코드(`.onDelete`, `.onMove`, `EditButton`, 두 개의 함수)만으로 완전한 인터랙티브 리스트가 완성됩니다. SwiftUI의 진가가 여기서 드러납니다. ## 인터페이스 애니메이션 SwiftUI는 애니메이션에서 탁월한 성능을 발휘합니다. UIKit에서 대량의 코드가 필요했던 애니메이션도 여기서는 모두 선언적으로 기술할 수 있습니다. 최종 상태를 기술하면 SwiftUI가 전환을 처리합니다. ### withAnimation을 활용한 암시적 애니메이션 애니메이션을 적용하는 가장 간단한 방법은 상태 변경을 `withAnimation`으로 감싸는 것입니다. SwiftUI가 변경 사항을 감지하고 영향을 받는 시각적 속성을 자동으로 애니메이션 처리합니다. ```swift // AnimatedCard.swift struct AnimatedCard: View { @State private var isExpanded = false var body: some View { VStack { RoundedRectangle(cornerRadius: 20) .fill(.blue) // Dimensions change based on state .frame( width: isExpanded ? 300 : 150, height: isExpanded ? 200 : 100 ) Button(isExpanded ? "Collapse" : "Expand") { // withAnimation animates ALL visual changes // resulting from this state change withAnimation(.spring(response: 0.5, dampingFraction: 0.7)) { isExpanded.toggle() } } .padding(.top) } } } ``` 버튼을 탭하면 `isExpanded`가 토글됩니다. `withAnimation` 덕분에 사각형의 크기 변경이 스프링 효과로 애니메이션 처리됩니다. 무엇을 애니메이션할지 지정할 필요가 없습니다. SwiftUI가 자동으로 판단합니다. ### 트랜지션: 표시와 사라짐의 애니메이션 트랜지션은 View가 어떻게 나타나고 사라지는지를 정의합니다. 기본값은 페이드(`opacity`)이지만 맞춤 설정이 가능합니다. ```swift // TransitionDemo.swift struct TransitionDemo: View { @State private var showDetails = false var body: some View { VStack(spacing: 20) { Button("Show Details") { withAnimation(.easeInOut(duration: 0.3)) { showDetails.toggle() } } // This view appears/disappears with a transition if showDetails { DetailCard() // Asymmetric transition: different for entry and exit .transition( .asymmetric( insertion: .scale.combined(with: .opacity), // Entry: zoom + fade removal: .slide // Exit: slide ) ) } } } } struct DetailCard: View { var body: some View { VStack(alignment: .leading, spacing: 12) { Text("Interview Details") .font(.headline) Text("25 questions • 45 minutes") .foregroundColor(.secondary) } .padding() .background(Color(.systemGray6)) .cornerRadius(16) } } ``` 이 예제에서 카드는 중앙에서 확대(scale + opacity)되며 나타나고, 옆으로 슬라이드(slide)하며 사라집니다. 이러한 마이크로 애니메이션이 인터페이스에 생동감과 전문적인 느낌을 부여합니다. > **Performance** > > SwiftUI는 애니메이션을 자동으로 최적화합니다. 자연스러운 동작을 위해 `.spring()`을 사용하고, 사용자에게 불편을 주는 과도하게 긴 애니메이션(0.5초 이상)은 피해야 합니다. ## 비동기 데이터 로딩 실제 애플리케이션에서 데이터는 대부분 API에서 가져옵니다. Swift Concurrency(async/await)는 `.task` 모디파이어를 통해 SwiftUI와 원활하게 통합됩니다. ### Loading / Error / Success 패턴 API 데이터를 표시하기 위한 표준 패턴을 소개합니다. 로딩 중, 오류, 성공의 세 가지 상태를 처리합니다. ```swift // AsyncDataView.swift struct AsyncDataView: View { @State private var questions: [Question] = [] @State private var isLoading = true @State private var errorMessage: String? var body: some View { Group { if isLoading { // State: loading in progress ProgressView("Loading...") } else if let error = errorMessage { // State: error VStack(spacing: 16) { Image(systemName: "exclamationmark.triangle") .font(.largeTitle) .foregroundColor(.orange) Text(error) Button("Retry") { Task { await loadQuestions() } } } } else { // State: success, display data List(questions) { question in Text(question.title) } } } // .task runs automatically when the view appears .task { await loadQuestions() } // Pull-to-refresh .refreshable { await loadQuestions() } } private func loadQuestions() async { isLoading = true errorMessage = nil do { // Async API call questions = try await QuestionService.shared.fetchQuestions() } catch { errorMessage = "Unable to load questions" } isLoading = false } } ``` `.task` 모디파이어는 필수적입니다. View가 표시될 때 비동기 태스크를 실행하고, View가 사라지면 자동으로 취소합니다. 메모리 누수의 위험이 없습니다. ## 결론 SwiftUI는 모던 iOS 개발에서 필수적인 존재가 되었습니다. iOS 18을 통해 프로덕션 환경의 애플리케이션에 충분한 성숙도에 도달했습니다. ### 핵심 요약 - **선언적 패러다임**: 구축 방법이 아닌, 표시하고 싶은 내용을 기술한다 - **@State와 @Binding**: 상태를 리액티브하게 관리하고 View 간에 전달한다 - **Stack**: VStack, HStack, ZStack을 조합하여 유연한 레이아웃을 구축한다 - **List**: 최소한의 코드로 네이티브 인터랙션이 포함된 컬렉션을 표시한다 - **애니메이션**: `withAnimation`으로 부드러운 자동 전환을 구현한다 - **Async/await**: `.task`로 데이터를 로딩하고 로딩/에러 상태를 처리한다 ### 체크리스트 - 명령형(UIKit)과 선언형(SwiftUI)의 차이를 이해하고 있다 - 모디파이어와 적용 순서를 숙달하고 있다 - @State, @Binding, @Observable의 사용 구분을 파악하고 있다 - Stack을 활용한 레이아웃 구축이 가능하다 - 액션이 포함된 리스트(삭제, 이동)를 구현할 수 있다 - withAnimation을 활용한 상태 변경 애니메이션을 적용할 수 있다 SwiftUI는 Apple 생태계 전반에 걸쳐 세련된 애플리케이션을 구축할 수 있는 길을 열어줍니다. 가장 좋은 학습 방법은 실천입니다. 작은 개인 프로젝트를 만들고 이 글에서 소개한 각 개념을 직접 시험해 보시기 바랍니다. --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/ko/blog/ios/swiftui-building-modern-interfaces