[SwiftUI] SwiftUI 상태 동기화 트러블슈팅

2025. 6. 25. 15:36·Apple/SwiftUI
728x90
반응형

안녕하세요! 피피아노입니다 🎵

 

이번 포스팅에서는 제가 프로젝트를 진행하면서 발생한 문제와 해결 과정을 공유해보려고 합니다.

 

그럼 바로 시작하겠습니다!

 

프로젝트 소개

우선 프로젝트에 대해서 간단하게 소개해보자면 인생네컷처럼 내 앨범에서 사진을 골라서 네 컷 사진을 만들 수 있는 앱을 개발하고 있었습니다.

 

사용자가 최대 4장의 사진을 선택하고, 원하는 사진을 삭제한 후, 새로운 사진을 추가할 수 있는 기능도 추가로 구현했습니다.

 

기본 구조는 아래처럼 설계를 했습니다.

  • ContentView: 사진 선택 및 관리(네컷 이미지를 만드는 View)
  • FourCutFrameModel: 프레임 데이터 모델
  • PhotoModel: 개별 사진 데이터

문제 발견 과정

앱의 기본 기능은 완성하였지만 기능을 더 확장하고 싶다는 생각이 들었습니다. 완전 프로젝트 초기에는 큰 프로젝트도 아니었고 사용되는 기능도 별로 없어서 View안에 로직들도 함께 담았지만 기능을 추가하려고 하니 의존도와 복잡도가 증가하면서 앱의 속도도 느려지고 컴파일 속도도 느려져서 MVVM 디자인패턴을 도입하기로 하였습니다.

 

그리고 이 MVVM 디자인패턴을 도입하고 나서 다시 QA 테스트 중 다음과 같은 상황이 발생하였습니다.

 

예상 시나리오

  • 4장 사진 서택 -> [사진1, 사진2, 사진3, 사진4]
  • 1번, 4번 사진 삭제 -> [nil, 사진2, 사진3, nil]
  • 1장 새 사진 추가 -> [새사진, 사진2, 사진3, nil]

실제 발생한 현상

  • 4장 사진 서택 -> [사진1, 사진2, 사진3, 사진4]
  • 1번, 4번 사진 삭제 -> [nil, 사진2, 사진3, nil]
  • 1장 새 사진 추가 -> [새사진, 사진2, 사진3, 기존 사진4 복구]

이런 식으로 마지막 배열은 nil이 나와야 하는데 처음에 넣었던 사진4가 복구되는 문제였습니다.

 

가설 세우기

문제 해결을 하기 위해 해당 문제가 발생되는 가설을 세워보았습니다.

  1. UI 렌더링 문제: SwiftUI View 업데이트 지연?
  2. 데이터 모델 문제: FourCutFrameModel에서 상태 동기화 실패?
  3. 로직 문제: loadTransferable() 함수에서 인덱스 처리 오류?

이렇게 3가지의 가설을 세우고 각각의 코드에 디버깅 로그를 추가하면서 디버깅을 시작하였습니다.

문제 원인 추적

func loadTransferable() async {
    print("=== 새 사진 추가 시작 ===")
    print("선택된 사진 개수: \(selectedPhotos.count)")
    
    // 현재 상태 확인
    let currentState = displayedImages.enumerated().map { index, image in 
        return "\(index): \(image == nil ? "nil" : "image")"
    }.joined(separator: ", ")
    print("현재 상태: [\(currentState)]")
    
    // 빈 자리 찾기
    let emptyIndices = displayedImages.enumerated().compactMap { index, image in
        image == nil ? index : nil
    }
    print("빈 자리 인덱스들: \(emptyIndices)")
    
    for (index, photoItem) in selectedPhotos.prefix(4).enumerated() {
        print("처리 중인 사진 인덱스: \(index)")
        do {
            if let imageData = try await photoItem.loadTransferable(type: Data.self),
               let uiImage = UIImage(data: imageData) {
                let photo = PhotoModel(uiImage: uiImage)
                print("사진을 \(index)번 자리에 배치 예정")
                frameModel.setPhoto(photo, at: index) // 의심 지점!
                
                // 배치 후 상태 확인
                let afterState = frameModel.displayedImages.enumerated().map { idx, img in 
                    return "\(idx): \(img == nil ? "nil" : "image")"
                }.joined(separator: ", ")
                print("배치 후 상태: [\(afterState)]")
            }
        } catch {
            print("이미지 로드 실패: \(error)")
        }
    }
    
    selectedPhotos.removeAll()
    self.displayedImages = frameModel.displayedImages
    print("=== 최종 결과 ===")
    let finalState = displayedImages.enumerated().map { index, image in 
        return "\(index): \(image == nil ? "nil" : "image")"
    }.joined(separator: ", ")
    print("최종 상태: [\(finalState)]")
}

로그를 추가하고 분석을 해보니 새 사진을 1장만 추가했는데 인덱스 0번에 배치하는 순간 인덱스 3번도 함께 채워지는 로그를 확인하였습니다.

 

문제는 frameModel.setPhoto(photo, at: index)에서 발생하고 있었습니다.

모델 내부 분석

기존 loadTransferable() 메서드를 분석해 보니 여러 문제점을 발견했습니다.

// 문제가 있던 기존 코드
func loadTransferable() async {
    var newImages: [Image] = []
    
    // 1. 새로운 이미지들을 임시 배열에 저장
    for photoItem in selectedPhotos {
        // ... 이미지 로드 로직
        newImages.append(image)
    }
    
    // 2. displayedImages를 복사하여 작업
    var updatedImages = displayedImages
    
    // 3. 빈 자리를 찾아서 새 이미지 배치
    var emptyIndices: [Int] = []
    for i in 0..<4 {
        if updatedImages[i] == nil {
            emptyIndices.append(i)
        }
    }
    
    // 4. frameModel 동기화
    frameModel.setImages(updatedImages)
}

 

 

