개요
Introspect 라이브러리를 사용할 때 유의해야 할 버그 발생 케이스와 우회 방법을 공유합니다.
Introspect: UIKit과 AppKit의 기능 또는 요소들을 SwiftUI로 사용할 수 있게 래핑 해서 제공하는 오픈 소스 라이브러리.
최근 100% SwiftUI로 작성되어 있는 iOS 프로젝트를 유지보수했던 적이 있습니다. iOS 개발자가 퇴사한 지 2년이 넘어, 2022년도에 출시된 이후로 거의 유지보수된 적이 없는 프로젝트의 일부 기능을 개선해야 하는 임무를 맡았습니다.
그 프로젝트를 열어보기 전에는 솔직히 약간 겁부터 났습니다. 그 앱이 출시되었던 시점이 2022년이었고 최소 지원 버전이 iOS 14로 되어있었던걸 감안했을 때, SwiftUI의 극초창기 시절에 개발된 프로젝트라서 매끄럽지 못하게 동작하는 기능이 있거나 땜빵식으로 작성된 코드가 많을 것이라고 지레 짐작했었기 때문인데요.
그런데 짐작과는 다르게, MVVM + Coordinator 기반으로 탄탄하게 코드가 작성되어 있었고 SPM을 이용해서 적절히 모듈화도 되어있었습니다. 무엇보다 별다른 인수인계 문서나 주석 없이 코드만으로도 이해가 되는 네이밍과 구조가 돋보였고, 이전 개발자분의 뛰어난 실력과 노고를 여실히 느낄 수 있었습니다. 덕분에 짧은 기간 동안 배운 게 많았습니다.
그런데, 아무래도 SwiftUI의 극초창기 시절에 진행했던 프로젝트였기 때문에 순수 SwiftUI의 기능만으로는 요구사항을 구현해내기 어려우셨던 것 같습니다. 여러 오픈 소스 라이브러리에 의존하고 있었는데, 그중에 "Introspect"가 있었습니다. 거의 대부분은 순수 SwiftUI로 작성되어 있었는데, `ScrollView` 에 관련해서는 UIKit의 도움이 필요했던 것으로 보입니다.
UIScrollView의 clipsToBounds 속성값에 대해
버그에 대한 얘기를 하기 전에 `clipsToBounds` 속성값에 대한 설명을 하고 넘어가겠습니다.
`UIScrollView`는 `UIView`로부터 상속받은 clipsToBounds 라는 속성값을 갖고 있습니다.
`clipsToBounds` 값은 "콘텐츠가 스크롤되어 UIScrollView 영역 바깥으로 빠져나갈 때, 콘텐츠를 화면에 빠져나간 모습 그대로 보여줄 것인지 또는 숨길 것인지"를 담당하는 플래그 역할을 합니다.
일반적으로는 왼쪽처럼 설정값이 `true`인 화면이 우리에게 익숙할 것입니다.

[참고]
UIView 클래스의 `clipsToBounds` 속성의 기본값은 `false` 입니다. 그러나 UIScrollView는 이 기본값을 `true`로 재정의합니다.
출처: clipsToBounds 공식 문서.
- The default value is false. Some subclasses of UIView, like UIScrollView, override the default value to true.
겪었던 버그
이 프로젝트에서는 Introspect 라이브러리를 최신버전으로 사용하지 않고 있었고, 버전업을 하기 어려운 상황이었습니다. 이유는 전임자분께서 Introspect 라이브러리 0.6.0 버전을 직접 fork 해서 몇 가지 개조를 해서 사용하고 있던 것이 남아있었기 때문입니다. 개조를 한 이유와 항목들을 분석해서 큰 이슈가 없다면 최신 라이브러리로 마이그레이션 해버리거나, 또는 걷어내 버리는 방법도 있었겠습니다만, 이번 유지보수 업무의 일정이 매우 빠듯했던 관계로 거기까지는 진행을 못해봤네요.
Introspect는 0.7.0 버전을 기준으로 사용법도 꽤 많이 바뀌고, 1.0.0 메이저 버전 릴리즈 이후로 안정성도 많이 강화되었다는 평가가 꽤 많은 것으로 알고 있습니다. 그래서 아래에서 말씀드릴 버그가 최신 라이브러리에서는 재현이 안 될수도 있다는 점 참고하여 읽어주시면 좋을 것 같습니다.
첫번째로, 제가 겪었던 버그는 iOS 17 이상의 환경에서만 발생했습니다.
제가 맡았던 앱에는 수직 `ScrollView` 내부에 수평 `ScrollView`가 한번 더 들어가 있는, 이중 ScrollView 구조를 갖고 있는 화면이 하나 있었습니다. 아래처럼요.

