iOS 개발을 하다 보면 "UIKit은 명령형, SwiftUI는 선언형"이라는 말을 자주 듣게 되는데요. 처음엔 그냥 "아 그렇구나" 하고 넘어갔었는데, 실제로 둘 다 써보니까 왜 이런 구분이 필요한지 몸소 느끼게 되더라고요.
특히 SwiftUI로 넘어오면서 코드가 훨씬 간결해지고 예측 가능해졌는데, 이게 단순히 새로운 문법 때문만은 아니라는 걸 깨달았어요. 프로그래밍 패러다임 자체가 다르기 때문이었거든요.
그런데 여기서 한 가지 깨달은 게 있었어요. 선언형 프로그래밍의 본질은 단순히 "무엇을 원하는가"를 명시하는 게 아니라, "추상화 레벨을 높여서 복잡함을 숨기는 것"이더라고요.
토스 팀의 아티클을 보면서 이 부분이 더 명확해졌는데, 결국 우리가 매일 쓰는 for...in 반복문도 선언형 코드라는 거예요.
프로그래밍 패러다임이 뭔가요?
프로그래밍 패러다임은 말 그대로 "프로그램을 바라보는 관점"이에요. 같은 문제를 해결하더라도 접근 방식이 완전히 달라질 수 있죠.
저도 처음엔 이런 개념적인 얘기가 실무와 무슨 상관이 있나 싶었는데, 실제로 코드를 짜다 보니 패러다임에 따라 사고방식 자체가 바뀌는 걸 경험할 수 있었어요.
명령형 프로그래밍 - "어떻게 할 것인가"
명령형 프로그래밍은 프로그램의 상태(state)와 그 상태를 변경하는 구문들의 관점으로 접근합니다. "어떻게(How) 할 것인가"에 초점을 맞춰서 실행할 명령들을 순서대로 작성하죠.
func permutation<T>(_ arrs: [T]) -> [[T]] {
var result = [[T]]()
var check = [Bool](repeating: false, count: arrs.count)
func permute(_ arr: [T]) {
if arr.count == arrs.count {
result.append(arr)
return
}
for i in 0..<arrs.count {
if check[i] == true {
continue
} else {
check[i] = true
permute(arr + [arrs[i]])
check[i] = false
}
}
}
permute([])
return result
}
이 코드를 보면 단계별로 "이걸 하고, 저걸 하고, 그 다음엔 이걸 해라"라고 컴퓨터에게 명령하고 있어요. for문과 조건문을 통해 매우 구체적으로 지시하는 방식이죠.
선언형 프로그래밍 - "무엇을 원하는가"
반면 선언형 프로그래밍은 "무엇을(What) 원하는가"에 집중합니다. 구체적인 실행 순서보다는 원하는 결과를 선언하면, 시스템이 알아서 그 방법을 결정하죠.
let numbers = [1, 2, 3, 4, 5]
let evenSquares = numbers
.filter { $0 % 2 == 0 }
.map { $0 * $0 }
이 코드는 "짝수만 골라서 제곱하고 싶다"는 의도를 직접적으로 표현해요. 내부적으로 어떤 반복문이 돌아가는지는 신경 쓰지 않아도 되거든요.
저는 처음에 filter와 map을 쓸 때 "이게 진짜 동작하나?" 싶어서 내부 구현을 찾아봤었는데, 결국 반복문이 들어있더라고요. 하지만 그 복잡함을 추상화해서 숨겨놓으니까 개발자는 의도만 표현하면 되는 거였어요
함수형 프로그래밍이 등장하는 이유
그런데 여기서 왜 함수형 프로그래밍이 나오는 걸까요?
선언형 프로그래밍에서 "정의된 방법"이라고 하는 것들이 바로 함수이기 때문이에요. filter, map, reduce 같은 것들 말이죠.
이런 함수들을 조합해서 원하는 결과를 만들어내는 방식이 함수형 프로그래밍의 핵심이거든요.
함수형 프로그래밍의 핵심 특징
1. 순수 함수 (Pure Function)
순수 함수는 동일한 입력에 대해 항상 동일한 출력을 보장하고, 부수효과(Side Effect)가 없는 함수예요.
// ✅ 순수 함수
func area(width: Double, height: Double) -> Double {
return width * height
}
// ❌ 비순수 함수 (외부 상태에 의존)
var discount = 0.1
func calculatePrice(amount: Int) -> Double {
return Double(amount) * (1 - discount) // 외부 변수 참조
}
순수 함수의 장점은 정말 명확해요. 같은 입력에 대해 같은 결과가 나오니까 테스트하기 쉽고, 디버깅하기 쉽고, 예측 가능하거든요.
2. 불변성 (Immutability)
데이터가 변경되지 않고, 변경이 필요하면 새로운 인스턴스를 생성하는 방식이에요.
let numbers = [1, 2, 3]
let newNumbers = numbers + [4] // 새 배열 생성
// numbers는 여전히 [1, 2, 3]
3. 1급 객체로서의 함수
- 함수를 변수에 할당하고 인자로 전달 가능
let add: (Int, Int) -> Int = { $0 + $1 }
let functions = [add, subtract, multiply] // 함수를 배열에 저장
이런 특징들이 왜 중요한지 처음엔 잘 몰랐는데, 멀티스레드 환경에서 개발해보니까 확실히 느껴지더라고요. 값이 변하지 않으니까 여러 스레드에서 동시에 접근해도 충돌이 발생하지 않거든요
UIKit vs SwiftUI: 실전 비교
1. UI 구현 방식
UIKit (명령형) - 단계별 지시
class ViewController: UIViewController {
@IBOutlet weak var countLabel: UILabel!
@IBOutlet weak var incrementButton: UIButton!
private var count = 0
override func viewDidLoad() {
super.viewDidLoad()
updateUI() // 1. UI 초기화
incrementButton.addTarget(self, action: #selector(buttonTapped), for: .touchUpInside) // 2. 이벤트 연결
}
@objc private func buttonTapped() {
count += 1 // 3. 상태 변경
updateUI() // 4. UI 업데이트
}
private func updateUI() {
countLabel.text = "Count: \(count)" // 5. 실제 업데이트 로직
}
}
UIKit에서는 정말 모든 걸 다 해줘야 해요. UI 컴포넌트 생성하고, 부모 뷰에 추가하고, 제약조건 설정하고, 버튼 액션 연결하고... 심지어 데이터가 변경될 때마다 updateUI()를 직접 호출해줘야 하죠.
저는 처음에 이런 코드를 짤 때 updateUI() 호출을 깜빡해서 UI가 업데이트 안 되는 버그를 정말 많이 만들었어요 😅
SwiftUI (선언형):
struct ContentView: View {
@State private var count = 0
var body: some View {
VStack {
Text("Count: \(count)")
Button("Increment") {
count += 1
}
}
}
}
SwiftUI에서는 원하는 UI 구조를 선언하기만 하면 돼요. 데이터가 변경되면 UI가 자동으로 업데이트되어서 별도의 동기화 코드가 필요 없죠. 사실 SwiftUI가 내부적으로 diffing 시스템을 돌려서 변경된 부분만 찾아서 렌더링하기 때문이에요.
선언형 프로그래밍 내가 느낀 장점
1. 코드의 가독성과 표현력
선언형 코드는 "무엇을 하려는지"가 바로 드러나요. 비즈니스 로직과 구현 디테일이 분리되어 있어서, 코드를 읽는 사람이 의도를 빠르게 파악할 수 있거든요.
// 명령형: 어떻게 하는지에 집중
var activeUsers = [User]()
for user in users {
if user.isActive {
activeUsers.append(user)
}
}
// 선언형: 무엇을 하는지에 집중
let activeUsers = users.filter { $0.isActive }
2. 상태 관리의 단순화
SwiftUI에서는 @State, @ObservedObject 같은 프로퍼티 래퍼가 상태 변화를 자동으로 감지해서 UI를 업데이트해줘요. 개발자는 상태만 관리하면 되고, UI 동기화는 프레임워크가 알아서 처리하죠.
3. 테스트 용이성
순수 함수와 불변성 덕분에 테스트가 훨씬 쉬워져요. 입력이 같으면 출력이 같다는 보장이 있으니까, 예상 결과를 정확히 예측할 수 있거든요.
4. 동시성 안전성
불변 데이터는 여러 스레드에서 동시에 접근해도 안전해요. 데이터가 변하지 않으니까 race condition이 발생할 일이 없거든요.
더 나아가
Swift 포럼에서 함수형 프로그래밍에 대한 토론을 보면 재미있는 관점들이 많아요.
"FP는 도착점이지 출발점이 아니다"
이 말이 정말 와닿더라고요. 저도 처음엔 객체지향으로 시작했는데,
점점 "대부분의 소프트웨어 문제는 데이터 변환이다"라는 걸 깨달으면서 함수형 접근을 하게 되었거든요.
현실적인 타협점
완전한 함수형 프로그래밍만이 정답은 아니에요. 때로는 성능을 위해 지역적인 가변성을 허용하는 것도 필요하죠:
// ✅ 허용되는 지역 가변성
func quickSort<T: Comparable>(_ array: [T]) -> [T] {
var result = array // 지역 변수
// 정렬 로직... (내부적으로 가변성 사용)
return result // 하지만 외부에서는 순수 함수처럼 보임
}
함수 내부에서는 성능을 위해 가변성을 사용하지만, 외부 인터페이스는 순수 함수처럼 설계하는 거죠.
정리
명령형 프로그래밍은 어떻게 수행할지에 대해 초점을 맞춘다. UIkit에서 테이블뷰 업데이트할때, 데이터 소스 변경하고, 메서드 호출하고 애니메이션을 지정하는 등 모든 단계를 명시적으로 코딩했어야했다. 각 단계별로 뭘 해야할지 명시적으로 지시한다.
반면 선언형은 무엇을 원하는지가 중요하고 시스템이 그 실현 방법을 결정한다. List와 ForEach를 사용해 데이터를 선언하면 프레임워크가 뷰를 생성하고 데이터가 변경될때 자동으로 UI업데이트한다. 그저 정의된 방법을 사용! 하면 되는것이다.
여기서 이 정의된 방법이 함수이기에 함수형 프로그래밍이 자연스럽게 나온다. 순수함수와 불변성을 강조한 패러다이밍/
순수함수: 동일한 입력에 항상 같은 결과가 나와야함. 즉 외부에 영향을 끼치지 않는다. (사이드 이펙트가 없다)-> 여러 쓰레드에서 병렬처리가 가능하고 결합도도 낮고 자연스레 리팩토링도 용이해진다.
스유에서는 클래스가 아닌 구조체를 주로 사용하므로 불변성이 장점이였다.이로인해 변경된 부분만 재 랜더링하고 여러쓰레드에서 동시접근할수 있어 동시성 문제를 고민하지 않아도 된다. 예측가능하다.
물론 스유가짱이다는 아니다 커스텀하고 세밀한 제어가 필요하고 특정 성능 최적화할때는 명령형인 UIKit이 더나은 방법이 될수도 있다.
Q1. 왜 SwiftUI를 더 선호하시나요?
기존에 UIKit을 통해 개발을 했을 때 명령형 프로그래밍이기에 각 단계별로 뭘 해야하는지 명시적으로 코드를 짜야했었습니다. 하지만 SwiftUI를 접하고 나서 선언형 프로그래밍답게 그저 이미 정의된 방법을 사용하여 제가 원하는 개발을 할 수 있었습니다.
이 정의된 방법이라는게 함수이기에 함수형 프로그래밍의 장점도 컸습니다. 순수함수로 외부에 영향을 끼치지 않아 사이드이펙트가 없었기에 여러쓰레드에서 병렬처리를 해도 동시성 문제가 적고 예측 가능한 코드를 작성할 수 있었습니다.
또한 SwiftUI에서는 구조체로 설계하다보니 불변성이 보장됩니다. 즉 클래스와달리 내부 상태가 변경될때마다 새로운 인스턴스를 생성합니다. 덕분에 상태변화 예측이 쉽고 변경된 부분만 렌더링하다보니 성능적으로도 더 효율적이었습니다.
Q2. 그러면 UIKit도 함수형으로 짜면 되지 않나요?
물론 가능하지만 한계가 있습니다. UIKit은 본질적으로 객체지향적이고 명령형으로 설계되어 있어서, 뷰의 상태를 직접 관리해야 하는 경우가 많습니다. 클래스 기반이라서 불변성을 보장하려면 추가적인 설계가 필요하고, 참조형 프로퍼티를 수정하는 과정에서 부수효과를 완전히 피하기는 어렵습니다.
예를 들어 UIKit에서는 viewController.view.backgroundColor = .red 같은 식으로 직접 상태를 변경해야 하는데, 이는 함수형 프로그래밍의 불변성 원칙과 맞지 않죠.
하지만 UIKit에서도 함수형 접근을 부분적으로 활용할 수는 있어요:
Q3. SwiftUI는 어떻게 변경된 부분만 재렌더링하나요?
SwiftUI는 내부적으로 Virtual DOM과 유사한 diffing 시스템을 사용합니다:
- 상태 감지: @State, @ObservedObject 등의 프로퍼티 래퍼가 상태 변경을 감지
- 뷰 트리 재생성: 상태가 변경되면 새로운 뷰 트리를 생성 (구조체 특성상)
- Diffing 알고리즘: 이전 뷰 트리와 새로운 뷰 트리를 비교하여 차이점 파악
- 선택적 업데이트: 실제로 변경된 부분만 UIKit 레이어에서 업데이트
이 과정이 매우 빠르게 이루어져서 성능상 문제가 없고, 개발자는 전체 상태만 관리하면 되는 거죠.
Q4. 함수형 프로그래밍의 핵심 개념을 설명해주세요.
1. 순수 함수 (Pure Function)
- 동일한 입력에 대해 항상 동일한 출력
- 외부 상태에 의존하지 않고 부수효과가 없음
// ✅ 순수 함수
func add(_ a: Int, _ b: Int) -> Int { a + b }
// ❌ 비순수 함수 (외부 상태 의존)
var multiplier = 2
func multiply(_ x: Int) -> Int { x * multiplier }
2. 불변성 (Immutability)
- 데이터가 한 번 생성되면 변경되지 않음
- 변경이 필요하면 새로운 인스턴스 생성
let numbers = [1, 2, 3]
let newNumbers = numbers + [4] // 기존 배열은 그대로
3. 고차 함수 (Higher-Order Function)
- 함수를 인자로 받거나 반환하는 함수
- map, filter, reduce 등이 대표적
Q5. 커링(Currying)이 무엇이고 언제 사용하나요?
커링은 다중 인자 함수를 단일 인자 함수들의 연쇄로 변환하는 기법입니다:
// 일반 함수
func add(_ a: Int, _ b: Int) -> Int { a + b }
// 커링된 함수
func curriedAdd(_ a: Int) -> (Int) -> Int {
return { b in a + b }
}
// 사용 예시
let addFive = curriedAdd(5)
let result = addFive(3) // 8
// 더 실용적인 예시
func calculate(_ operation: @escaping (Int, Int) -> Int) -> (Int) -> (Int) -> Int {
return { a in
return { b in
operation(a, b)
}
}
}
let multiply = calculate(*)
let double = multiply(2)
double(10) // 20
언제 사용하나요?
- 특정 설정을 고정하고 재사용하고 싶을 때
- 함수 조합을 통해 더 복잡한 로직을 만들 때
- 부분 적용(Partial Application)이 필요한 경우
Q6. Swift에서 지연 평가(Lazy Evaluation)는 어떻게 동작하나요?
지연 평가는 실제로 값이 필요할 때까지 계산을 미루는 방식입니다:
let lazyNumbers = (1...1000000).lazy
.filter { $0 % 2 == 0 }
.map { $0 * 2 }
.prefix(5) // 실제로는 처음 5개만 계산됨
// 실제 계산은 Array(lazyNumbers) 호출 시점에 발생
장점:
- 메모리 효율성: 필요한 만큼만 계산
- 성능 향상: 무한 시퀀스 처리 가능
- 조합성: 여러 변환을 체이닝해도 한 번에 처리
Q7. 함수형 프로그래밍의 성능은 어떤가요?
장점:
- 컴파일러 최적화가 더 잘 됨 (순수 함수는 예측 가능하니까)
- 병렬 처리가 쉬워서 멀티코어 활용도가 높음
- 메모이제이션 같은 최적화 기법 적용이 쉬움
단점:
- 불변 데이터 구조는 때로 더 많은 메모리를 사용
- 대량의 데이터를 처리할 때는 명령형이 더 효율적일 수 있음
실제로 제가 측정해본 바로는, 일반적인 앱 개발에서는 성능 차이를 체감하기 어려웠어요. 오히려 코드의 명확성과 유지보수성이 훨씬 중요한 요소였죠.
'면접준비' 카테고리의 다른 글
Dynamic Dispatch는 어떻게 이루어지는가? 클래스 VS 프로토콜 (0) | 2025.03.02 |
---|---|
NSObjcet 음.. SwiftUI에선? (0) | 2025.02.16 |
SwiftUI runLoop (0) | 2025.02.05 |
메모리관리(weak self와 guard의 만남) (0) | 2025.01.12 |
Hash-Hashable을 곁들인 (1) | 2025.01.05 |