getter와 setter, 어떻게 생각하시나요? #86
-
SnapKit에서 발견한 궁금증최근 코드에 대한 시야를 넓히고 싶어서 라이브러리를 훕쳐보고 있습니다. 그러던 중, ...
public var isActive: Bool {
set {
if newValue {
activate()
}
else {
deactivate()
}
}
get {
for layoutConstraint in self.layoutConstraints {
if layoutConstraint.isActive {
return true
}
}
return false
}
}
... 위 코드는 'isActive'라는 프로퍼티를 get, set할 때 computedProperty를 통해 로직을 수행하여 결과를 내고 있습니다. 기존 코드리뷰를 하다보면, 장단점과 활용개인적으로 위 코드에 대한 생각들을 듣고 싶습니다. 선언적이라고는 보기 어렵지만, isActive라는 프로퍼티에 관련한 로직을 응집해두었다는 점에서 유지보수성이 높아지는 것 같습니다. 독립적인 데이터 의존성을 가지고 있는 프로퍼티에 관한 경우 이런 코드를 작성하신 분이나, 위 코드의 활용에 대한 의견이 있으신 분은 답급 달아주시면 감사하겠습니다 :) |
Beta Was this translation helpful? Give feedback.
Replies: 1 comment 1 reply
-
생각을 공유하는데 앞서 @moonkey48의 글을 읽고 제가 이해한 바에 대해 먼저 풀어보겠습니다. 기존에 많이 보던 패턴 -> 상태변수와 부수효과를 분리SwiftUI와 같은 선언형 프레임워크는 @State와 같은 상태변수를 이용하여
struct ContentView: View {
// 화면의 가능한 상태를 정의
@State private var isComplete = false
var body: some View {
VStack {
// 각 상태에 따른 최종 화면 모습을 선언
Text(isComplete ? "완료" : "미완료")
.foregroundColor(isComplete ? .green : .red)
Button("상태 변경") {
isComplete.toggle()
}
}
}
} 이렇게 정의된 상태(
struct ProfileView: View {
@State private var isEditing = false
var body: some View {
VStack {
// UI 상태 관리
if isEditing {
ProfileEditForm()
} else {
ProfileDisplayView()
}
EditButton(isEditing: $isEditing)
}
// 상태 변화에 따른 부가 작업
.onChange(of: isEditing) { _, isEditing in
if !isEditing {
// 1. 데이터 저장
saveProfile()
// 2. 서버 동기화
syncWithServer()
// 3. 분석 이벤트 전송
Analytics.log("profile_edited")
}
}
}
} 그리고 위와 같이 상태 변수를 단지 화면 상태관리에만 사용하지 않고 로직 실행을 위한 트리거로 활용함으로써 화면 변경에 따라 데이터 흐름을 관리할 수 있게 하였습니다. 그리하여 관심사의 자연스러운 분리를 달성할 수 있게 됩니다.
결과적으로 SwiftUI는 상태 변수를 이용해 상태변수를 통해 화면 상태를 관리하는 동시에 상태변수의 변경을 트리거로 활용하여 UI와 데이터흐름을 제어하는 방식이 오스틴이 언급해주신 3가지 방식입니다.
struct ContentView: View {
// 상태변수를 통해 화면 상태를 관리
@State private var isActive = false
var body: some View {
Toggle("Active", isOn: $isActive)
// onChange 수정자를 이용해 상태변수 값 변경을 트리거로 사용
.onChange(of: isActive) { oldValue, newValue in
if newValue {
activate()
} else {
deactivate()
}
}
}
private func activate() {
// 활성화 로직
}
private func deactivate() {
// 비활성화 로직
}
}
class ViewController: UIViewController {
// 1. 상태변수를 통해 화면 상태 관리
private var isActive: Bool = false {
didSet {
// 3. Combine 의 subject와 didSet(프로퍼티 옵저버)를 조합해
// 상태변수 값의변경이 있을 때 이를 발행하도록 함
isActiveSubject.send(isActive)
}
}
// 2. Subject를 통해 변경된 상태값에 대한 발행자를 설정
private let isActiveSubject = CurrentValueSubject<Bool, Never>(false)
private var cancellables = Set<AnyCancellable>()
override func viewDidLoad() {
super.viewDidLoad()
// 4. 상태변수 값 변경에 대한 발행 값을 구독하여 이 값에 대한 로직을 처리
isActiveSubject
.sink { [weak self] newValue in
if newValue {
self?.activate()
} else {
self?.deactivate()
}
}
.store(in: &cancellables)
}
private func activate() {
// 활성화 로직
}
private func deactivate() {
// 비활성화 로직
}
}
class MyView: UIView {
// 1. 상태변수를 통해 화면 상태 관리
// private 접근자를 이용, 해당 프로퍼티에 대한 직접적인 get, set을 막습니다.
// let myView = MyView()
// myView.isActive = false -> 불가
private var isActive: Bool = false
// 2. 상태변수 변경을 위한 별도의 setter를 정의
func setActive(_ active: Bool) {
isActive = active
// 3. 정의한 setter 내부에 상태값 변경에 따라 실행할 로직을 삽입
updateActiveState()
}
private func updateActiveState() {
if isActive {
activate()
} else {
deactivate()
}
}
private func activate() {
// 활성화 로직
}
private func deactivate() {
// 비활성화 로직
}
}
// 사용 예
let myView = MyView()
myView.setActive(true) 이에 비해 SnapKit에서는 현재 상태변수 값을 별도의 저장프로퍼티를 통해 관리하지 않고 (즉 상태변수를 따로 두지 않고) 상태값 변경과 변경 값에 대응하는 로직을 계산 프로퍼티를 이용해 모아두고 있습니다. SnapKit은 Auto Layout을 쉽고 직관적으로 사용할 수 있는 api 를 제공하는 라이브러리입니다.
class MyView: UIView {
var isActive: Bool {
set {
if newValue {
activate()
} else {
deactivate()
}
}
get {
// 현재 상태를 확인하는 로직
return someInternalState
}
}
private func activate() {
// 활성화 로직
}
private func deactivate() {
// 비활성화 로직
}
}
// 사용 예
let myView = MyView()
myView.isActive = true // 단순히 값을 할당하는 것으로 원하는 상태를 반환할 수 있음 Computed Property를 사용해서 얻을 수 있는 장점은 다음과 같습니다.
class NavigationBarManager {
// 내부 구현은 숨기고 간단한 인터페이스만 제공
var isHidden: Bool {
set {
animate {
updateNavBarVisibility(newValue)
updateStatusBarStyle(newValue)
updateLayoutConstraints(newValue)
}
}
get {
return navigationBar.alpha == 0
}
}
}
// 사용
navManager.isHidden = true // 단순한 사용법
class CollapsiblePanel {
var isCollapsed: Bool {
set {
// 모든 관련 로직이 한 곳에 모여있음
animateStateChange(to: newValue)
updateChildViews(for: newValue)
updateAccessibilityState(newValue)
saveState(newValue)
}
get {
return currentState == .collapsed
}
}
} 각 방식의 특징을 고려하여 적합한 사용 상황을 생각해보았습니다. 상태 변수와 부수 효과(데이터 로직)을 나누는 것이 적합한 경우
struct ProfileEditView: View {
@State private var isEditing = false
@State private var username = ""
var body: some View {
VStack {
TextField("사용자 이름", text: $username)
Button(isEditing ? "저장" : "수정") {
isEditing.toggle()
}
}
// 단순한 UI 상태 변화에 따른 대응
.onChange(of: isEditing) { _, isEditing in
if !isEditing {
// 간단한 동기 작업
saveProfile()
updateUIState()
}
}
}
}
class SearchViewModel: ObservableObject {
@Published var searchQuery = ""
@Published var searchResults: [Item] = []
private var cancellables = Set<AnyCancellable>()
init() {
// 복잡한 데이터 흐름 처리
$searchQuery
.debounce(for: .milliseconds(300), scheduler: RunLoop.main)
.removeDuplicates()
.filter { !$0.isEmpty }
.map { query -> AnyPublisher<[Item], Error> in
// API 호출을 통한 데이터 변환
return self.searchService.search(query)
}
.switchToLatest()
.retry(3)
.receive(on: DispatchQueue.main)
.sink(
receiveCompletion: { completion in
// 에러 처리
},
receiveValue: { [weak self] results in
self?.searchResults = results
}
)
.store(in: &cancellables)
}
}
class ImageUploader {
private var isUploading = false
func uploadImage(_ image: UIImage) {
guard !isUploading else { return }
isUploading = true
// 순차적 작업 흐름이 명확함
compressImage(image) { compressedData in
validateSize(compressedData) { isValid in
if isValid {
performUpload(compressedData) { success in
if success {
self.updateImageURL()
self.notifyCompletion()
}
self.isUploading = false
}
}
}
}
}
} 계산 프로퍼티를 이용해 코드의 응집도를 높이는 것이 적합한 경우
class ExpandableView: UIView {
var isExpanded: Bool {
set {
if newValue {
self.frame.size.height = 200
self.alpha = 1.0
updateCornerRadius()
updateShadow()
} else {
self.frame.size.height = 50
self.alpha = 0.8
updateCornerRadius()
updateShadow()
}
}
get {
return frame.size.height > 50
}
}
}
class AudioManager {
var isMuted: Bool {
set {
if newValue {
currentVolume = 0
updateVolumeIndicator()
saveSettings()
notifyVolumeChange()
} else {
currentVolume = lastActiveVolume
updateVolumeIndicator()
saveSettings()
notifyVolumeChange()
}
}
get {
return currentVolume == 0
}
}
} 계산 프로퍼티를 이용하기 힘든 경우비동기 작업 처리의 한계
// ❌ 잘못된 사용 예
var isLoading: Bool {
set {
if newValue {
Task { // 비동기 작업은 적합하지 않음
await loadData()
updateUI()
}
}
}
get { ... }
}
// ✅ 이런 경우는 onChange나 Combine이 더 적합
@Published var isLoading = false
// ...
.onChange(of: isLoading) { _, isLoading in
if isLoading {
Task {
await loadData()
updateUI()
}
}
} 복잡한 상태 관리의 한계
// ❌ 너무 복잡한 상태 처리
var currentState: ViewState {
set {
switch newValue {
case .loading:
// 복잡한 상태 전이
case .error(let error):
// 에러 처리
case .success(let data):
// 데이터 처리
}
}
get { ... }
}
// ✅ 이런 경우는 상태 관리 패턴이 더 적합
@Published var viewState = ViewState.initial |
Beta Was this translation helpful? Give feedback.
생각을 공유하는데 앞서 @moonkey48의 글을 읽고 제가 이해한 바에 대해 먼저 풀어보겠습니다.
기존에 많이 보던 패턴 -> 상태변수와 부수효과를 분리
SwiftUI와 같은 선언형 프레임워크는 @State와 같은 상태변수를 이용하여
이렇게 정의된 상태(
@State
)가 변경되면 SwiftUI가 자동으로 화면을 업데이트합니다. 개발자는 "어떻게" 화면을 업데이트할지가 아니라, "무엇이" 보여져야 하는지만 정의하면…