티스토리 뷰

들어가기 전에

안녕하세요 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!!!

참고

댓글