Replies: 1 comment
-
저희 매크로 팀도 음악 추출 과정에 있어서의 특히 안되는 건 없다는 마지막 문장이 정말 와닿네요,,, 좋은 글 감사합니다!! |
Beta Was this translation helpful? Give feedback.
0 replies
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
-
안녕하세요 3기 나무디입니다.
매크로는 다들 안녕하신가요? 저희 팀은 이번에 스프린트를 끝내고 리팩토링과 UX 개선 등의 작업을 진행하고 있는데요.
열심히 달려오던 중 잠깐의 시간이 생겨 개인적으로 하고 있던 ‘럭키비키’ 토이 프로젝트의 성능 개선을 도전해봤습니다.
성능 개선을 도전해보면서 경험한 Combine과 저번에 오스틴이 남겨주신 글을 통해 Xcode Instrument를 활용했던 과정들을 공유해보고자 합니다.
성능 문제의 발견
먼저 ‘럭키비키’의 솔루션은 ‘부정적인 생각을 ChatGPT API를 통해 초긍정적인 생각(럭키비키)로 변환해주자!’ 입니다. 아마 몇몇분은 들어보셨을 ‘원영적 사고 변환기’ 의 모바일 앱 버전인데요. 이 앱에서 ChatGPT API를 통해 변환된 응답을 받을 때, 사용자 경험의 향상을 위해 응답을 완성된 형태로 받는게 아닌 ‘데이터 스트림’ 형태로 받았습니다.
여기서 데이터 스트림이란 무엇인지 짧게 설명하자면, ‘데이터를 연속적으로 전송하거나 처리하는 방식’을 의미합니다. 이를 통해 데이터를 한 번에 가져오는 것이 아니라 ‘작은 조각’으로 나누어 실시간으로 받아 처리할 수 있습니다. 예시를 통해 이해하자면,
이해가 되셨나요? 이렇게 스트리밍 형태로 받은 이유는, 응답이 완성되는 데에 시간이 걸리기 때문에 사용자가 지루해하지 않도록 즉각적인 피드백을 제공하기 위함이었습니다.
그런데 이렇게 데이터를 스트리밍 형태로 받게 되는 경우엔 성능 최적화와 뷰 업데이트 로직의 설계가 잘 되어야 하는데요.
’럭키비키’는 빠르게 개발하다보니 이를 챙기지 못해서 많은 응답을 받는 경우에 화면의 프레임이 저하되는 문제가 발생했습니다.
ScreenRecording_11-17-2024.21-44-59_1.MP4
문제의 원인이 무엇일까 코드를 수정하며 실험적 테스트를 해 본 결과 스트림 데이터를 받을 때 애니메이션을 삭제하면 문제가 발생하지 않았습니다.
응답을 실시간으로 띄워줄 때 애니메이션을 통해 응답의 자연스러운 생성을 보여주고자 한 것인데, 이게 문제였던 것이었죠.
문제 분석
일단 애니메이션 때문에 문제가 발생한다는 것은 알았으니, 정확히 애니메이션이 어떤 동작을 하기에 프레임이 저하되는지에 대해 찾아봤습니다.
또, 이 과정에서 Xcode Instrument를 통해서도 성능 프로파일링을 해보았습니다.
이 프로파일링 결과를 보면 여러 문제들을 예상해 볼 수 있는데요.
먼저 SwiftUI 뷰의 업데이트를 담당하는 View Properties에서 응답을 생성할 때 과도하게 뷰가 업데이트되고 있다는 것을 확인할 수 있었습니다.
이 부분은 데이터 스트림을 받을 때마다 응답을 표시하는 텍스트의 상태가 업데이트되기 때문에 응답이 많아지는 경우 뷰 업데이트도 그만큼 많이 되기 때문이었습니다.
Core Animation Commits에서는 애니메이션의 양과 부하도를 보여주는데요.
빨간 부분은 부하가 많이 발생한다는 것을 의미합니다. 데이터 스트림의 업데이트와 동시에 애니메이션이 실행되기 때문이었습니다.
즉, 응답을 받을 때마다 애니메이션을 적용했기 때문에 애니메이션이 반복 호출되어 성능 문제를 야기하는 것입니다.
이 때문에 데이터 스트림도 처리해야 하고, 뷰 업데이트도 해야하기 때문에 CPU 사용량이 증가하고 앱 중단 또는 느려짐을 의미하는 Hang 섹션에서 일부 구간이 표시되고 있었습니다. Hang 섹션이 표시되는 구간이 실제 프레임 저하가 발생하는 구간을 의미합니다.
문제가 발생하는 부분을 이해하기 쉽도록 예시를 들어보면,
이런 식으로 동작하고 있었던 것입니다. 이러다보니 애니메이션이 계속 중첩되거나 새로 시작되며 부하가 증가했던 것이었죠.
해결 방법
1. 애니메이션 삭제하기
애니메이션 때문에 문제가 발생하니, 애니메이션을 삭제하면 문제가 해결됩니다.
실제로 애니메이션을 삭제하고 프로파일링을 해보았을 때, 과도한 부하가 발생하는 부분도 없었고 표시되는 Hang 섹션도 없어 문제가 해결되었습니다.
하지만 이건 임시 방편적 해결 방안에 불과하고 ‘사용자 경험 향상’ 이라는 목적에 반하기 때문에 다른 방법을 찾아봐야 했습니다.
2. 타이머 사용해보기
단순하지만 타이머를 통해 받은 응답을 일정 주기로 업데이트하여 애니메이션 호출 빈도를 줄이면 되는게 아닌가 생각했습니다.
이렇게 하면 상태 변화가 빈번하지 않고 일정한 간격으로 발생하게 되어서 애니메이션 중첩 문제를 줄일 수 있을 것 같았습니다.
하지만 이 경우 GPT의 응답 속도와는 관계없이 일정 주기로 표시되는 것이기 때문에 응답이 빠르게 완성되더라도,
타이머가 모든 토큰을 가져와 업데이트할 때까지 기다려야 한다는 치명적인 단점이 있었습니다.
GPT의 응답은 20ms만에 완성되었는데 뷰 업데이트는 800ms를 기다려야 하는 것이었죠.
근데 그러면 타이머 업데이트 간격을 더 짧게 설정하면 되는 것 아닐까? 도 생각해봤지만,
이렇게 하더라도 타이머의 생성과 해제, 데이터 스트림과의 생성 시간 불일치 등 여러 문제가 있어 타이머는 근본적인 해결책이 아닌 것 같아 다른 방법을 찾아봤습니다.
3. Combine 사용하기
사실 Combine의 존재는 이미 알고 있었지만, 다른 대안은 정말 없을까 찾아보고자 위의 방법들을 생각했었습니다.
Combine의 기본 개념을 전에 잠깐 공부했던 적이 있었는데 적용해본 적은 없더라도 기본 개념은 알아두었던게 해결 방법을 찾는데 도움이 많이 되었습니다.
Combine의 기본 개념은 “데이터 스트림의 복잡한 처리”, “반응형 프로그래밍의 구현” 이 핵심이라고 할 수 있는데요.
이 두가지를 간단히 설명하자면, 아래와 같습니다.
이 개념중에서 우리는 ‘반응형 프로그래밍’ 보다는 ‘데이터 스트림 처리’ 에 중점을 둘 것인데요.
위에서 발생한 문제가 스트림 형태로 들어오는 데이터의 애니메이션 중첩이었습니다. 이를 해결하기 위해서 이 데이터 스트림을 ‘묶어서’ 처리하고자 합니다.
collect
collect는 데이터 스트림에서 들어오는 값을 ‘묶어서’ 처리할 수 있는 연산자입니다.
동작 방식으로 이해하는게 더 빠를 것 같은데요.
이런 식으로 데이터 스트림을 배열로 묶을 수 있습니다.
또, collect 연산자는 시간 단위로 묶을 수도 있어서 특정 시간 동안 들어온 데이터 스트림을 그룹화할 수 있습니다.
이런 식으로 묶을 수 있습니다. 이렇게 데이터를 시간 단위로 묶게 되면, 위의 타이머와 비슷하게 동작하되 특정 시간 동안 들어온 데이터를 묶어서 뷰를 업데이트하게 됩니다.
collect의 시간 간격을 0.5초로 주면 0.5초간 들어오는 데이터 스트림을 묶고, 전달하기 때문에 뷰는 0.5초마다 업데이트하게 되는 것이죠.
위에서 우리의 문제가 ‘0.2초의 주기를 가진 애니메이션의 중첩’ 이었기 때문에 0.2초동안 들어오는 데이터 스트림을 묶어서 0.2초마다 뷰를 업데이트하면 애니메이션 또한 0.2초마다 발생하기 때문에 애니메이션이 중첩될 일이 줄어들고, 상태 업데이트 또한 0.2초마다 되기 때문에 뷰 업데이트도 일정한 간격으로 할 수 있게 되죠.
성능 비교
코드를 적용한 후 실제로 성능이 어떻게 변화했는지를 Xcode Instrument를 통해 비교해보았습니다.
기존 코드 (문제가 발생한 코드)
성능 문제가 발생한 경우 위와 같이 뷰 업데이트 횟수가 잦고, CPU 사용량, Hang 섹션 등 문제가 많은 것을 볼 수 있습니다.
애니메이션을 제거한 코드
애니메이션을 제거한 경우, 뷰 업데이트 횟수와 CPU 사용량은 들쭉날쭉하지만 프레임 드랍이 일어나는 Hang 구간이 확연히 줄어든 것을 확인할 수 있습니다.
Combine을 적용한 코드
Combine의 collect 연산자를 적용한 경우 위와 같습니다. 프레임 드랍이 나타나는 Hangs는 애니메이션을 제거한 경우와 비슷하게 거의 표시되지 않았습니다.
또한 연산자 덕분에 뷰가 일정하게 업데이트되는 것을 확인할 수 있었습니다. 하지만 텍스트의 양이 많아질수록 CPU 사용량이 크게 늘어났고 Activity에서도 노란 부분이 표시되는 등 성능상 완전한 해결을 했다기엔 조금 아쉬운 부분이 있었습니다.
성능을 더 개선할 수 있는 방법은 없을까?
SwiftUI 뷰 업데이트를 반복하지 않는 방법 #번역본
위 글을 보면 SwiftUI에서 뷰 업데이트를 최적화할 수 있는 방법에 대해서 설명하고 있습니다.
현재는 코드에 이를 반영하지 않아서 텍스트가 업데이트될때마다 불필요한 부분까지 과도하게 뷰가 업데이트되고 있는데, 이를 나중에 반영해서 뷰 업데이트를 최적화할 수 있을 것 같습니다.
또, SwiftUI의 Text 뷰는 간단한 텍스트를 표시하는 데에는 적합하지만 긴 텍스트를 효율적으로 렌더링하는 데에는 제한이 있다고 합니다. 이 때 UIKit의 UITextView 등 긴 텍스트를 효율적으로 처리하는 컴포넌트를 활용해서 추가적인 성능 개선을 이룰 수도 있을 것 같습니다.
결론
이번 트러블 슈팅을 경험하며 가장 크게 느낀 점은 “Combine을 왜 써야하는지” 였습니다. 복잡한 데이터 스트림의 처리를 할 때 Combine이 어떤 역할을 하는지와, 어떻게 사용하는지를 배울 수 있었던 소중한 경험이었습니다.
물론 Combine 말고도 이런 복잡한 데이터 스트림을 처리할 수 있는 방법은 많겠지만, Combine을 사용하며 느낀 가장 큰 장점은 이러한 처리를 SwiftUI의 선언적 프로그래밍과 자연스럽게 통합해 깔끔하고 가독성 높은 코드를 작성할 수 있다는 점인 것 같습니다.
또, 사용자 경험과 성능 개선을 고민하며 애니메이션을 없애버릴까 수없이 고민했지만.. 역시 안되는건 없다는걸 다시 배웠습니다.
Beta Was this translation helpful? Give feedback.
All reactions