위 화면에 한가지 디테일이 더 있습니다. 자세히 보시면, 수평 스크롤 영역인 붉은색 상자들은 `clipsToBounds` 설정 값이 `true`로 설정되어, ScrollView 영역 밖으로 상자들이 빠져나가는 것을 알 수 있습니다.
제가 맡았던 프로젝트의 한 화면은 의도적으로 위와 같은 기능을 구현해야만 했습니다. 그런데 여기서 한 가지 문제점이 발생했습니다. iOS16까지는, 순수 SwiftUI의 ScrollView에서 `clipsToBounds` 속성을 설정해주는 modifier인 `scrollClipDisabled()` 를 사용할 수 없다는 것입니다. 이 modifier는 2023년에 iOS17의 발표와 함께 추가되었습니다. 전 담당자분께서 이 프로젝트를 개발할 당시에는 iOS 15 버전이 최신이던 2021년이었기 때문에, 당연히 이 옵션 자체가 없었던 것이죠.
그래서 선택한 해결방법이 Introspect 라이브러리를 통해 UIKit의 속성값을 참조하도록 하는 방법이었던 것입니다.
위 화면의 코드는 대략적으로 아래와 같은 구조입니다.
import SwiftUIIntrospect
...
...
ScrollView {
...
...
ScrollView(.horizontal) {
...
...
}
.introspectScrollView { scrollView in
scrollView.clipsToBounds = false
}
...
...
}
위에서 말씀드렸다시피, introspect 최신 버전과는 코드 작성 방법에 있어서 차이가 있습니다.
두 번째 `ScrollView` 에 `clipsToBounds` 속성값을 `false`로 설정해 준 것을 볼 수 있습니다.
이 코드는 iOS14부터 iOS16 까지는 멀쩡하게 동작하고 있었습니다. 그런데 iOS17부터는 설정이 적용되지 않아, 영역 밖으로 요소가 빠져나가지 않고 있었습니다.
혹시 원인이 Introspect 라이브러리가 아닌, 다른 코드에 있는 것일수도 있다는 생각에 우선 제가 손댈 수 있는 곳에서부터 코드를 여러 방면으로 고쳐보면서 디버깅을 시도했었습니다. `.introspectScrollView { }` 코드를 선언해 준 위치도 바꿔보고, ScrollView 내부에 선언되어 있는 다른 View 요소들을 없애거나 옮겨보면서 버그의 원인이 다른 코드에 있는 건 아닌지 검증해보려고 했는데요. 다행히 다른 코드가 버그를 유발하고 있는 건 아닌 것으로 확인했었습니다.
그래서 iOS17부터 추가된 `scrollClipDisabled()` modifier를 사용하는 것으로 방법을 결정했습니다.
해결 - 버전별 분기처리
위에서 말씀드린대로 `scrollClipDisabled()` modifier는 iOS17 미만의 버전에서는 사용할 수 없기 때문에, 버전으로 분기 처리를 해주어야 합니다. 해당 View 코드에 바로 작성하는 것보다, `View` 의 커스텀 함수를 extension으로 분리해서 작성해 주는 것이 보기도 좋고 사용하기도 편하겠죠.
public extension View {
func disabledScrollClip(_ disabled: Bool) -> some View {
if #available(iOS 17.0, *) {
return self.scrollClipDisabled(disabled)
} else {
return self
.introspectScrollView { scrollView in
scrollView.clipsToBounds = !disabled
}
}
}
}
유의해야 할 것은은 UIKit의 `clipsToBounds` 와 SwiftUI의 `scrollClipDisabled()` 에 넣어주어야 하는 플래그의 개념이 반대라는 점입니다. 그래서 Introspect로 속성을 변경해 주는 쪽은 넘겨받은 파라미터를 반전시켜서 할당해주고 있습니다.
그리고 함수 이름을 오리지널인 `scrollClipDisabled`를 그대로 사용하고 싶었는데, ambiguous 문제 때문에 disabled 위치만 앞으로 옮기는 방식으로 지었는데요, 더 좋은 이름이 있다면 알려주세요.. ㅎㅎ..
정리
버그에 대한 설명을 최대한 자세하게 적어보고 싶었는데, 어쩌다보니 엄청 장황해졌네요.
아무튼, 제가 전달하고 싶었던 내용은
1. Introspect 라이브러리를 사용한다면, 새로운 iOS 버전이 출시될 때마다 버그가 생기지 않았는지 확인해 보자.
2. 아직 100% SwiftUI 만으로 상용 프로덕트를 만들기는 조금(?) 이른 것 같다.
입니다.
iOS17부터 `scrollClipDisabled()` 를 사용하면 똑같은 기능을 구현할 수 있다는 것을 알게 되었을 때, 때마침 modifier가 추가되어서 다행이다라는 생각보다는 "애플 이 녀석들이 공식적으로 modifier 추가해 줄 때마다 이런 일이 또 생길 수 있겠구나" 라는 생각이 먼저 들었습니다. 아직 UIKit에서 제공하는 기능을 순수 SwiftUI만으로 100% 다 구현할 수 없는 게 현 상황이고, 매년 업데이트마다 조금씩 옵션들이 추가되고 있는 중이죠. 이때 하위 버전도 사용 가능하도록 지원이 된다면 좋을 텐데, 그렇지 않기 때문에 개발자들은 Introspect 같은 thrid-party 라이브러리에 의존하게 되고, 버전별 분기처리 코드를 어쩔 수 없이 추가하게 됩니다.
분기처리 하는 코드 추가하면 그만이고, third-party 라이브러리 의존하기 싫다면 Representable로 포팅해서 사용하면 된다고 말할 수 있겠지만.. 결국 유지보수 관점에서 관리 포인트를 셀프로 늘려버리는 모습밖에 안 된다고 생각합니다. (이럴 거면 UIKit으로 하지..)
여담으로, 왜 버그가 발생했는지 분석해 보면서 Introspect 라이브러리의 최신 버전을 사용해서 데모앱을 만들어보기도 했는데요,
사용법에서 크게 바뀐 점 중 하나가 속성을 적용시킬 `scope` 를 직접 지정해 줄 수 있게 된 것이라는 점을 알게 되었습니다. 그리고 최신버전을 사용한 버전에서는 애초에 위의 버그가 발생이 안되더라구요.
여기서 "구버전을 사용하는 제 프로젝트에서 버그가 발생한 건 아마도 `scope`와 연관이 있지 않을까?"라는 뇌피셜 추측을 해볼 수 있을 것 같습니다. ScrollView가 두 개 이상 겹쳐있는 상황에서 옵션을 적용하는 코드가 다른 ScrollView로 잘못 적용되었을 가능성이 있지 않을까.. 라는 생각인 거죠.
그러니 최소 1.0.0 이상의 최근 버전을 사용한다면, 이 글에서 언급한 버그는 발생하지 않을 것이라고 예상합니다.
최신버전을 애용합시다.
'iOS(macOS) > SwiftUI' 카테고리의 다른 글
[SwiftUI/TCA] Scope 예제 (0) | 2023.09.18 |
---|---|
SwiftUI - List의 scroll 비활성화 (0) | 2023.08.19 |
[SwiftUI/iOS] 이미지 파일로 Launch Screen 만들기 (0) | 2023.08.08 |
[iOS/SwiftUI] Navigation - Back Button 커스텀 하기 (0) | 2023.04.26 |
[iOS/SwiftUI] ScrollView - 키보드 화면 가림 해결 예제 (0) | 2023.03.26 |
개요
Introspect 라이브러리를 사용할 때 유의해야 할 버그 발생 케이스와 우회 방법을 공유합니다.
Introspect: UIKit과 AppKit의 기능 또는 요소들을 SwiftUI로 사용할 수 있게 래핑 해서 제공하는 오픈 소스 라이브러리.
최근 100% SwiftUI로 작성되어 있는 iOS 프로젝트를 유지보수했던 적이 있습니다. iOS 개발자가 퇴사한 지 2년이 넘어, 2022년도에 출시된 이후로 거의 유지보수된 적이 없는 프로젝트의 일부 기능을 개선해야 하는 임무를 맡았습니다.
그 프로젝트를 열어보기 전에는 솔직히 약간 겁부터 났습니다. 그 앱이 출시되었던 시점이 2022년이었고 최소 지원 버전이 iOS 14로 되어있었던걸 감안했을 때, SwiftUI의 극초창기 시절에 개발된 프로젝트라서 매끄럽지 못하게 동작하는 기능이 있거나 땜빵식으로 작성된 코드가 많을 것이라고 지레 짐작했었기 때문인데요.
그런데 짐작과는 다르게, MVVM + Coordinator 기반으로 탄탄하게 코드가 작성되어 있었고 SPM을 이용해서 적절히 모듈화도 되어있었습니다. 무엇보다 별다른 인수인계 문서나 주석 없이 코드만으로도 이해가 되는 네이밍과 구조가 돋보였고, 이전 개발자분의 뛰어난 실력과 노고를 여실히 느낄 수 있었습니다. 덕분에 짧은 기간 동안 배운 게 많았습니다.
그런데, 아무래도 SwiftUI의 극초창기 시절에 진행했던 프로젝트였기 때문에 순수 SwiftUI의 기능만으로는 요구사항을 구현해내기 어려우셨던 것 같습니다. 여러 오픈 소스 라이브러리에 의존하고 있었는데, 그중에 "Introspect"가 있었습니다. 거의 대부분은 순수 SwiftUI로 작성되어 있었는데, ScrollView
에 관련해서는 UIKit의 도움이 필요했던 것으로 보입니다.
UIScrollView의 clipsToBounds 속성값에 대해
버그에 대한 얘기를 하기 전에 clipsToBounds
속성값에 대한 설명을 하고 넘어가겠습니다.
UIScrollView
는 UIView
로부터 상속받은 clipsToBounds 라는 속성값을 갖고 있습니다.
clipsToBounds
값은 "콘텐츠가 스크롤되어 UIScrollView 영역 바깥으로 빠져나갈 때, 콘텐츠를 화면에 빠져나간 모습 그대로 보여줄 것인지 또는 숨길 것인지"를 담당하는 플래그 역할을 합니다.
일반적으로는 왼쪽처럼 설정값이 true
인 화면이 우리에게 익숙할 것입니다.

