함수형 프로그래밍 & 명령형과 (유킷 스유)
뭐 우리가 UIKit이나 SwiftUI를 공부하다보면 UIKit은 명령형 프로그래밍이였는데 SwiftUI는 선언형 프로그래밍이라 짱짱 편해요~!!
🤔 음. 그러면 왜 명령형보다 선언형을 좋아하시나요?
먼저 프로그래밍 패러다임이 뭔지부터 알아야한다. :) 프로그래머들이 프로그램을 바라보는 관점 👀
명령형 프로그래밍
프로그램의 상태(state)와 그 상태를 변경시키는 구문(statements)의 관점으로 접근한다. 프로그램은 실행할 명령들을 실행할 순서로 작성한다. 대부분의 객체지향이 속한다. 우리가 알고리즘을 풀 때 사용하는 방식이 명령형 프로그램이다.
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문을 통한 반복문..들을 명령해서 결과를 뽑아낸다.
선언형 프로그래밍
반면 선언형은 프로그램이 무엇을 하는지에 집중하는것이지 어떻게(HOW)에 집중하지 않는다. 뭐 구체적인 작동 순서 나열(for문같은)하지 않고 선언으로만 동작시킨다. 우리가 아는 map, filter등 고차함수를 활용한 코드가 선언형의 예라 들 수 있다. 그런데 여기서 함수형 프로그래밍을 통해 일정 수준의 선언형 프로그래밍을 할 수 있다. 즉 함수형 프로그래밍은 선언형 프로그래밍의 한 종류이다.
함수형 프로그래밍
함수형 프로그래밍은 수학적 함수의 계산을 기반으로 자료를 처리하는 프로그래밍 패러다임으로, 선언형 프로그래밍의 한 종류입니다.
- 순수 함수: 동일 입력에 대해 항상 동일 출력을 반환하며 부수효과가 없다.
- 1급 객체(에관한건 따로 이미 블로그에 있다): 함수를 변수나 데이터에 할당 할 수 있고, 객체의 인자로 넘길수 있고, 객체의 리턴값으로 사용 가능하다.
- 불변성(Immutable): 내부에 데이터를 가지지 않아 데이터는 변경되지 않으며, 시간적 결합(time coupling) 및 멀티스레드 환경의 충돌(race condition)을 방지.
- 오로지 입력값에만 영향을 받아 테스트 코드짜기가 쉽고 검증이 쉽다.
- 이전에 계산한 함수를 캐싱해두었다가 다시 사용하는 메모리제이션이 가능하다.
그렇다면 어떻게 함수형 프로그래밍은 레이스 컨디션을 방지할까?
-> 값이 변하지 않으니까 여러스레드에서 동시에 함수에 접근해도 충돌이 없다!
선언형 코드는 제어 흐름을 최소화하여 코드 예측성이 좋다.
그러면 우리의 UIKit과 스유얘기를 더해보자
1. UI 구현방식.
UIKit: UI컴포넌트를 하나씩 만들고 부모 뷰에 추가하고 레이아웃 제약을 설정한다. 만약 버튼인 경우 눌렀을때 실행될 액션 메서드 정의하고 버튼에 액션을 추가하겠지.
SwiftUI: body 속성 내에 UI컴포넌트를 선언한다. VStack, HStack, ZStack과 같은 컨테이너를 이용해 뷰 계층을 구성한다. UI의 상태를 직접 조작하는 것이 아니라, 데이터의 변화를 바탕으로 UI를 갱신한다.
2. 상태관리
UIKit: UILabel.text나 UIButton.setTitle(_:for:) 같은 메서드를 호출하여 UI 요소를 직접 업데이트해야 한다.
즉 데이터가 변경될때마다 모든 상태 변화를 추적하고 UI를 동기화해주어야했다. 상태가 복잡해질 수록 관리하기 어려었다.
SwiftUI: State, @Binding, @ObservedObject 등의 속성을 사용하여 상태를 추적하고, 상태가 변경되면 UI가 자동으로 업데이트된다.
이러한 선언형 방식은 UI 상태와 데이터 흐름을 예측 가능하게 만들어 코드의 유지보수성을 높인다.
3. 간결함.
당연히 선언형언어인 스유가 동일한 UI를 작성해도 코드가 짧다.
정리
명령형 프로그래밍은 어떻게 수행할지에 대해 초점을 맞춘다. UIkit에서 테이블뷰 업데이트할때, 데이터 소스 변경하고, 메서드 호출하고 애니메이션을 지정하는 등 모든 단계를 명시적으로 코딩했어야했다. 각 단계별로 뭘 해야할지 명시적으로 지시한다.
반면 선언형은 무엇을 원하는지가 중요하고 시스템이 그 실현 방법을 결정한다. List와 ForEach를 사용해 데이터를 선언하면 프레임워크가 뷰를 생성하고 데이터가 변경될때 자동으로 UI업데이트한다. 그저 정의된 방법을 사용! 하면 되는것이다.
여기서 이 정의된 방법이 함수이기에 함수형 프로그래밍이 자연스럽게 나온다. 순수함수와 불변성을 강조한 패러다이밍/
순수함수: 동일한 입력에 항상 같은 결과가 나와야함. 즉 외부에 영향을 끼치지 않는다. (사이드 이펙트가 없다)-> 여러 쓰레드에서 병렬처리가 가능하고 결합도도 낮고 자연스레 리팩토링도 용이해진다.
스유에서는 클래스가 아닌 구조체를 주로 사용하므로 불변성이 장점이였다.이로인해 변경된 부분만 재 랜더링하고 여러쓰레드에서 동시접근할수 있어 동시성 문제를 고민하지 않아도 된다. 예측가능하다.
물론 스유가짱이다는 아니다 커스텀하고 세밀한 제어가 필요하고 특정 성능 최적화할때는 명령형인 UIKit이 더나은 방법이 될수도 있다.
그래서 대체적으로 나는 어차피 IOS13이상을 타겟으로하다보니 스유를 베이스로 사용하고 유킷을 때에따라 필요시 사용하는 개발자가 되려 노력한다.
자네는 왜 스유를 더 좋아하는가?
기존에 UIKit을 통해 개발을 했을때 명령형 프로그래밍이기에 각 단계별로 뭘 해야하는지 명시적으로 코드를 짜야했었습니다. 하지만 SwiftUI를 접하고 나서 선언형 프로그래밍답게 그저 이미 정의된 방법을 사용하여 제가 원하는 개발을 할 수 있었습니다. 이 정의된 방법이라는게 함수이기에 함수형 프로그래밍의 장점도 컸습니다. 순수함수로 외부에 영향을 끼치지 않아 사이드이펙트가 없었기에 여러쓰레드에서 병렬처리를 해도 동시성 문제가 적고 예측 가능한 코드를 작성할 수 있었습니다.
또한 Swiftui에서는 구조체로 설계하다보니 불변성이 보장됩니다. 즉 클래스와달리 내부 상태가 변경될때마다 새로운 인스턴스를 생성하빈다. 덕분에 상태변화 예측이 쉽고 변경된 부분만 렌더링하다보니 성능적으로도 더 효율적이었습니다.
그러면 UIkit도 함수형으로 짜면 되지않는가?
ㅇㅇ 가능. 하지만 UIKit은 본질적으로 객체지향적이고 명령형으로 설계되어 뷰의 상태를 직접 관리해야함(상태를 변경할때마다 참조형 프로퍼티를 수정). 결국 이런 작업들은 사이드이펙트를 피하기 힘들다고 생각합니다. 클래스 기반이기에 불변성 보장하려면 추가적인 설계가 필요합니다.
그러면 어떻게 변경된 부분만 재랜더링하는거지?
- 상태 프로퍼티가 변경되면 시스템이 이를 감지합니다.