비전OS 앱 개발의 핵심을 이루는 두 가지 중요한 개념은 바로 Swift와 SwiftUI입니다.
Swift는 빠르고 안정적이며 현대적인 프로그래밍 언어로, 복잡한 문제를 단순하고 편리하게 해결합니다. SwiftUI는 그 위에 얹어진 강력한 UI 프레임워크로, 한 번의 코드로 다양한 애플 플랫폼에서 일관된 사용자 경험을 구현할 수 있도록 돕습니다.
Swift
Swift의 주요 특징으로는 안정성(Safety), 성능(Performance), 현대적인 프로그래밍 패러다임(Modern Programming Paradigms) 세 가지가 핵심으로 강조됩니다.
1. 안정성(Safety)
A. 타입 안전성
먼저 Swift의 가장 큰 장점은 안전하게 코딩을 할 수 있다는 것입니다. 다른 언어들을 사용하다 보면 종종 겪게 되는 "런타임 에러" 같은 상황을 많이 줄여줍니다.
예를 들어, 타입 안전성 덕분에 실수로 문자열을 숫자처럼 다루려고 하면 컴파일러가 바로 알려줍니다.
JavaScript로 개발할 때는 이렇나 경험을 하셨을 수도 있습니다.
let number = "123" + 456 // "123456" 의도와 다른 결과
하지만 Swift에서는 이런 실수를 완전 차단해 줍니다.
let number: Int = "123" + 456 // 컴파일 에러!
컴파일러가 문자열과 숫자는 더할 수 없다는 것을 바로 알려주기에 실수를 미리 잡을 수 있습니다.
B. 옵셔널
그리고 특히, 옵셔널은 nil 때문에 앱이 갑자기 죽어버리는 상황을 방지할 수 있습니다. Swift는 "이 값은 없을 수도 있어요"라고 미리 말해줄 수 있기에 그런 문제를 많이 예방할 수 있습니다.
옵셔널을 사용하지 않는 경우
let userInput = "안녕하세요"
let number = Int(userInput) // nil이 될 수 있음
let result = number + 10 // 크래시 발생
Int(userInput)은 문자열을 정수로 변환하는 메서드로, 변환에 실패할 경우 nil을 반환합니다.
여기서 number는 nil일 수 있지만 옵셔널로 처리하지 않고 바로 연산을 수행하려 하면 런타임에서 크래시가 발생합니다. (number가 nil이므로 덧셈 연산이 불가능합니다.)
옵셔널을 사용하는 경우
if let number = Int(userInput) {
let result = number + 10 // 안전하게 처리
} else {
print("숫자를 입력해주세요!")
}
if let 구문은 옵셔널 바인딩을 통해 number가 nil이 아닐 경우에만 코드를 실행하도록 합니다.
변환에 실패한 경우 else 블록에서 적절한 처리를 할 수 있어 런타임 크래시를 방지합니다.
C. 자동 메모리 관리 (ARC)
Swift는 ARC(Automatic Reference Counting)를 통해 객체의 생성과 해제를 자동으로 관리합니다. 이를 통해 메모리 누수 문제를 최소화하며, 개발자가 메모리 할당과 해제에 직접 신경 쓸 필요를 줄여줍니다.
ARC(Automatic Reference Counting)는 객체의 생성과 해제가 명시적으로 호출되지 않아도 자동으로 처리하기 때문에 메모리 관리가 효율적입니다.
그전에 메모리 관리가 필요한 이유는 C++이나 Objective-C 같은 언어에서는 객체를 생성한 후 수동으로 해제해야 합니다. 예를 들어, C++에서는 메모리를 동적으로 할당할 때 new를 사용하고, 더 이상 필요하지 않을 때 delete를 호출해야 합니다. 그렇지 않으면 메모리 누수(Memory Leak)가 발생합니다.
Swift에서는 객체의 생명주기가 초기화(init)와 해제(deinit) 과정을 통해 자동으로 관리됩니다. ARC(Automatic Reference Counting)가 객체의 생명주기를 추적하며, 객체를 더 이상 참조하지 않으면 메모리에서 자동으로 해제합니다. 이를 통해 개발자는 메모리 관리에 신경 쓰지 않고도 안전하고 효율적인 코드를 작성할 수 있습니다.
class MyImage {
let data: Data
init(data: Data) {
self.data = data
print("이미지 생성")
}
deinit {
print("이미지 메모리에서 자동 제거")
}
}
객체 생성 (init)
객체를 생성하면 다음과 같은 과정이 발생합니다:
let image = MyImage(data: someData)
- 메모리 할당: ARC가 객체를 저장할 메모리를 자동으로 할당합니다.
- 속성 초기화: init 메서드를 통해 객체의 속성을 초기화합니다.
- 추가 동작: init 메서드 내에서 필요한 작업을 수행합니다. 위 예제에서는 "이미지 생성" 메시지를 출력합니다.
객체 해제 (deinit)
객체가 더 이상 필요하지 않을 때 ARC는 자동으로 객체를 해제하며, 다음이 발생합니다:
- 참조 카운트 감소: 객체를 참조하는 변수가 없으면 참조 카운트가 0이 됩니다.
- 메모리 해제: 참조 카운트가 0이 되면 메모리가 해제됩니다.
- 정리 작업: deinit 메서드를 호출하여 필요한 작업을 수행할 수 있습니다.
이 모든 과정은 자동으로 처리되므로, 개발자가 수동으로 메모리 할당에 대해 걱정할 필요가 없는 것입니다.
2. 성능(Performance)
A. 최적화된 컴파일러
Swift는 LLVM(저수준 가상 머신) 컴파일러를 활용해서 코드를 최적화된 기계어로 변환합니다. 이를 통해 Swift로 작성된 코드는 최신 하드웨어의 성능을 최대한 활용할 수 있습니다.
func calculateSum(numbers: [Int]) -> Int {
return numbers.reduce(0, +)
}
let result = calculateSum(numbers: Array(1...1_000_000))
위 코드를 보면 1부터 1,000,000까지의 정수를 합산하는 함수입니다. LLVM 컴파일러는 이러한 코드를 취적화하여, 마치 C언어로 작성된 코드처럼 높은 성능을 발휘하도록 합니다. 이러한 최적화를 통해 Swift는 손목에 착용한 시계에서부터 서버 클러스터에 이르기까지 다양한 환경에서 효율적으로 동작할 수 있게 됩니다.
B. 효율적인 메모리 구조
Swift는 값 타입과 참조 타입을 명확하게 구분하여 메모리 관리의 효율성을 높입니다.
값 타입 (Value Type)
값 타입은 데이터를 전달할 때 해당 값을 복사하여 전달합니다. Swift의 구조체(struct)와 열거형(enum)이 이에 해당합니다.
이러한 값 타입은 스택(Stack) 메모리에 저장되며, 복사 시 메모리 접근이 빠릅니다.
예를 들어, 다음과 같은 구조체가 있습니다.
struct Point {
var x: Int
var y: Int
}
Point 구조체는 값 타입이므로, 이를 다른 변수에 할당하거나 함수에 전달할 때마다 새로운 복사본이 생성됩니다.
따라서 원본과 복사본은 독립적으로 존재하며, 한쪽의 변경이 다른 쪽에 영향을 미치지 않습니다.
참조 타입 (Reference Type)
참조 타입은 데이터를 전달할 때 해당 값의 메모리 주소를 전달합니다. Swift의 클래스(class)가 이에 해당합니다. 참조 타입은 힙(Heap) 메모리에 저장되며, 여러 변수가 동일한 인스턴스를 참조할 수 있어 유연한 메모리 관리가 가능합니다.
예를 들어, 다음과 같은 클래스가 있습니다.
class Rectangle {
var origin: Point
var width: Double
var height: Double
init(origin: Point, width: Double, height: Double) {
self.origin = origin
self.width = width
self.height = height
}
}
Rectangle 클래스는 참조 타입이므로, 이를 다른 변수에 할당하거나 함수에 전달할 때 동일한 인스턴스를 참조하게 됩니다. 따라서 한 변수가 인스턴스의 속성을 변경하면, 그 인스턴스를 참조하는 다른 변수에서도 변경된 내용이 반영됩니다.
값 타입은 스택 메모리에 저장되어 빠른 접근이 가능하며, 복사 비용이 적습니다.
반면, 참조 타입은 힙 메모리에 저장되어 더 유연한 메모리 관리가 가능합니다.
이러한 특성으로 인해, 값 타입은 주로 간단한 데이터 구조에 사용되며, 참조 타입은 복잡한 객체나 상속이 필요한 경우에 사용됩니다.
이러한 값 타입과 참조 타입의 구분은 Swift의 메모리 관리 효율성을 높이게 되는 것입니다.
3. 현대적인 프로그래밍 패러다임(Modern Programming Paradigms)
A. 타입 추론
Swift는 타입 추론 기능을 통해 코드의 간결함과 가독성을 높여줍니다. 변수나 상수를 선언할 때 타입을 명시하지 않아도, 컴파일러가 할당된 값에 따라 타입을 자동으로 추론합니다.
예를 들어,
// 타입 명시
let explicitName: String = "Swift"
// 타입 추론
let implicitName = "Swift" // 컴파일러가 String임을 알아서 추론
위에서 implicitName은 타입을 명시하지 않았지만, 컴파일러는 문자열 "Swift"를 할당하는 것을 보고 String 타입으로 추론합니다.
B. 클로저
클로저는 코드의 블록을 캡처하고 전달할 수 있는 자율적인 함수로, 매우 중요한 기능입니다. 클로저를 활용하면 고차 함수와 함께 강력하고 유연한 코드를 작성할 수 있습니다.
예를 들어,
let numbers = [1, 2, 3, 4, 5]
// 기본 형태의 클로저
let doubled = numbers.map({ (number: Int) -> Int in
return number * 2
})
위 코드를 보면 map 함수를 사용하여 배열의 각 요소를 두 배로 만든 새로운 배열을 생성합니다. 여기서 클로저는 매개변수와 반환 타입을 명시적으로 선언하고 있습니다.
하지만 Swift의 문법을 활용하면 클로저를 더욱 간결하게 표현할 수 있습니다.
// 축약형 클로저
let doubled = numbers.map { $0 * 2 }
여기서는 매개변수와 반환 타입을 생략하고, $0은 첫 번째 매개변수를 의미합니다. 결과적으로 코드가 훨씬 짧아지고 읽기 쉬워집니다.
C. 프로토콜 지향 프로그래밍
프로토콜 지향 프로그래밍은 코드의 유연성과 재사용성을 높이는 핵심 개념입니다.
특정 기능이나 속성을 정의하는 청사진으로, 클래스, 구조체, 열거형 등 다양한 타입이 이를 채택하여 해당 기능을 구현할 수 있습니다.
예를 들어,
protocol Movable {
func move()
}
protocol Flyable {
func fly()
}
Movable 프로토콜은 move() 메서드를, Flyable 프로토콜은 fly() 메서드를 요구합니다. 이제 Bird와 Robot 구조체가 이러한 프로토콜을 채택하여 각각의 기능을 구현할 수 있습니다.
struct Bird: Movable, Flyable {
func move() {
print("걸어갑니다")
}
func fly() {
print("날아갑니다")
}
}
struct Robot: Movable {
func move() {
print("굴러갑니다")
}
}
Bird는 Movable과 Flyable 프로토콜을 모두 채택하여 move()와 fly() 메서드를 구현하고, Robot은 Movable 프로토콜만 채택하여 move() 메서드를 구현합니다. 이러한 방식으로 프로토콜을 통해 필요한 기능만을 조합하여 사용할 수 있어, 마치 레고 블록을 조립하듯 유연한 설계가 가능합니다.
프로토콜 지향 프로그래밍은 상속의 복잡성을 피하면서도 코드의 재사용성을 극대화할 수 있는 장점을 제공합니다. 이를 통해 다양한 타입이 공통된 인터페이스를 공유하면서도 각자의 고유한 구현을 가질 수 있습니다.
SwiftUI
SwiftUI는 Apple이 2019년 WWDC에서 발표한 선언형 사용자 인터페이스 프레임워크로, 모든 Apple 플랫폼(iOS, iPadOS, macOS, watchOS, tvOS)에서 일관된 UI를 구축할 수 있도록 설계되었습니다.
SwiftUI의 가장 중요한 특징은 여러 가지가 있지만, 세 가지만 간추려서 얘기해 보겠습니다.
1. 선언형 문법 (Declarative Syntax)
SwiftUI는 UI를 선언형(declarative) 방식으로 작성합니다. 선언형 프로그래밍은 "무엇을 할 것인지"를 기술하는 방식으로, "어떻게 할 것인지"에 대한 세부 구현은 시스템이나 프레임워크에 맡기는 프로그래밍 패러다임입니다. 이러한 접근 방식은 코드의 가독성과 유지보수성을 높이며, 복잡한 로직을 단순화하는 데 도움을 줍니다.
import SwiftUI
struct ContentView: View {
var body: some View {
VStack {
Text("Hello")
.font(.title)
Button("Click me") {
}
}
}
}
#Preview {
ContentView()
}
A. 무엇을 할 것인지(무엇이 보일지)
VStack {
Text("Hello")
Button("Click me") { }
}
- VStack: 뷰를 세로로 배치하는 컨테이너를 선언합니다.
- Text("Hello"): 화면에 "Hello"라는 텍스트를 표시합니다.
- Button("Click me"): "Click me"라는 버튼을 화면에 추가합니다.
B. 어떻게 할 것인지 (어떻게 보일지)
Text("Hello")
.font(.title)
- 텍스트의 크기를 .title 스타일로 설정한다고 선언합니다.
- 구체적으로 폰트를 어떻게 적용할지 구현하지 않아도 SwiftUI가 내부적으로 처리합니다.
C. UI 업데이트 로직 자동화
- 선언형 코드에서는 상태 관리만 하면, UI 업데이트는 SwiftUI가 자동으로 처리합니다.
- 현재 코드에는 상태가 없지만, 상태 추가 시 뷰와 상태 간의 자동 동기화가 선언형 프로그래밍의 핵심입니다.
D. 간결함과 가독성
선언형 문법은 간결하고 읽기 쉬운 코드로, UI의 구조와 동작을 직관적으로 이해 가능합니다.
2. 크로스 플랫폼 지원
SwiftUI는 iOS, macOS, watchOS, tvOS 등 모든 Apple 플랫폼에서 동일한 코드로 UI를 구축할 수 있도록 설계되었습니다.
이를 통해 개발자는 각 플랫폼별로 별도의 코드를 작성할 필요 없이, 하나의 코드베이스로 다양한 기기에서 일관된 사용자 경험을 제공할 수 있습니다.
아래 코드는 SwiftUI가 플랫폼별로 조건부 UI를 제공하면서도 공통된 구조를 유지할 수 있음을 보여줍니다.
import SwiftUI
struct ContentView: View {
var body: some View {
VStack {
Text("Welcome to SwiftUI!")
.font(.largeTitle)
.padding()
// 플랫폼별로 다른 UI 구성
#if os(iOS)
Text("Running on iOS")
.foregroundColor(.blue)
Button("iOS Button") {
print("iOS Button Clicked")
}
#elseif os(macOS)
Text("Running on macOS")
.foregroundColor(.green)
Button("macOS Button") {
print("macOS Button Clicked")
}
#elseif os(watchOS)
Text("Running on watchOS")
.foregroundColor(.purple)
Button("watchOS Button") {
print("watchOS Button Clicked")
}
#endif
}
.padding()
}
}
#Preview {
ContentView()
}
A. 공통된 구조
- 모든 플랫폼에서 동일한 VStack 레이아웃을 사용합니다 Text("Welcome to SwiftUI!")를 사용합니다.
- 이러한 방식으로 기본적인 UI 구조는 재사용 가능합니다.
B. 플랫폼별 커스터마이징
if os(...) 조건문을 통해 플랫폼에 따라 다른 UI를 제공합니다.
- iOS: 파란색 텍스트와 "iOS Button" 추가합니다.
- macOS: 녹색 텍스트와 "macOS Button" 추가합니다.
- watchOS: 보라색 텍스트와 "watchOS Button" 추가합니다.
C. 효율적인 코드 관리
- 하나의 코드베이스에서 플랫폼별 요구사항을 처리하기 때문에 코드 중복을 줄이고, 유지보수성을 높입니다.
실행 환경에 따른 동작 방식
작성한 코드는 어떤 플랫폼에서 실행되느냐에 따라 다른 UI를 보여줍니다.
SwiftUI는 실행 중인 디바이스를 자동으로 감지하고, 그에 맞는 코드를 실행합니다. 즉, iOS 앱에서는 iOS 관련 UI가 표시되고, macOS 앱에서는 macOS UI가 표시됩니다.
위 코드를 각 플랫폼에서 실행했을 때 플랫폼별 특성에 맞게 표시되는 화면입니다.
3. 실시간 미리보기 (Live Preview)
SwiftUI의 실시간 미리 보기(Live Preview)는 Xcode에서 SwiftUI 코드 변경 사항을 즉시 시각적으로 확인할 수 있는 기능입니다. 이를 통해 앱을 빌드하거나 시뮬레이터를 실행하지 않고도 UI의 디자인과 동작을 빠르게 테스트할 수 있습니다.
SwiftUI 이전에 사용 되던 UIKit에는 Preview 기능이 존재하지 않았습니다. 그래서 UI 확인 과정이 비교적 느리고 번거로웠던 부분이 있습니다. 하지만 SwiftUI에서는 Priview 기능을 통해 여러 가지 장점이 존재합니다.
- 즉각적인 피드백: 코드 수정 시 Xcode의 캔버스(Canvas)에서 해당 변경 사항이 즉시 반영되어, UI의 변화를 실시간으로 확인할 수 있습니다.
- 다양한 환경 설정: 미리보기에서 다크 모드, 다양한 기기 크기, 현지화 등 다양한 환경을 설정하여 UI가 다양한 조건에서 어떻게 표시되는지 확인할 수 있습니다.
- 상호작용 지원: 미리보기에서 버튼 클릭, 스크롤 등 사용자 상호작용을 테스트할 수 있어, 앱의 동작을 실제와 유사하게 검토할 수 있습니다.
다음은 SwiftUI의 Live Preview 기능을 잘 나타내는, 상태 변화를 실시간으로 확인할 수 있는 간단한 코드입니다.
import SwiftUI
struct ContentView: View {
@State private var isToggled = false
var body: some View {
VStack {
Text(isToggled ? "Switch is ON" : "Switch is OFF")
.font(.title)
.foregroundColor(isToggled ? .green : .red)
.padding()
Toggle("Toggle Switch", isOn: $isToggled)
.padding()
}
.padding()
}
}
#Preview {
ContentView()
}
작성 후 프리뷰를 확인해 보면
우측에서 스위치를 켜고 끄는 동작을 수행하면서 텍스트와 색상이 실시간으로 변하는 것을 확인할 수 있습니다.
Live Preview는 이러한 상태 변화를 빠르게 테스트할 수 있는 환경을 제공하는 것이 큰 장점이라고 할 수 있습니다.
잘못된 내용 혹은 오타가 있거나 더 좋은 내용 피드백은 언제나 환영입니다 :)
'Apple > visionOS' 카테고리의 다른 글
[Apple/visionOS] Apple Vision Pro 공간 컴퓨팅 개념 정리 (0) | 2024.10.15 |
---|---|
[Apple/visionOS] See-Through와 Pass-Through (0) | 2024.10.08 |
[Apple/visionOS] “VR, AR, MR 그리고 XR 그 차이는?” (1) | 2024.10.04 |