[참고]
UIView 클래스의clipsToBounds
속성의 기본값은false
입니다. 그러나 UIScrollView는 이 기본값을true
로 재정의합니다.
출처: clipsToBounds 공식 문서.
- The default value is false. Some subclasses of UIView, like UIScrollView, override the default value to true.
겪었던 버그
이 프로젝트에서는 Introspect 라이브러리를 최신버전으로 사용하지 않고 있었고, 버전업을 하기 어려운 상황이었습니다. 이유는 전임자분께서 Introspect 라이브러리 0.6.0 버전을 직접 fork 해서 몇 가지 개조를 해서 사용하고 있던 것이 남아있었기 때문입니다. 개조를 한 이유와 항목들을 분석해서 큰 이슈가 없다면 최신 라이브러리로 마이그레이션 해버리거나, 또는 걷어내 버리는 방법도 있었겠습니다만, 이번 유지보수 업무의 일정이 매우 빠듯했던 관계로 거기까지는 진행을 못해봤네요.
Introspect는 0.7.0 버전을 기준으로 사용법도 꽤 많이 바뀌고, 1.0.0 메이저 버전 릴리즈 이후로 안정성도 많이 강화되었다는 평가가 꽤 많은 것으로 알고 있습니다. 그래서 아래에서 말씀드릴 버그가 최신 라이브러리에서는 재현이 안 될수도 있다는 점 참고하여 읽어주시면 좋을 것 같습니다.
첫번째로, 제가 겪었던 버그는 iOS 17 이상의 환경에서만 발생했습니다.
제가 맡았던 앱에는 수직 ScrollView
내부에 수평 ScrollView
가 한번 더 들어가 있는, 이중 ScrollView 구조를 갖고 있는 화면이 하나 있었습니다. 아래처럼요.

