Swift

클린 아키텍처: TextField 데이터를 Interactor로 전달하는 방법 - (1)

GH_iOS 2024. 12. 19. 14:49

클린 아키텍처를 적용하면서, 가장 저수준 객체인 View에서 TextField를 통해 입력된 텍스트 데이터를

고수준 객체인 Interactor에 강결합 없이 전달하는 방법에 대해 고민했습니다.

이번 글에서는 이 문제를 해결하기 위해 제가 고민했던 여러 가지 방법과, 최종적으로 선택한 방법에 대해 정리해 보려고 합니다.


데이터의 흐름

현재 데이터의 흐름은 View → ViewController → Interactor 순으로 진행되고 있습니다.

이를 기반으로 조사한 결과, 총 3가지 방법을 생각해 볼 수 있었습니다.

오늘은 그중 제가 선택한 방법에 대해 설명해 보겠습니다.


첫 번째 방법: 직관적인 접근법

이 방법은 제가 실제로 프로젝트에 적용한 방법입니다.

장점
구현 난이도가 낮고 코드가 직관적입니다.
계층 간 데이터 흐름이 명확합니다.
단점
View와 ViewController 간의 강결합이 발생합니다.
큰 프로젝트에서는 유지보수에 어려움이 발생합니다.

 

구체적인 방식

1. View에 있는 TextField의 text 속성을 public으로 설정합니다.
2. 데이터 흐름이 발생할 때 ViewController에서 View의 text 속성에 접근합니다.
3. 이를 통해 데이터를 Interactor에 전달합니다.


선택한 이유

이 방법은 View와 ViewController의 강결합이라는 단점이 있지만, 현재 프로젝트에서는 적절하다고 판단했습니다.
그 이유는 다음과 같습니다
1. 현재 사용하는 클린 아키텍처에서는 View와 ViewController가 동일한 UI 레이어에 속합니다.
2. View가 재사용될 경우에도 ViewController와 함께 활용된다고 보았습니다.

결론적으로, 단점이 존재하지만 현재 상황에서는 이 방법이 가장 간단하고 실용적인 선택이라고 판단했습니다.


예제코드

 

TextView는 사용자로부터 데이터를 입력받고 UI를 관리합니다.

// TextView.swift

import UIKit

protocol TextViewDelegate: AnyObject {
    func didTapSaveButton()
}

final class TextView: UIView {
    weak var delegate: TextViewDelegate?
    
    private let titleLabel: UILabel = {
        let label = UILabel()
        label.text = "Title"
        return label
    }()
    
    private let titleTextField: UITextField = {
        let textField = UITextField()
        textField.placeholder = "Enter Title"
        textField.borderStyle = .roundedRect
        return textField
    }()
    
    private let descriptionLabel: UILabel = {
        let label = UILabel()
        label.text = "Description"
        return label
    }()
    
    private let descriptionTextView: UITextView = {
        let textView = UITextView()
        textView.layer.borderWidth = 1
        textView.layer.borderColor = UIColor.lightGray.cgColor
        textView.layer.cornerRadius = 8
        return textView
    }()
    
    private lazy var saveButton: UIButton = {
        let button = UIButton(type: .system)
        button.setTitle("Save", for: .normal)
        button.setTitleColor(.white, for: .normal)
        button.backgroundColor = .blue
        button.layer.cornerRadius = 8
        button.addTarget(self, action: #selector(saveButtonTapped), for: .touchUpInside)
        return button
    }()
    
    override init(frame: CGRect) {
        super.init(frame: frame)
        setupUI()
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    private func setupUI() {
        addSubview(titleLabel)
        addSubview(titleTextField)
        addSubview(descriptionLabel)
        addSubview(descriptionTextView)
        addSubview(saveButton)
        
        titleLabel.translatesAutoresizingMaskIntoConstraints = false
        titleTextField.translatesAutoresizingMaskIntoConstraints = false
        descriptionLabel.translatesAutoresizingMaskIntoConstraints = false
        descriptionTextView.translatesAutoresizingMaskIntoConstraints = false
        saveButton.translatesAutoresizingMaskIntoConstraints = false
        
        NSLayoutConstraint.activate([
            titleLabel.topAnchor.constraint(equalTo: safeAreaLayoutGuide.topAnchor, constant: 20),
            titleLabel.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 20),
            
            titleTextField.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 10),
            titleTextField.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 20),
            titleTextField.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -20),
            
            descriptionLabel.topAnchor.constraint(equalTo: titleTextField.bottomAnchor, constant: 20),
            descriptionLabel.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 20),
            