핵심 문제점

1. 이중 상태 관리

displayedImages와 frameModel.photos가 별도로 관리되어 동기화에 문제가 발생하였습니다.

2. 복잡한 로직 처리

MVVM 디자인패턴으로 변경하면서 여러 가지 코드가 꼬일 때 제가 로직을 추가했던 것이 여기에서 발목을 잡고 있었습니다.

3. 상태 불일치

frameModel 업데이트와 displayedImages 업데이트가 따로 이루어져서 일관성이 부족한 문제도 존재했습니다.

해결 방안 설계

문제의 근본적인 원인은 상태가 2군데에서 관리되고 있다는 점이었습니다. 이러한 문제를 해결하기 위해 다음과 같은 해결책을 세웠습니다.

1. 이중 상태 관리 통합

frameModel을 유일한 데이터 소스로 설정

2. 간단한 로직으로 변경

복잡한 빈자리 찾기 로직 제거(불필요한 코드)

3. 순차적 배치

새로운 사진들을 순서대로 배치하는 로직 채택

 

func loadTransferable() async {
    // 1. frameModel에 직접 PhotoModel 객체 저장
    for (index, photoItem) in selectedPhotos.prefix(4).enumerated() {
        do {
            if let imageData = try await photoItem.loadTransferable(type: Data.self),
               let uiImage = UIImage(data: imageData) {
                let photo = PhotoModel(uiImage: uiImage)
                frameModel.setPhoto(photo, at: index) // 직접 frameModel 업데이트
            }
        } catch {
            print("이미지 로드 실패: \(error)")
        }
    }
    selectedPhotos.removeAll()
    self.displayedImages = frameModel.displayedImages // frameModel에서 UI 상태 동기화
}

 

개선된 점

  • 버그 해결: 위에서 나타난 오류가 해결됨
  • 일관성: 상태가 항상 일정하게 유지됨
  • 유지 보수성: 코드가 간단해짐
  • 성능: 불필요한 배열 복사와 같은 복잡한 로직 제거로 성능 향상

마무리

이번 트러블슈팅을 통해 SwiftUI는 여러 곳에서 같은 데이터를 관리하면 동기화 문제가 발생할 수 있다는 것을 확실하게 느꼈고 Single Source of Truth 원칙을 지키는 것이 중요하다는 것을 다시 알게 되었습니다.

 

비슷한 문제로 고민하시는 분들이 있다면 이 글이 조금이나마 도움이 되면 좋겠습니다!


감사합니다.

 

잘못된 내용이 있거나 더 좋은 내용 피드백은 언제나 환영합니다!

궁금하신 부분은 댓글로 질문 부탁드립니다!

 

728x90
반응형

'Apple > SwiftUI' 카테고리의 다른 글

[SwiftUI] 앱에 Face ID 잠금 기능 적용하기  (8) 2025.07.08
[SwiftUI] CADisplayLink를 활용한 부드러운 ProgressView 애니메이션 구현 트러블슈팅  (6) 2025.06.06
[SwiftUI] NavigationStack 사용 시 화면 전환 안 되는 문제와 title 깨짐 문제 트러블슈팅  (0) 2025.06.03
[SwiftUI] SwiftUI로 카메라 기능 구현하기  (2) 2025.03.01
[SwiftUI] ProgressView 생성하기  (15) 2025.01.15
'Apple/SwiftUI' 카테고리의 다른 글
  • [SwiftUI] 앱에 Face ID 잠금 기능 적용하기
  • [SwiftUI] CADisplayLink를 활용한 부드러운 ProgressView 애니메이션 구현 트러블슈팅
  • [SwiftUI] NavigationStack 사용 시 화면 전환 안 되는 문제와 title 깨짐 문제 트러블슈팅
  • [SwiftUI] SwiftUI로 카메라 기능 구현하기
P_Piano
P_Piano
Apple 생태계 개발자가 되기 위한 학습과 경험의 기록
    반응형
    250x250
  • P_Piano
    피피아노의 개발 일지
    P_Piano
  • 전체
    오늘
    어제
    • 분류 전체보기 (211) N
      • Apple (129) N
        • iOS (22)
        • visionOS (4)
        • Swift (69) N
        • UIKit (2)
        • SwiftUI (24)
        • RxSwift (2)
        • Xcode (5)
      • C언어 (5)
      • C++ (8)
      • Dart (1)
      • Python (3)
      • JavaScript (17)
      • Git (1)
      • CS (39)
        • 디자인 패턴 (6)
        • 네트워크 (20)
        • 운영체제 (8)
        • Database (5)
        • 자료구조 (0)
      • IT 지식 (2)
      • IT 뉴스 (4)
      • 출처 표기 (1)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

  • 공지사항

  • 인기 글

  • 태그

    비동기
    옵셔널
    스위프트
    swiftUI
    코딩테스트
    Vision Pro
    ios
    운영체제
    네트워크
    자바스크립트
    제어문
    Apple
    프로그래머스
    visionOS
    함수
    프로세스
    UIKit
    combine
    Xcode
    배열
    티스토리챌린지
    프로퍼티 래퍼
    클래스
    연산자
    Initializers
    SWIFT
    이니셜라이저
    오블완
    디자인패턴
    변수
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.1
P_Piano
[SwiftUI] SwiftUI 상태 동기화 트러블슈팅
상단으로

티스토리툴바