iOS 웹뷰 쿠키 삭제를 깊게 살펴보기
WKDataStore API와 WKHTTPCookieStore API 차이점을 확인해 보기
회사에서 웹뷰 쿠키 삭제 코드 리뷰를 하던 중 다음과 같은 이야기가 있어 글을 쓰게 되었습니다.
리뷰어: WKDataStore API를 사용하여 쿠키 삭제를 구현하셨네요. WKHTTPCookieStore API를 사용하지 않은 이유가 특별히 있을까요? 참고한 레퍼런스가 있다면 알려주셔도 좋아요.
코드 작성자: Apple 개발자 사이트를 참고했고, 특별한 이유는 없습니다. 사용을 하면 문제가 있을까요?
리뷰어: WKHTTPCookieStore는 여러 가지 웹뷰 쿠키 문제를 해결하기 위하여 더 나중에 나온 API라서 이야기했습니다.
코드 작성자: WKDataStore를 사용해도 쿠키 전체 삭제 기능 구현에 문제가 없었습니다.
작성한 코드의 사용이 기대대로 동작하였으므로 당연하게 코드 변경사항은 머지 되었습니다. 그러나 코드 리뷰에서 명확하게 설명하지 못한 제 코멘트가 마음의 짐으로 남았습니다. 그래서 코드 동작의 결과가 같다고 내부 수행 로직도 같은지 알아보기로 하였습니다.
애플 개발자 문서의 2개 항목과 WKWebView 내부 동작을 확인하기 위하여 WebKit 오픈소스를 확인하였습니다. WKWebView는 애플 주도의 오픈소스 웹 엔진 WebKit을 사용하고 있습니다. 애플 사파리의 구현과 동작의 확인은 어렵지만, 서드파티 개발자와 함께 사용하는 WKWebView의 코드 구현과 동작은 다행히 오픈소스에서 찾을 수 있습니다.
- https://developer.apple.com/documentation/webkit/wkwebsitedatastore
- https://developer.apple.com/documentation/webkit/wkhttpcookiestore
- https://github.com/WebKit/WebKit
1. WKWebSiteDataStore
An object that manages cookies, disk and memory caches, and other types of data for a web view.
이 오브젝트는 웹뷰의 쿠키 외에 다른 형태의 데이터를 함께 다루고 있습니다. 그러나 여기에 var httpCookieStore: WKHTTPCookieStore
와 같이 재미난 프로퍼티가 있습니다. 놓칠 수 없는 이유가 하나 더 생겼네요.
이번에 집중해서 살펴볼 동작은 removeData(ofTypes:modifiedSince:completionHandler:)
함수입니다.
우리는 클래스명을 알고 있니 바로 WKWebSiteDataStore.mm
이라는 구현 파일을 찾아볼 수 있습니다. 여기서부터 코드를 하나하나 따라가보도록 하겠습니다.
// 1. 시작점: WKWebSiteDataStore.mm
- (void)removeDataOfTypes:(NSSet *)dataTypes modifiedSince:(NSDate *)date completionHandler:(void (^)(void))completionHandler
{
auto completionHandlerCopy = makeBlockPtr(completionHandler);
// 2. 여기에요. 여기를 따라가기로 해요.
_websiteDataStore->removeData(WebKit::toWebsiteDataTypes(dataTypes), toSystemClockTime(date ? date : [NSDate distantPast]), [completionHandlerCopy] {
completionHandlerCopy();
});
}
// 3. _websiteDataStore 를 알기 위해서 WKWebsiteDataStoreInternal.h 를 열어봅니다.
@interface WKWebsiteDataStore () <WKObject> {
@package
// 4. 중요한 것은 <> 안의 내용입니다.
API::ObjectStorage<WebKit::WebsiteDataStore> _websiteDataStore;
WeakObjCPtr<id <_WKWebsiteDataStoreDelegate> > _delegate;
}
@end
// 5. 이제 WebsiteDataStore.cpp 를 열어서 확인합니다.
void WebsiteDataStore::removeData(OptionSet<WebsiteDataType> dataTypes, WallTime modifiedSince, Function<void()>&& completionHandler)
{
// 생략...
#if ENABLE(INTELLIGENT_TRACKING_PREVENTION)
bool didNotifyNetworkProcessToDeleteWebsiteData = false;
#endif
auto networkProcessAccessType = computeNetworkProcessAccessTypeForDataRemoval(dataTypes, !isPersistent());
switch (networkProcessAccessType) {
case ProcessAccessType::Launch:
networkProcess();
ASSERT(m_networkProcess);
FALLTHROUGH;
case ProcessAccessType::OnlyIfLaunched:
if (m_networkProcess) {
// 6. 찾았어요! 여기에요.
m_networkProcess->deleteWebsiteData(m_sessionID, dataTypes, modifiedSince, [callbackAggregator] { });
#if ENABLE(INTELLIGENT_TRACKING_PREVENTION)
didNotifyNetworkProcessToDeleteWebsiteData = true;
#endif
}
break;
case ProcessAccessType::None:
break;
}
// 생략...
}
// 7. m_networkProcess는 NetworkProcessProxy 입니다.
// 웹킷의 프로세스간 통신(IPC)은 Proxy를 통해서 NetworkProcess로 메시지를 전달하여 동작하는 방식입니다.
// 그래서 NetworkProcess.cpp 를 열어봅니다.
void NetworkProcess::deleteWebsiteData(PAL::SessionID sessionID, OptionSet<WebsiteDataType> websiteDataTypes, WallTime modifiedSince, CompletionHandler<void()>&& completionHandler)
{
// 생략...
if (websiteDataTypes.contains(WebsiteDataType::Cookies)) {
if (auto* networkStorageSession = storageSession(sessionID))
// 8. Cookies 라는 커다란 힌트 덕분에 쉽게 찾을 수 있어요.
networkStorageSession->deleteAllCookiesModifiedSince(modifiedSince, [clearTasksHandler] { });
}
// 생략...
}
// 9. NetworkStorageSession.cpp를 찾으면, deleteAllCookiesModifiedSince()가 없습니다.
// 당황하지 말고 Cocoa 구현체인 NetworkStorageSessionCocoa.mm 을 열어보아요.
void NetworkStorageSession::deleteAllCookiesModifiedSince(WallTime timePoint, CompletionHandler<void()>&& completionHandler)
{
ASSERT(hasProcessPrivilege(ProcessPrivilege::CanAccessRawCookies));
// FIXME: Do we still need this check? Probably not.
if (![NSHTTPCookieStorage instancesRespondToSelector:@selector(removeCookiesSinceDate:)])
return completionHandler();
NSTimeInterval timeInterval = timePoint.secondsSinceEpoch().seconds();
auto work = [completionHandler = WTFMove(completionHandler), storage = RetainPtr { nsCookieStorage() }, date = RetainPtr { [NSDate dateWithTimeIntervalSince1970:timeInterval] }] () mutable {
// 10. 마침내!! 여기서 지우고 있군요. 그런데 storege인 nsCookieStorage() 를 따라가니 NSHTTPCookieStorage 입니다.
[storage removeCookiesSinceDate:date.get()];
[storage _saveCookies:makeBlockPtr([completionHandler = WTFMove(completionHandler)] () mutable {
ensureOnMainThread(WTFMove(completionHandler));
}).get()];
};
if (m_isInMemoryCookieStore)
return work();
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), makeBlockPtr(WTFMove(work)).get());
}
코드를 확인한 결과
NSHTTPCookieStorage
의removeCookies(since:)
함수를 최종 호출합니다.
2. WKHTTPCookieStore
An object that manages the HTTP cookies associated with a particular web view.
개발자 문서의 한 줄 설명으로는 아직 차이를 알기 어렵습니다. 그런데 본문에 아래 내용이 있습니다.
You don’t create a
WKHTTPCookieStore
object directly. Instead, retrieve this object from theWKWebsiteDataStore
object in your web view’s configuration object.
앞서 WKWebsiteDataStore에서 확인한 httpCookieStore property와 연결고리를 찾았네요.
이제 delete(_:completionHandler:)
함수를 기준으로 코드를 따라가면서 살펴보겠습니다.
// 1. 시작점: WKHTTPCookieStore.mm
- (void)deleteCookie:(NSHTTPCookie *)cookie completionHandler:(void (^)(void))completionHandler
{
// 2. 여기에요. 여기를 따라가기로 해요.
_cookieStore->deleteCookie(cookie, [handler = adoptNS([completionHandler copy])]() {
auto rawHandler = (void (^)())handler.get();
if (rawHandler)
rawHandler();
});
}
// 3. WKHTTPCookieStoreInternal.h 열어보기
@interface WKHTTPCookieStore () <WKObject> {
@package
// 4. _cookieStore 확인 완료
API::ObjectStorage<API::HTTPCookieStore> _cookieStore;
}
@end
// 5. 이제 APIHTTPCookieStore.cpp 열어봅니다.
void HTTPCookieStore::deleteCookie(const WebCore::Cookie& cookie, CompletionHandler<void()>&& completionHandler)
{
if (auto* networkProcess = networkProcessIfExists())
// 6. 발견, Proxy는 앞 구조와 같으니 생략하고 메세지를 확인합니다.
networkProcess->sendWithAsyncReply(Messages::WebCookieManager::DeleteCookie(m_sessionID, cookie), WTFMove(completionHandler));
else
completionHandler();
}
// 7. NetworkProcess 아래에 WebCookieManager.cpp 를 열어요.
void WebCookieManager::deleteCookie(PAL::SessionID sessionID, const Cookie& cookie, CompletionHandler<void()>&& completionHandler)
{
if (auto* storageSession = m_process.storageSession(sessionID))
// 8. 다음 차례가 쉽게 보이네요.
storageSession->deleteCookie(cookie, WTFMove(completionHandler));
else
completionHandler();
}
// 9. NetworkStorageSessionCocoa.mm 를 열어봅니다.
void NetworkStorageSession::deleteCookie(const Cookie& cookie, CompletionHandler<void()>&& completionHandler)
{
ASSERT(hasProcessPrivilege(ProcessPrivilege::CanAccessRawCookies) || m_isInMemoryCookieStore);
auto work = [completionHandler = WTFMove(completionHandler), cookieStorage = RetainPtr { nsCookieStorage() }, cookie = RetainPtr { (NSHTTPCookie *)cookie }] () mutable {
// 10. 최종 확인 완료!
[cookieStorage deleteCookie:cookie.get()];
ensureOnMainThread(WTFMove(completionHandler));
};
if (m_isInMemoryCookieStore)
return work();
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), makeBlockPtr(WTFMove(work)).get());
}
코드 확인 결과
NSHTTPCookieStorage
의deleteCookie(_:)
를 최종 호출합니다.
3. 결론
WKWebSiteDataStore 또는 WKHTTPCookieStore 모두 최종 쿠키 접근 Cocoa 구현에 따르면 NSHTTPCookieStorage API를 사용하고 있습니다. 따라서 쿠키 삭제 호출 형태는 다를지라도, 동작은 같다고 기대할 수 있습니다.
여기서 잠깐!
WKWebView 구현에서 쿠키의 삭제를 NSHTTPCookieStorage를 사용하고 있다면, WebKit API 대신에 iOS Native API를 사용하면 어떨까요?
우리는 이미 이 내용을 경험적으로 알고 있습니다. iOS 8에서 WKWebView가 처음 공개되었고, iOS 9에서 WKWebSiteDataStore API가 추가되었으며, iOS 11에서 WKHTTPCookieStore API가 추가되었습니다. 즉 웹뷰의 쿠키 관리는 네이티브 API로는 해결되지 않았음을 알 수 있는데요.
그 이유를 웹킷 코드에서 계속 알아보겠습니다. 이번에는 바로 WebSiteDataStoreCocoa.mm
파일을 열어보겠습니다.
// 1. WebSiteDataStoreCocoa.mm
String WebsiteDataStore::resolvedCookieStorageDirectory()
{
if (m_resolvedCookieStorageDirectory.isNull()) {
if (!isPersistent())
m_resolvedCookieStorageDirectory = emptyString();
else {
// 2. 여기서 쿠키 경로를 설정하고 있습니다. 함수를 따라가볼게요.
auto directory = cacheDirectoryInContainerOrHomeDirectory("/Library/Cookies"_s);
m_resolvedCookieStorageDirectory = resolveAndCreateReadWriteDirectoryForSandboxExtension(directory);
}
}
return m_resolvedCookieStorageDirectory;
}
// 3. 경로 생성 관여 로직
String WebsiteDataStore::cacheDirectoryInContainerOrHomeDirectory(const String& subpath)
{
// 4. 내용 확인을 위해 다른 파일로 이동해볼게요.
String path = pathForProcessContainer();
if (path.isEmpty())
path = NSHomeDirectory();
return path + subpath;
}
// 5. SandboxUtilities.mm 파일 열기
String pathForProcessContainer()
{
std::array<char, MAXPATHLEN> path;
path[0] = 0;
// 6. 확인 완료. pid(=프로세스 아이디)를 사용하고 있습니다.
sandbox_container_path_for_pid(getpid(), path.data(), path.size());
return String::fromUTF8(path.data());
}
코드 확인 결과
- 경로 생성에 pid, 즉 프로세스 아이디를 사용합니다.
- WKWebView는 멀티 프로세스를 사용합니다. iOS 애플리케이션 프로세스와 다른 프로세스가 생성되므로 다른 파일 경로에 쿠키를 읽고 쓰도록 동작합니다.
웹뷰 쿠키 삭제, 이제 어려워하지 말고 경우에 따라서 편한 함수를 마음껏 사용하세요. (단, NSHTTPCookieStore API를 바로 쓰는 것은 안돼요)