            descriptionTextView.topAnchor.constraint(equalTo: descriptionLabel.bottomAnchor, constant: 10),
            descriptionTextView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 20),
            descriptionTextView.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -20),
            descriptionTextView.heightAnchor.constraint(equalToConstant: 100),
            
            saveButton.topAnchor.constraint(equalTo: descriptionTextView.bottomAnchor, constant: 20),
            saveButton.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 20),
            saveButton.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -20),
            saveButton.heightAnchor.constraint(equalToConstant: 50)
        ])
    }
    
    @objc private func saveButtonTapped() {
        delegate?.didTapSaveButton()
    }
    
    var titleText: String {
        return titleTextField.text ?? ""
    }
    
    var descriptionText: String {
        return descriptionTextView.text
    }
    
    func setTitle(_ title: String) {
        titleTextField.text = title
    }
    
    func setDescription(_ description: String) {
        descriptionTextView.text = description
    }
}

 

코드 설명:

  • saveButtonTapped는 사용자가 Save 버튼을 눌렀을 때 호출됩니다. 이 메서드는 delegate를 통해 ViewController로 이벤트를 전달합니다.
  • titleText와 descriptionText는 View에서 현재 입력된 텍스트를 ViewController가 가져갈 수 있도록 제공합니다.

TextViewController는 View와 Interactor를 연결하며, 데이터의 흐름을 조정합니다.

// TextViewController.swift

import UIKit

final class TextViewController: UIViewController {
    private let contentView = TextView()
    private let interactor: TextInteractor
    
    init(interactor: TextInteractor) {
        self.interactor = interactor
        super.init(nibName: nil, bundle: nil)
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    override func loadView() {
        view = contentView
    }
    
    override func viewDidLoad() {
        super.viewDidLoad()
        contentView.delegate = self
        interactor.loadTextData()
    }
}

extension TextViewController: TextViewDelegate {
    func didTapSaveButton() {
        let title = contentView.titleText
        let description = contentView.descriptionText
        
        interactor.updateTitle(title)
        interactor.updateDescription(description)
        interactor.saveTextData()
    }
}

extension TextViewController: TextInteractorDelegate {
    func didLoadTextData(title: String, description: String) {
        contentView.setTitle(title)
        contentView.setDescription(description)
    }
}

 

코드 설명:

  • didTapSaveButton은 TextView에서 전달받은 이벤트를 처리하고, 데이터를 Interactor에 전달합니다.
  • didLoadTextData는 Interactor로부터 전달받은 데이터를 View에 설정합니다.

Interactor는 텍스트 데이터를 관리하며, 데이터를 업데이트하거나 저장하는 역할을 담당합니다.

// TextInteractor.swift

import Foundation

protocol TextInteractorDelegate: AnyObject {
    func didLoadTextData(title: String, description: String)
}

protocol TextInteractor {
    func updateTitle(_ title: String)
    func updateDescription(_ description: String)
    func loadTextData()
    func saveTextData()
}

final class TextInteractorImpl: TextInteractor {
    private var title: String = ""
    private var description: String = ""
    
    weak var delegate: TextInteractorDelegate?
    
    func updateTitle(_ title: String) {
        self.title = title
    }
    
    func updateDescription(_ description: String) {
        self.description = description
    }
    
    func loadTextData() {
        delegate?.didLoadTextData(title: title, description: description)
    }
    
    func saveTextData() {
        print("Saving data...")
        print("Title: \(title)")
        print("Description: \(description)")
    }
}


코드 설명:

  • updateTitle과 updateDescription 메서드는 ViewController로부터 전달받은 데이터를 내부 상태로 저장합니다.
  • loadTextData는 저장된 데이터를 delegate를 통해 ViewController로 전달합니다.
  • saveTextData는 데이터를 저장하는 로직을 담당하며, 현재는 콘솔에 출력하는 방식으로 구현되어 있습니다
  • Interactor에서 데이터 상태를 관리하면서 데이터의 일관성을 보장할 수 있습니다.
  • 도메인레이어인 Interactor에서 데이터의 처리, UI레이어인 화면만 책임을 지기 때문에 좀 더 명확한 책임 분리가 일어납니다.

마무리

오늘은 클린 아키텍처를 설계하며 발생한 View → ViewController → Interactor의 데이터 흐름을 어떻게 설계하고 활용했는지 정리해 보았습니다.

 

비록 View와 ViewController 간에 강결합이라는 단점이 있지만,

현재 프로젝트에서는 두 계층이 동일한 UI 레이어에 속하며, 이 강결합이 큰 문제가 되지 않을 것이라 판단했습니다.

 

처음 클린 아키텍처를 설계하고 활용하다 보니, 직관적인 데이터 흐름이 구현 난이도가 낮고 현재 상황에 적합하다고 판단하여 이 방법을 선택하게 되었습니다.

 

다음 게시물에서는 delegate 패턴을 활용해 강결합을 완화하는 방법과, ViewModel을 활용하여 데이터 흐름을 개선하는 방법에 대해 정리해 보겠습니다.

더 나은 구조를 고민하며 개선해 나가는 과정을 공유할 예정이니 기대해 주세요!