위 화면에 한가지 디테일이 더 있습니다. 자세히 보시면, 수평 스크롤 영역인 붉은색 상자들은 clipsToBounds
설정 값이 true
로 설정되어, ScrollView 영역 밖으로 상자들이 빠져나가는 것을 알 수 있습니다.
제가 맡았던 프로젝트의 한 화면은 의도적으로 위와 같은 기능을 구현해야만 했습니다. 그런데 여기서 한 가지 문제점이 발생했습니다. iOS16까지는, 순수 SwiftUI의 ScrollView에서 clipsToBounds
속성을 설정해주는 modifier인 scrollClipDisabled()
를 사용할 수 없다는 것입니다. 이 modifier는 2023년에 iOS17의 발표와 함께 추가되었습니다. 전 담당자분께서 이 프로젝트를 개발할 당시에는 iOS 15 버전이 최신이던 2021년이었기 때문에, 당연히 이 옵션 자체가 없었던 것이죠.
그래서 선택한 해결방법이 Introspect 라이브러리를 통해 UIKit의 속성값을 참조하도록 하는 방법이었던 것입니다.
위 화면의 코드는 대략적으로 아래와 같은 구조입니다.
import SwiftUIIntrospect
...
...
ScrollView {
...
...
ScrollView(.horizontal) {
...
...
}
.introspectScrollView { scrollView in
scrollView.clipsToBounds = false
}
...
...
}
위에서 말씀드렸다시피, introspect 최신 버전과는 코드 작성 방법에 있어서 차이가 있습니다.
두 번째 ScrollView
에 clipsToBounds
속성값을 false
로 설정해 준 것을 볼 수 있습니다.
이 코드는 iOS14부터 iOS16 까지는 멀쩡하게 동작하고 있었습니다. 그런데 iOS17부터는 설정이 적용되지 않아, 영역 밖으로 요소가 빠져나가지 않고 있었습니다.
혹시 원인이 Introspect 라이브러리가 아닌, 다른 코드에 있는 것일수도 있다는 생각에 우선 제가 손댈 수 있는 곳에서부터 코드를 여러 방면으로 고쳐보면서 디버깅을 시도했었습니다. .introspectScrollView { }
코드를 선언해 준 위치도 바꿔보고, ScrollView 내부에 선언되어 있는 다른 View 요소들을 없애거나 옮겨보면서 버그의 원인이 다른 코드에 있는 건 아닌지 검증해보려고 했는데요. 다행히 다른 코드가 버그를 유발하고 있는 건 아닌 것으로 확인했었습니다.
그래서 iOS17부터 추가된 scrollClipDisabled()
modifier를 사용하는 것으로 방법을 결정했습니다.
해결 - 버전별 분기처리
위에서 말씀드린대로 scrollClipDisabled()
modifier는 iOS17 미만의 버전에서는 사용할 수 없기 때문에, 버전으로 분기 처리를 해주어야 합니다. 해당 View 코드에 바로 작성하는 것보다, View
의 커스텀 함수를 extension으로 분리해서 작성해 주는 것이 보기도 좋고 사용하기도 편하겠죠.
public extension View {
func disabledScrollClip(_ disabled: Bool) -> some View {
if #available(iOS 17.0, *) {
return self.scrollClipDisabled(disabled)
} else {
return self
.introspectScrollView { scrollView in
scrollView.clipsToBounds = !disabled
}
}
}
}
유의해야 할 것은은 UIKit의 clipsToBounds
와 SwiftUI의 scrollClipDisabled()
에 넣어주어야 하는 플래그의 개념이 반대라는 점입니다. 그래서 Introspect로 속성을 변경해 주는 쪽은 넘겨받은 파라미터를 반전시켜서 할당해주고 있습니다.
그리고 함수 이름을 오리지널인 scrollClipDisabled
를 그대로 사용하고 싶었는데, ambiguous 문제 때문에 disabled 위치만 앞으로 옮기는 방식으로 지었는데요, 더 좋은 이름이 있다면 알려주세요.. ㅎㅎ..
정리
버그에 대한 설명을 최대한 자세하게 적어보고 싶었는데, 어쩌다보니 엄청 장황해졌네요.
아무튼, 제가 전달하고 싶었던 내용은
1. Introspect 라이브러리를 사용한다면, 새로운 iOS 버전이 출시될 때마다 버그가 생기지 않았는지 확인해 보자.
2. 아직 100% SwiftUI 만으로 상용 프로덕트를 만들기는 조금(?) 이른 것 같다.
입니다.
iOS17부터 scrollClipDisabled()
를 사용하면 똑같은 기능을 구현할 수 있다는 것을 알게 되었을 때, 때마침 modifier가 추가되어서 다행이다라는 생각보다는 "애플 이 녀석들이 공식적으로 modifier 추가해 줄 때마다 이런 일이 또 생길 수 있겠구나" 라는 생각이 먼저 들었습니다. 아직 UIKit에서 제공하는 기능을 순수 SwiftUI만으로 100% 다 구현할 수 없는 게 현 상황이고, 매년 업데이트마다 조금씩 옵션들이 추가되고 있는 중이죠. 이때 하위 버전도 사용 가능하도록 지원이 된다면 좋을 텐데, 그렇지 않기 때문에 개발자들은 Introspect 같은 thrid-party 라이브러리에 의존하게 되고, 버전별 분기처리 코드를 어쩔 수 없이 추가하게 됩니다.
분기처리 하는 코드 추가하면 그만이고, third-party 라이브러리 의존하기 싫다면 Representable로 포팅해서 사용하면 된다고 말할 수 있겠지만.. 결국 유지보수 관점에서 관리 포인트를 셀프로 늘려버리는 모습밖에 안 된다고 생각합니다. (이럴 거면 UIKit으로 하지..)
여담으로, 왜 버그가 발생했는지 분석해 보면서 Introspect 라이브러리의 최신 버전을 사용해서 데모앱을 만들어보기도 했는데요,
사용법에서 크게 바뀐 점 중 하나가 속성을 적용시킬scope
를 직접 지정해 줄 수 있게 된 것이라는 점을 알게 되었습니다. 그리고 최신버전을 사용한 버전에서는 애초에 위의 버그가 발생이 안되더라구요.
여기서 "구버전을 사용하는 제 프로젝트에서 버그가 발생한 건 아마도scope
와 연관이 있지 않을까?"라는 뇌피셜 추측을 해볼 수 있을 것 같습니다. ScrollView가 두 개 이상 겹쳐있는 상황에서 옵션을 적용하는 코드가 다른 ScrollView로 잘못 적용되었을 가능성이 있지 않을까.. 라는 생각인 거죠.
그러니 최소 1.0.0 이상의 최근 버전을 사용한다면, 이 글에서 언급한 버그는 발생하지 않을 것이라고 예상합니다.
최신버전을 애용합시다.
'iOS(macOS) > SwiftUI' 카테고리의 다른 글
[SwiftUI/TCA] Scope 예제 (0) | 2023.09.18 |
---|---|
SwiftUI - List의 scroll 비활성화 (0) | 2023.08.19 |
[SwiftUI/iOS] 이미지 파일로 Launch Screen 만들기 (0) | 2023.08.08 |
[iOS/SwiftUI] Navigation - Back Button 커스텀 하기 (0) | 2023.04.26 |
[iOS/SwiftUI] ScrollView - 키보드 화면 가림 해결 예제 (0) | 2023.03.26 |