목록의 썸네일이 처음에는 정상적으로 로드되고 두 번째에는 거의 즉시 나타나면 ‘캐시가 작동한다’고 판단하기 쉽습니다. 하지만 뷰가 이미지 객체를 그대로 보유하고 있을 수도 있습니다. iOS의 HTTP 캐시를 검증하려면 서버가 실제로 어떤 요청을 받았는지 확인해야 합니다. 최초 요청에는 이미지 본문을 반환하고, 콘텐츠가 만료된 뒤에는 클라이언트가 ETag를 담아 재검증을 요청해야 합니다. 오프라인 읽기는 빠른 새로고침 한 번으로 대신하지 말고 별도로 테스트해야 합니다.
먼저 테스트 범위 정하기
클라우드 Mac에서 테스트 서버와 iOS 시뮬레이터를 함께 실행합니다. 시뮬레이터에서 localhost에 접속하면 지역 간 네트워크 경로의 변동이 캐시 판단에 섞이지 않습니다. 테스트 이미지로 작은 PNG 파일 하나를 골라 현재 작업 디렉터리에 thumb.png라는 이름으로 둡니다. 이번 테스트 중에는 URL, 계정, 이미지 디코더를 바꾸지 마세요.
확인할 관찰 결과는 세 가지입니다.
| 상황 | 서버에서 확인할 내용 | 앱에서 얻어야 할 결과 |
|---|---|---|
| 최초 요청 | If-None-Match 없이 요청을 받고 200 반환 |
이미지 바이트 |
| 재요청 | 일치하는 ETag를 담은 요청을 받고 304 반환 | 캐시를 재검증해 얻은 이미지 |
| 명시적인 오프라인 읽기 | 새 요청 없음 | 저장된 이미지 또는 명확한 캐시 미스 |
304 응답에는 이미지 본문이 없습니다. 앱에 이미지가 표시되는 것은 클라이언트가 저장된 콘텐츠와 재검증 결과를 함께 사용하기 때문입니다. ‘UI에 이미지가 표시됐다’를 ‘서버가 이미지를 다시 보냈다’로 잘못 기록하지 마세요.
조건부 요청을 기록하는 서버 실행하기
이미지가 있는 디렉터리에 다음 스크립트를 fixture.py로 저장합니다. 이 서버는 로컬 루프백 주소에서만 수신하며 테스트 포트를 외부에 열지 않습니다. max-age=0은 이후 일반 요청에서 재검증이 필요하도록 하고, 고정된 ETag는 요청 흐름을 관찰하기 쉽게 해 줍니다.
from http.server import BaseHTTPRequestHandler, HTTPServer
from pathlib import Path
BODY = Path("thumb.png").read_bytes()
TAG = '"thumb-v1"'
class Handler(BaseHTTPRequestHandler):
def do_GET(self):
if self.path != "/thumb":
self.send_error(404)
return
matched = self.headers.get("If-None-Match") == TAG
status = 304 if matched else 200
print(f"If-None-Match={self.headers.get('If-None-Match')!r} status={status}", flush=True)
self.send_response(status)
self.send_header("ETag", TAG)
self.send_header("Cache-Control", "public, max-age=0")
if not matched:
self.send_header("Content-Type", "image/png")
self.send_header("Content-Length", str(len(BODY)))
self.end_headers()
if not matched:
self.wfile.write(BODY)
HTTPServer(("127.0.0.1", 8765), Handler).serve_forever()
python3 fixture.py를 실행한 뒤 다른 터미널에서 curl -si http://127.0.0.1:8765/thumb -o /dev/null을 실행해 200 응답을 확인합니다. 이어서 다음 명령을 실행합니다.
curl -si -H 'If-None-Match: "thumb-v1"' \
http://127.0.0.1:8765/thumb -o /dev/null
로그에 각각 200과 304가 나타나야 합니다. 이 단계는 테스트 서버가 예상대로 동작한다는 것만 보여 줍니다. iOS가 조건부 요청을 자동으로 보냈다는 증거는 아닙니다.
같은 세션에서 시뮬레이터 요청 검증하기
테스트 타깃에 별도의 URLSessionConfiguration.default를 사용하고, 메모리 용량이 충분한 URLCache를 설정합니다. 요청 정책은 .useProtocolCachePolicy로 유지합니다. 같은 URL과 같은 session으로 두 번 연속 요청하되 매번 작업이 완료될 때까지 기다리고, 받은 PNG를 디코딩할 수 있는지 확인합니다. 서버 로그에서 첫 번째 요청은 200이어야 합니다. 두 번째 요청이 실제로 서버에 도달했다면 If-None-Match를 포함하고 304 응답을 받아야 합니다.
let configuration = URLSessionConfiguration.default
configuration.urlCache = URLCache(
memoryCapacity: 8 * 1024 * 1024,
diskCapacity: 0
)
configuration.requestCachePolicy = .useProtocolCachePolicy
let session = URLSession(configuration: configuration)
let url = URL(string: "http://localhost:8765/thumb")!
앱의 두 이미지 요청이 이 session을 공유하도록 하고, 요청마다 새 구성을 만들지 마세요. 시뮬레이터의 테스트 타깃에서 전송 보안 정책 때문에 로컬 HTTP가 차단된다면 테스트 타깃의 설정에만 로컬 네트워크 예외를 허용할 수 있습니다. 개발용 예외를 배포용 앱에 포함하지 마세요. 서버 로그에 두 번째 요청 자체가 없다면 먼저 메모리에 있는 이미지가 재사용됐는지, 두 번째 로드를 실제로 시작했는지 확인한 다음 캐시 정책을 살펴보세요. 요청 완료 시간만으로는 이 경우들을 구분할 수 없습니다.
이미지를 바꿀 때 ETag 확인하기
서버의 TAG를 '"thumb-v2"'로 바꾸고 thumb.png도 교체합니다. 서버를 다시 시작한 뒤 요청하면 기존 ETag가 일치하지 않으므로 새 응답은 200이어야 하며, 앱도 새 이미지를 받아야 합니다. 실제 프로젝트에서는 콘텐츠 버전이 바뀔 때 ETag도 바뀌어야 합니다. 파일만 교체하고 이전 식별자를 그대로 쓰면 재검증 과정에서 오래된 이미지가 잘못 유지될 수 있습니다.
오프라인 읽기는 별도로 테스트하기
먼저 온라인에서 200 요청을 한 번 완료하고 해당 응답을 저장할 수 있는지 확인합니다. 그런 다음 테스트 서버를 중지하고 같은 URL에 대해 .returnCacheDataDontLoad 정책을 적용한 요청을 만듭니다. 작업은 기존 캐시를 보유한 session으로 계속 실행합니다. 성공했다면 이번 요청은 캐시에서 읽은 것입니다. 실패했다면 서버 장애라고 단정하지 말고 캐시 미스로 기록해야 합니다. 테스트가 끝나면 서버 로그를 확인하세요. 서버를 중지하기 전후로 오프라인 요청에서 비롯된 새 기록이 없어야 합니다.
여기서 사용하는 오프라인 정책은 앱이 명시적으로 선택한 동작입니다. 기본 요청이 네트워크가 끊겼을 때 반드시 오래된 이미지로 대체된다는 뜻은 아닙니다. 캐시 용량, 시스템의 캐시 정리, 응답 헤더에 따라 적중 여부가 달라집니다. 실제 화면에서는 캐시 미스에 대비한 자리 표시 상태를 준비하고 네트워크가 복구되면 다시 시도할 수 있게 해야 합니다. ‘전에 열어 봤다’는 이유만으로 항상 읽을 수 있다고 가정하지 마세요.
오판을 점검하고 검증 기록 남기기
매번 테스트 URL, ETag, 응답의 캐시 헤더, 서버 로그의 상태 코드 순서, 앱에 최종 표시된 이미지 버전을 기록합니다. 200→200이 나온다면 응답을 캐시할 수 있는지, 두 요청이 URL과 캐시를 공유하는지부터 확인하세요. 서버에는 304가 기록됐는데 UI에 여전히 이전 이미지가 보인다면 응답 처리 후 뷰 계층에 남아 있던 이전 객체가 이미지를 덮어썼는지 확인합니다. 이미지를 업데이트한 뒤에도 304가 반환된다면 서버의 식별자가 실제로 바뀌었는지 먼저 확인하세요.
마지막으로 시뮬레이터에 앱을 새로 설치해 첫 테스트를 반복합니다. 이전 실험에서 남은 캐시가 결론에 영향을 주지 않도록 하기 위해서입니다. 이렇게 기록하면 ‘이미지가 빨리 뜨는 것 같다’는 인상을 재확인 가능한 200, 304, 오프라인 캐시 적중, 캐시 미스의 네 가지 결과로 바꿀 수 있습니다. 팀이 이미지 서비스나 클라이언트 캐시 정책을 조정한 뒤 재검증하기도 쉬워집니다.
자주 묻는 질문
서버가 실제로 304를 반환했는지 어떻게 확인하나요?
서버 요청 기록에서 If-None-Match 헤더와 응답 상태를 확인하세요. 앱에 이미지가 표시되는지만으로는 판단할 수 없습니다.
한 번 받은 이미지를 오프라인에서 읽지 못하는 이유는 무엇인가요?
캐시 항목이 제거됐거나 저장할 수 없는 응답일 수 있습니다. 일반 네트워크 요청과 별도로 캐시 미적중 상태도 처리해야 합니다.
다음 원격 개발을 위한 Mac 준비하기
용도에 맞는 모델과 리전을 선택하고 화면 공유, VNC 또는 SSH로 연결하세요. 가격과 실제 이용 가능 여부는 콘솔에서 실시간으로 확인할 수 있습니다.