Search

Clean Swift 적용해보기(3) - 이전 화면으로 값 전달하기

하단 북마크 The Clean Swift Handbook에 나오는 Example 코드를 참고하여 Image Collection Example를 만들었습니다.
Clean Swift 개념은 The Clean Swift 에 작성해두었습니다.

 샘플 프로젝트 만들기 시작

이전에 MVP, MVVM 패턴으로 만들어 두었던 Image Example 프로젝트를 Clean Swift를 적용한 방식으로 수정해보려고 합니다.
기존 프로젝트에는 이미지를 가져와서 보여주는 메인 화면만 존재합니다. 이번에 Clean Swift를 적용하면서 상세 화면과 인덱스 변경 화면을 추가로 만들 예정입니다.
상세 화면 구현에 이어 데이터 전달에 대한 코드를 수정해보려고 합니다.
*상세 화면 구현은 Clean Swift 적용해보기(2) - 상세 화면 만들어보기 에서 보실 수 있습니다.
이전에 상세 화면에서 인덱스 변경을 진행한 후, 메인 화면으로 진입했을 때 변경된 인덱스가 반영되지 않는 이슈가 있었습니다. 해당 문제를 해결해보려고 합니다.
*화면 UI 로직에 대한 설명은 따로 넣어두지 않았습니다. 화면 구현 방식이 궁금하시다면 레포지토리에서 코드를 참고해주세요.

 데이터 전달 문제 해결

