티스토리 뷰
Xcode 15 의 iOS 17 빌드에서 User Agent 가 원하는 값으로 설정되지 않을때 (feat. iOS 버전별 WebKit 버전과 작업 내역 확인하기)
지마켓 강수진 2023. 10. 19. 14:47들어가기 전에
안녕하세요 Mobile Application Team iOS 개발자 강수진입니다.
오늘은
- Xcode 15로 빌드한 iOS 17에서 웹 뷰의 User Agent 가 원하는 값으로 설정되지 않는 이슈와
- 원인으로 추정되는 커밋 내역이 iOS 17부터 반영된 게 맞냐!
를 추적해 가는 여정을 떠나 보도록 하겠습니다..ㅋㅎ 그럼 같이 가보져!!
문제의 시작..
Xcode 15.0 이 9월 18 일 (Beta 8과 RC 1을 거쳐) 릴리즈 되었습니다 👏🏻👏🏻👏🏻
그러면 우리 서비스도 문제가 없나 한번 돌려봐야겠죠? Xcode 15로 iOS 17 빌드 가보자고~~~!
네? ㅠ 역시 우리의 금쪽이 엑스코드에서 한 번에 잘 될 리가 없죠?
앱에서는 자체적인 native header 가 있기 때문에 웹 뷰 내의 헤더는 사라져 보여야 하는데, 그대로 유지되어 있는 모양새가.. 마치 앱 단의 요청인지 모르는 것처럼 동작했습니다.
그럼 이런 현상은 왜 발생하는 걸까요? 첫 번째 의심 후보는 바로 User-Agent였습니다.
User-Agent 가 뭔데?
User-Agent는 애플리케이션, 운영 체제, 공급 업체, 또는 버전을 식별할 수 있게 해주는 HTTP request header의 한 종류입니다.
Javascript로는 navigator.userAgent 명령어로 확인할 수 있는데요, iOS 앱에서 웹뷰의 userAgent는 아래와 같은 형식으로 설정되어 있습니다.
어떻게 아냐구여? UserAgentIOS.mm 파일에 가면 이런 형식으로 지정되어 있기 때문이죠 ㅎ
그렇다면 '이건 앱에서의 웹 뷰 요청이다!!!'를 더 자세히 구분해야 할 때는 어떻게 해야 할까요?
저희 서비스에서는 앱 내 웹 뷰의 요청일 때는 기본 User-Agent의 값 뒤에 custom 정보를 추가하고 있습니다. 아래에서 굵은 글씨로 표시된 정보처럼요!
ex. User-Agent: Mozilla/5.0 (iPhone; CPU iPhone OS 14_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148 MobileApp/1.0 (iOS; 앱 버전; Bundle identifier)
하지만 Xcode 15 + iOS 17로 앱을 빌드하고, User agent 값을 찍어봤을 때는 이러한 custom 정보가 싹 날아가 있었습니다.
흠.. 이러한 추가 정보의 누락 때문에 웹 뷰는 해당 요청을 앱에서 보내는 건지 판단할 수 없었을 수도?!라는 생각이 듭니다.
원인 찾아가기
그럼 custom 한 정보가 왜 누락되는지 원인을 알기 위해서는 webview의 user agent를 설정하는 코드부터 찾아가야 합니다.
저희 쪽에서는 아래와 같은 코드를 사용하고 있었는데요,
UserDefaults.standard.register(defaults: ["UserAgent": 원하는 값])
🤔 : 엥 보통.. webview의 프로퍼티를 사용하지 않나..?? 이건 그냥 유저 디폴트에 저장만 하는 거잖아....??? 이게 웹뷰의 유저 에이전트랑 무슨 상관인데..???
예.. 놀랍게도 UserDefault에 "UserAgent"라는 키로 값을 정의해 두면, 이 값이 override 되어 웹 뷰의 user agent로 사용되었다고 합니다.
실제로 코드에서도 강제로 값을 변경하고, userAgent를 디버깅해 보면 설정한 값으로 나오는 것을 확인할 수 있습니다.
UserDefaults.standard.register(defaults: ["UserAgent":"Hello!"])
🤔 : ㅇㅋㅇㅋ.. 이상하긴 하지만.. 알겠어! 근데 잘되던 게 왜 갑자기 안됨?!
그 실마리는 Remove iOS-only NSUserDefaults-backed UA override 커밋에서 찾아볼 수 있습니다. 제목부터 "iOS 전용 NSUserDefaults로 지원되는 UA(유저 에이전트) 오버라이드 제거"네요.
그럼 iOS 16 / 17 빌드했을 때 webkit 버전을 분석해서, 위에서 찾은 커밋이 해당 버전에 포함되어 있나 확실히 하고 싶었는데요, navigator.userAgent 를 찍어봤을 때는 각 iOS 버전에서 같은 값이 나와서 이 방법으로는 분석할 수 없었습니다.
Mozilla/5.0 (iPhone; CPU iPhone OS 14_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.0 Mobile/15E148 Safari/604.1
또한 WebKit Features in Safari 17.0 에는 해당 커밋의 버그 번호인 70699433 가 포함되어있지 않았고, apple developer 사이트의 검색 결과에도 걸리지 않았습니다.
흠.. 어떻게 하지... 생각하던 중 Xcode14.3에 내장되어 있는 iOS 16.4의 Webkit.framework 의 내용과, Xcode 15에 내장되어 있는 iOS 17.0의 Webkit.framework 의 내용을 비교해 보기로 했습니다.
참고로 경로는 다음과 같았습니다
// Xcode 14.3
/Applications/Xcode_14.3.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS.sdk/System/Cryptexes/OS/System/Library/Frameworks/WebKit.framework
// Xcode 15.0
/Applications/Xcode_15.0.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS.sdk/System/Cryptexes/OS/System/Library/Frameworks/WebKit.framework
그리고 17.0은 16.4와 비교했을 때 아래 파일들에서의 변화가 있었는데요,
주목할 점은 WebKit.tbd 파일에서 version 이 615.1.26 -> 616.1.27 로 변했다는 것이었습니다.
저는 이렇게 힘든 과정을 거쳤는데,, 이렇게 알아낸 "615.1.26"으로 구글링을 해보니 iOS 버전에 따른 WebKit 버전 정보를 정리해 둔 safari_ios.json 파일이 있었네요 ㅎ Safari version history라는 위키 페이지도 있습니다.
그럼 다시 본론으로 돌아와서 Remove iOS-only NSUserDefaults-backed UA override 커밋이 iOS 16 버전대에 해당하는 615.~ 버전에는 포함되어있지 않고, iOS 17에 해당하는 616.~ 에는 포함되어 있다! 를 어떻게 알 수 있을까요?
해당 커밋은 WebKit-7616.1.20 부터 반영되어 있는데요
여기서 우리가 먼저 알아야 할 것은 웹킷의 버저닝 규칙입니다. 바로 가장 앞 7을 제외하고 순서대로 MAJOR - MINOR - TINY - MICRO - NANO 를 뜻합니다.
- MAJOR_VERSION : 616
- MINOR_VERSION : 1
- TINY_VERSION : 27
- MICRO_VERSION : 11
- NANO_VERSION : 7
Version.xcconfig 파일 히스토리 중 아무 커밋이나 클릭해서 살펴보면, 버전이 올라갈 때마다 이 값들을 적절히 변경하고 있는 걸 확인할 수 있습니다.
다만 WebKit-7616.1.20 에서 맨 앞에 7의 유래는.. 열심히 찾아봤지만 모르겠습니다 🙄 아시는 분 있으면 댓글 달아주세여
다시 본론으로 돌아와서!! 아까 iOS 16 은 웹킷의 Major 버전이 615 였고, iOS 17 은 616.1.27 였죠?
그리고 Remove iOS-only NSUserDefaults-backed UA override 커밋은 WebKit-7616.1.20 부터 반영되어 있다고 했고요!
따라서 해당 커밋은 당연히 iOS 17에서 사용하는 웹킷 616.1.27 버전에도 반영이 되어있네요! ( WebKit-7616.1.27 )
따라서 해당 커밋이 iOS 16의 웹킷에는 반영이 되어있지 않고, iOS 17의 웹킷에는 반영이 되어있다는 것을 확신할 수 있었습니다.
그래서 어떻게 하라고요?
Remove iOS-only NSUserDefaults-backed UA override 논의에서도 보면, 클라이언트가 user agent를 설정하기 위해서는 User Default 값을 사용하는 대신, 기존 API를 사용하는 것을 강력히 권장합니다.
우선 WKWebView의 인스턴스 프로퍼티인 customUserAgent 을 이용하면 기존의 user-agent를 원하는 값으로 완전히 덮어씌울 수 있습니다.
하지만 우리가 원하는 건 기본 user agent 값의 뒤에 custom 정보가 붙는 형식이었습니다. 이를 위해서는 WKWebViewConfiguration의 프로퍼티인 applicationNameForUserAgent 를 사용해볼 수 있는데요,
그러면 원래 아래와 같은 형식으로 맨 뒤에 붙었던 applicationName 이, 우리가 설정한 값으로 변경됩니다.
여기서 주의할 점은, 이렇게 마지막 applicationName 의 값을 바꾸면 다른 곳에서 문제가 발생할 수 있다는 건데요!
예를 들어 닷넷 코드에서 mobile 임을 판단할 때 Request.Browser.IsMobileDevice 를 사용하고 있다면, 해당 값 변경시 false 가 return 될 수 있습니다.
따라서 기본적으로 설정되어있는 applicationName (ex. Mobile/15E148) 까지 포함해서 그 뒤에 custom 값을 붙여주는 것이 안전하다고 판단됩니다.
이미 올라간 앱은요?
여기까지 읽으신 분들이라면,
🤔 : 잠깐.. 이미 스토어에 올라간 앱의 iOS 17 은 문제가 발생하는 거 아냐?! 괜찮아??
라고 생각하실 수 있습니다.
하지만 해당 앱은 Xcode 14.3.1에서 빌드를 했기 때문에 괜찮은데요, 이게 무슨 말인가... 싶져..? ㅎ
이해를 돕기 위해 iOS 16.4와 iOS 17.0에서 사용하는 WebKit의 version을 각각 A와 B라고 해보겠습니다.
- iOS 16.4 - WebKit A version
- iOS 17.0 - Webkit B version
만약 WebKit 코드에서 해당 프레임워크가 언제 링킹 되었는지 등의 버전 분기등을 태우지 않는다면 iOS 16.4 사용자는 iOS 17.0으로 업데이트할 때 자연스럽게 A->B 버전의 이점을 누릴 수 있습니다.
하지만 문제는 동일한 iOS 17 기기에서 WebKit을 사용하더라도, 해당 코드에 버전 분기 등이 들어가 있다면 어떤 버전의 Xcode 에서 빌드했냐에 따라 동작이 달라질 수 있다는 건데요..! 코드를 다시 한번 볼까요?
조금 자세히 들여다보면, override 허용 조건은 " DoesNotOverrideUAFromNSUserDefault 와 함께 링킹 되지 않았을 때" 임을 알 수 있습니다.
그리고 같은 커밋의 RuntimeApplicationChecksCocoa.cpp 파일을 보면, iOS 17.0 미만일 때 링킹 되었다면 DoesNotOverrideUAFromNSUserDefault 를 비활성화시키고 있습니다.
여기서 Xcode 14.3.1과 15.0 이 사용하는 iOS SDK 버전은 각각 16.4와 17.0이라는 것을 떠올린다면
해당 코드를 보고
// RuntimeApplicationChecksCocoa.cpp
if (linkedBefore(dyld_fall_2023_os_versions, DYLD_IOS_VERSION_17_0, DYLD_MACOSX_VERSION_14_0)) {
// override 허용되는 플래그
}
아래 사실을 유추할 수 있습니다.
- Xcode 14.3으로 빌드한 애들은 웹킷의 링킹 시점은 DYLD_IOS_VERSION_17_0 미만 -> 왜냐하면 Xcode 14.3 이 사용하는 iOS SDK 버전은 16.4 이기 때문 -> 따라서 override 허용
- Xcode 15.0으로 빌드한 애들은 웹킷의 링킹 시점은 DYLD_IOS_VERSION_17_0 이상 -> 왜냐하면 Xcode 15.0 이 사용하는 iOS SDK 버전은 17.0 이기 때문 -> 따라서 override 차단
(실행 환경이 iOS 17 미만이라면 어떤 버전의 엑스코드로 빌드하든지 간에, 애초에 가지고 있는 WebKit 자체가 예전 버전이기 때문에 이는 신경 쓸 필요가 없었구요)
UA override 여부를 표로 정리하면 이렇게 되겠죠?
~ iOS 16.6 | iOS 17 ~ | |
Xcode 14.3.1 | O | O |
Xcode 15.0 | O | X |
마무리
사실 문제의 원인 파악과 해결 자체는 크게 어렵지 않았고, 의심되는 커밋 내역이 iOS 17에서 반영된 게 맞냐~~! 이미 올라간 앱에서는 문제가 없냐~ 를 확인하는 게 까다로웠던 이슈였네요
웹 뷰.. 가끔씩 이럴 때마다 날 너무 지치게 해.. But! 있으면 편한걸..? 하지만!! 변경 사항이 있으면 알려줘야 할 거 아냐? However!! 애플이 언제부터 친절했다고.. Nevertheless! 또 이렇게 하나의 디버깅? 방법을 배워간 거 같아서 좋아.. 진짜… 내 마음은 뭘까?
ㅎ 이번 글로 비슷한 이슈를 겪고 있는 다른 분들께 도움이 되길 바라며!! 그럼 20000!!!
참고
'Mobile' 카테고리의 다른 글
jcenter, 이제 문 닫습니다 (32) | 2024.07.17 |
---|---|
statements 가 있는 switch/when 구문 deep dive (feat. bytecode) (0) | 2023.10.25 |
OS 10 이하에서 Backstack 에 Activity 가 중첩되어 쌓이는 이슈 (0) | 2023.07.05 |
DiffUtil 이해하기 (0) | 2023.05.17 |
버그와 함께 알아보는 RecyclerView 에서 wrap_content 사용을 조심해야 하는 이유 (0) | 2023.04.05 |