[#317] Canvas에서 메인스레드가 아닌 스레드에서 이미지를 비동기적으로 가져오도록 개선한다#318
Conversation
YunDaeHyeon
left a comment
There was a problem hiding this comment.
📄 PR Code Review Report
🔍 Summary
이번 PR은 이미지 로딩 및 렌더링 과정을 동기 방식에서 비동기 방식으로 전환하여 앱의 반응성과 안정성을 크게 향상시키는 중요한 변경 사항을 포함합니다. 특히, 메인 스레드를 블로킹할 수 있는 동기 파일 I/O를 제거하고, 전용 PhotoImageLoader를 통해 백그라운드에서 이미지를 로드하도록 개선한 점이 긍정적입니다. 전반적으로 코드의 견고함과 성능이 개선되었습니다.
⚠️ Key Issues
- 동시성 비효율성 (PhotoImageLoader.loadImages):
PhotoImageLoader.loadImages함수는 여러 이미지를 순차적으로 로드하도록 구현되었습니다. 각 이미지를await loadImage(from: photo.url)호출로 개별적으로 비동기 처리하지만,for루프 내에서 순차적으로 기다리므로, 여러 이미지가 있다면 총 로딩 시간이 각 이미지 로딩 시간의 합이 됩니다. 이는 불필요한 직렬화로 이어져 전체 이미지 로딩 성능을 저하시킬 수 있으며, 특히 렌더링에 필요한 이미지 수가 많을 때 사용자 경험에 영향을 줄 수 있습니다.
🛠 Improvement Suggestions
- PhotoImageLoader.loadImages 병렬 처리: 위 'Key Issues'에서 언급했듯이,
PhotoImageLoader.loadImages내부에서await withTaskGroup또는async let을 사용하여 여러 이미지를 병렬로 로드하도록 변경하면 전체 로딩 시간을 단축할 수 있습니다. 이는 특히 이미지 수가 많을 때 렌더링 성능에 크게 기여할 수 있습니다. - PhotoFramePreview의
synchronizePhotoImages에서 로깅 강화: 현재loadImage실패 시continue하여 다음 이미지로 넘어갑니다. 이는 기능적으로 올바르지만, 어떤 이미지 로딩이 실패했는지 알기 어렵습니다. 디버깅 및 잠재적인 문제 진단을 위해 이미지 로딩 실패 시 로그를 남기는 것을 고려해볼 수 있습니다.
✅ Positive Observations
- 메인 스레드 블로킹 문제 해결:
Photo모델에서imageData계산 속성을 제거하고 이미지 로딩을PhotoImageLoader를 통해 비동기로 처리하도록 변경한 점은 매우 긍정적입니다. 이는 UI 스레드 블로킹을 방지하고 앱의 반응성을 크게 향상시킵니다. - 적절한 Swift Concurrency 도입:
async/await및Task.detached,.task수정자를 적절하게 활용하여 비동기 작업을 안전하고 효율적으로 관리하고 있습니다. 특히Task.isCancelled체크를 통해 작업 취소 가능성을 고려한 점은 매우 좋습니다. - UI 렌더링과 데이터 로딩 분리:
PhotoComposer가PhotoImageLoader를 통해 이미지를 미리 로드한 후PhotoFramePreview에 전달하도록 구조화하여, 렌더링 시점에 이미지가 모두 준비되도록 한 점은 효율적이고 안정적인 UI 렌더링을 보장합니다. UIImage(contentsOfFile:)및 경로 처리:PhotoImageLoader에서UIImage(contentsOfFile: photoUrl.path(percentEncoded: false))를 사용하여 로컬 파일에서 이미지를 로드하는 효율적인 방법을 택했고,percentEncoded: false옵션으로 URL 경로의 인코딩 문제를 방지한 점은 세심한 처리입니다.- 뷰 초기화 시
@State초기화:PhotoFramePreview의init에서 외부photoImages를 받아_photoImagesByURL = State(initialValue: ...)방식으로@State를 초기화한 방식은 매우 올바르고 안전한 SwiftUI 패턴입니다.
GRJeon
left a comment
There was a problem hiding this comment.
이미지 로딩을 일원화 하셨다고 하셨는데 기존에 사용하던 LocalAsyncImage는 이번 변경 범위 외인가요? AI리뷰 중 비동기 문제에 대한 의견도 궁금합니다.
@GRJeon AI 리뷰 중 이미지 병렬 로드는 마지막 영상에서 보듯이 모든 이미지가 바로 나와서 생각하지 못한 부분입니다. 오래 걸리지 않을 작업같아서 곧바로 해보겠습니다 👍
|
sangYuLv
left a comment
There was a problem hiding this comment.
메인 스레드에서 이미지를 로딩해서 찝찝했던 부분을 개선해주셨군요 👍👍
| struct PhotoFramePreview: View { | ||
| let information: PhotoInformation | ||
| private let hasPreloadedPhotoImages: Bool | ||
| @State private var photoImagesByURL: [URL: UIImage] |
There was a problem hiding this comment.
PhotoCompositionView에서 PhotoFramePreview가 만들어지는 형태를 보면, 사진이 선택될 때마다 (상태가 변할 때마다) 새로운 PhotoFramePreview가 만들어지고 있습니다.
그래서 시도하신 캐싱 로직은 수행되지 않는 상태인 듯 합니다...!
해당 흐름 다시 한 번 확인부탁드립니다!
PhotoFramePreview(
information: PhotoInformation(
layout: store.state.selectedLayout,
frame: store.state.selectedFrame,
photos: store.state.selectedPhotos
)
)더해서 캐싱을 적용한다면, 이미지를 렌더링하는 CPU 사용률은 줄어들겠지만 UIImage 저장으로 인해 메모리 사용량은 늘어날 듯 한데요,
개선 이전에도, 개선 후에도 사용 측면에서는 버벅임이 없지만 이런 사용량 변화에 대한 분석도 같이 공유해주시면 좋을 듯 합니다.
There was a problem hiding this comment.
피드백 감사합니다!
- PhotoFramePreview
아시겠지만 SwiftUI는 상태가 변경되면 그 뷰를 완전히 새로 그리지 않고 재계산을 통해 알아서 뷰를 재사용하는 형태입니다. 특히 PhotoFramePreview에서 자주 변경이 되는 selectedPhotos 가 채택한 Photo 타입은 Identifiable을 채택함으로서 뷰 계산에 영향을 주고 있어서 selectedPhotos가 변경된다고 PhotoFramePreview가 완전히 다시 그려진다고 보기엔 좀 어려울것 같습니다. 아래 영상은 selectedPhotos가 변경되는 것과 별개로 PhotoFramePreview의 onAppear가 최초 한번만 뜨는 것을 확인할 수 있는 영상입니다
2026-03-13.3.54.12.mov
- 메모리 사용 및 캐싱
UIImage를 저장하는 딕셔너리(photoImagesByURL) 가 실제로 PhotoFramePreview에서의 메모리 캐싱 역할을 하고 있습니다. PhotoFramePreview를 설명한 대로, 뷰가 재계산되는 것이니 캐싱도 정상적으로 되는것을 확인할 수 있습니다. (아래 이미지 참고)
다만 변경 전과 변경 후에 대해 명확하게 설명하지 않는 것은 PR 영상과 기존 리팩토링 문서 아래쪽의 첫번째 영상과 비교가 가능하다고 생각하여 작성하지 않았는데 작성하는게 오히려 나았을 것 같네요. 요약 정리만 하자면, 개선 전은 최대 410MB, 개선 후는 최대 390MB 사용량을 보여 메모리 사용 개선이 조금 있었다고 말씀드릴 수 있겠습니다!
sangYuLv
left a comment
There was a problem hiding this comment.
사진 선택 상태가 바인딩 되는 형태가 아니고, PhotoFramePreview의 초기화 매개변수로 전달되어서 그 상태가 바뀔 때마다 새로 생긴다고 생각했었습니다.
PhotoInformation도 struct다보니 캐싱이 잘 동작하고 있는 것인지 의아했었네요
사실 아직도 어떻게 상태 변경이 반영되는지 이해가 되진 않지만, 확인해주셔서 감사합니다!
지금과 같은 상태 변경으로 CPU 사용률이 줄고 메모리 사용량이 늘 것이라고 예측했는데 오히려 메모리 사용이 줄었다는 것도 신기하네요
시간이 되신다면 여러 사진을 선택했다가 적은 사진을 선택하는 상황도 개선 전/후로 비교해보시면 좋을 것 같아요!
(저한테 증명해달라는 말은 아닙니다 😅)
제가 지금 가지는 의문들은 따로 학습하면서 파헤칠게요!
수고 많으셨습니다 👍
📝 작업 내용
📌 요약
Photo가 직접 이미지 데이터를 읽지 않도록 정리하고, 이미지 로딩 책임을PhotoImageLoader로 일원화했습니다.PhotoFramePreview가 필요한 이미지를 비동기로 불러오도록 개선했습니다.PhotoComposer.render를 비동기 처리로 전환해 렌더링 전에 이미지를 미리 로드하고, 결과 화면에서도 해당 흐름을 반영했습니다.🔍 상세
Photo.imageData계산 프로퍼티를 제거해 모델이 파일 I/O를 직접 수행하지 않도록 수정했습니다.PhotoImageLoader를 추가해 단일 이미지와 다중 이미지 로딩을 한 곳에서 처리하도록 구성했습니다.PhotoFramePreview는[URL: UIImage]상태를 사용해 이미 로드된 이미지를 재사용하고, 없는 이미지만.task에서 비동기로 불러오도록 변경했습니다.PhotoComposer는 합성 렌더링 전에 필요한 이미지를 먼저 로드한 뒤PhotoFramePreview에 전달하도록 수정했습니다.ResultView는 렌더링 결과 생성 시await PhotoComposer.render(with:)를 호출하도록 변경했습니다.💬 리뷰 노트
Instruments 시도
print문으로 메인 스레드에서 동작하는지 확인
Thread.isMainThread를 통해 해당 코드가 메인스레드에서 동작하는지를 확인했습니다2026-03-09.3.19.00.mov
📸 영상 / 이미지 (Optional)
정상 작동 확인 및 메모리 사용량 확인
2026-03-09.2.21.19.mov
2026-03-09.10.45.52.mov