다음 화면으로는 데이터 전달이 되지만 이전 화면으로 변경된 데이터를 전달할 때, 전달이 안되는 이슈가 있었습니다.
왜 이전 화면으로 데이터 전달이 안되었을까?
Router에서 이전 화면으로 값을 전달할 때, 이미 띄워져 있는 이전 화면으로 값을 전달한 것이 아닌 새로운 클래스로 값을 전달하고 있었기 때문입니다.
public func routeToImageDetail() { let destinationVC = ImageDetailViewController() // 이전 화면이 아닌 새로운 클래스 선언 var destinationDS = destinationVC.router!.dataStore! passDataToImageDetail(source: dataStore!, destination: &destinationDS) navigateToImageDetail(source: viewController!, destination: destinationVC) } private func passDataToImageDetail(source: ImageCollectionDataStore, destination: inout ImageDetailDataStore) { destination.imageURL = source.imageURL }
Swift
복사
Present 시켜준 화면으로 값을 전달하는 것이 아니니 변경된 값이 설정되지 못했던 겁니다.
어떻게 수정해야 할까요?
Delegate 패턴을 사용해서 이전 화면으로 값을 전달해줍시다.

⓵ 인덱스 변경 → 이미지 상세 코드 수정

먼저, ImageIndexEditRouterDelegate 프로토콜을 하나 생성합니다.
해당 프로토콜 내에 있는 dismissWithSuccess 메서드를 사용해서 변경된 Index를 전달할 예정입니다.
protocol ImageIndexEditRouterDelegate: AnyObject { func dismissWithSuccess(changedIndex: Int) }
Swift
복사
Router 내부에 delegate 프로퍼티를 weak하게 선언해주고, index 값을 넘겨받는 메서드를 하나 생성합니다.
weak var delegate: ImageIndexEditRouterDelegate? public func routeToImageDetail(with changedIndex: Int) { delegate?.dismissWithSuccess(changedIndex: changedIndex) viewController?.dismiss(animated: true) }
Swift
복사
해당 메서드는 ViewController에서도 호출할 수 있도록 RoutingLogic에도 추가해줍니다.
protocol ImageIndexEditRoutingLogic { func routeToImageDetail() func routeToImageDetail(with changedIndex: Int) // 추가 }
Swift
복사
routeToImageDetail 호출하던 부분에서 index까지 전달할 수 있도록 메서드를 수정해줍니다.
public func displaySaveIndex(viewModel: ImageIndexEdit.SaveIndex.ViewModel) { guard let index = viewModel.index else { showAlert(title: "에러 발생", message: "잘못된 값이 전달되었습니다.") return } router?.routeToImageDetail(with: index) }
Swift
복사
Delegate를 사용하기 위해선 ImageIndexEditViewController에 있는 setup 메서드 내에서 delegate를 주입해주어야 합니다. 하지만, 이전 화면 Router가 Delegate를 conform하기 때문에 ViewController의 init 메서드에서 delegate를 주입해주지 않으면 delegate를 설정할 수가 없습니다.
init 메서드에서 delegate를 받아서 넘겨줄 수 있도록 코드를 수정합니다.
init(delegate: ImageIndexEditRouterDelegate) { super.init(nibName: nil, bundle: nil) setup(delegate: delegate) } private func setup(delegate: ImageIndexEditRouterDelegate) { let viewController = self let interactor = ImageIndexEditInteractor() let presenter = ImageIndexEditPresenter() let router = ImageIndexEditRouter() router.delegate = delegate viewController.interactor = interactor viewController.router = router interactor.presenter = presenter presenter.viewController = viewController router.viewController = viewController router.dataStore = interactor }
Swift
복사
인덱스 변경 화면 쪽에 있는 코드들은 모두 수정된 거 같습니다. 상세 화면도 마저 완료해봅시다.
상세 화면 Router가 ImageIndexEditRouterDelegate를 conform하도록 합니다.
그리고 해당 메서드 내에서 상세 화면 Index를 변경된 Index로 수정하도록 코드를 작성합니다.
extension ImageDetailRouter: ImageIndexEditRouterDelegate { func dismissWithSuccess(changedIndex: Int) { self.dataStore?.index = changedIndex } }
Swift
복사
상세 화면 Router가 RouterDelegate를 conform하고 있기 때문에 ImageIndexEditViewController delegate 인수로 자기 자신을 넘겨줍니다.
public func routeToSheetView() { let destinationVC = ImageIndexEditViewController(delegate: self) var destinationDS = destinationVC.router!.dataStore! if let sheet = destinationVC.sheetPresentationController { sheet.detents = [.medium()] sheet.largestUndimmedDetentIdentifier = .medium sheet.prefersScrollingExpandsWhenScrolledToEdge = false sheet.prefersEdgeAttachedInCompactHeight = true sheet.widthFollowsPreferredContentSizeWhenEdgeAttached = true } passDataToSheetView(source: dataStore!, destination: &destinationDS) navigationToSheetView(source: viewController!, destination: destinationVC) }
Swift
복사
여기까지만 구현하면 인덱스 변경은 되지만 메인 화면으로 이동했을 때는 변경된 값이 반영되지 않을겁니다.
이번엔 상세 화면에서 메인 화면으로 변경된 값이 전달되도록 해줍시다.

⓶ 이미지 상세 → 메인 코드 수정

상세 화면에서 메인 화면으로 데이터 전달하는 것도 위와 동일한 방법으로 구현해주면 됩니다.
DetailRouter에도 EditIndexRouter와 동일하게 Delegate를 하나 만듭니다.
protocol ImageDetailRouterDelegate: AnyObject { func dismissWithChange(index: Int) }
Swift
복사
ImageDetailRouter가 delegate를 프로퍼티로 가질 수 있도록 하나 생성해준 뒤, 메인 화면으로 pop 되는 부분에 delegate 메서드를 호출할 수 있도록 해줍니다.
weak var delegate: ImageDetailRouterDelegate? public func routeToImageCollection() { let destinationVC = ImageCollectionViewController() navigateToImageCollection(source: viewController!, destination: destinationVC) } private func navigateToImageCollection(source: ImageDetailViewController, destination: ImageCollectionViewController) { if let index = dataStore?.index { delegate?.dismissWithChange(index: index) } source.navigationController?.popViewController(animated: true) }
Swift
복사
상세 화면에서는 dataStore에 있는 index를 가져와서 delegate 메서드로 전달할 수 있도록 했습니다.
이제 메인 화면에서 해당 값을 사용할 수 있도록 코드를 수정해봅시다.
이전과 동일하게 메인 Router가 상세 RouterDelegate 프로토콜을 conform하도록 합니다.
extension ImageCollectionRouter: ImageDetailRouterDelegate { func dismissWithChange(index: Int) { dataStore?.changedIndex = index } }
Swift
복사
그리고 이전처럼 자기자신을 ImageDetailViewController 에 넘겨줍니다.
public func routeToImageDetail() { let destinationVC = ImageDetailViewController(delegate: self) var destinationDS = destinationVC.router!.dataStore! passDataToImageDetail(source: dataStore!, destination: &destinationDS) navigateToImageDetail(source: viewController!, destination: destinationVC) }
Swift
복사
여기까지 하면 아마 값을 변경된 상태로 잘 전달될 겁니다.
한 번 확인해볼까요?
잘 동작합니다.

 마무리하며

이전에 사용해보았던 MVP나 MVVM 패턴과 비교해 보았을 때, Clean Swift는 훨씬 구조적으로 잘 짜여진 패턴이라는 인상을 받았습니다.
특히 MVVM의 경우, ViewModel 내부에 비즈니스 로직과 포맷팅 로직이 혼재되는 경우가 자주 발생했었습니다. 하지만 Clean Swift에서는 이러한 로직들을 Interactor와 Presenter로 명확하게 분리할 수 있었고, 이를 통해 코드의 가독성과 유지보수성이 크게 향상되었다고 느꼈습니다.
Clean Swift의 저자는 이 패턴을 적용했을 때 정말 중요한 부분에 집중할 수 있다는 점을 자주 강조했습니다. 실제로 제가 Clean Swift를 프로젝트에 적용해 보니, 이전보다 비즈니스 로직에 대한 깊은 고민을 하게 되었고, 그 중요성을 체감하게 되었습니다. Clean Swift는 이미 정해진 코드의 위치와 명확한 역할 분담이 있기 때문에, 자연스럽게 비즈니스 로직에 더 많은 집중을 기울이게 되었습니다. 그 덕분에 샘플 코드를 작성할 때도 꼼꼼하게 방어 코드를 추가할 수 있었습니다.
아직 테스트 코드는 작성해보지 않았지만, Clean Swift의 구조를 기반으로 테스트 코드를 작성하게 되면 각 기능별로 세밀한 테스트가 가능할 것이라는 기대감을 갖고 있습니다. 얼른 테스트 코드도 작성하여 이를 직접 경험해보고 싶은 마음이 큽니다.
마지막으로 Clean Swift를 소개해 준 팀원에게 깊은 감사의 마음을 전합니다.
만약 누군가 Clean Swift를 경험해 보고 싶다면, 저는 주저 없이 추천하고 싶습니다.