문제 상황
음악 스트리밍 앱을 만들고 있었는데요. 앨범 아트워크가 계속 깜빡이는 거예요. 곡이 바뀔 때만이 아니라 같은 곡을 재생하고 있는데도 아트워크가 순간적으로 사라졌다 나타나고요. 특히 풀 플레이어의 blur 배경이 심했어요.

“이미지 로딩이 느린가?” 싶어서 캐시를 넣었는데 여전히 깜빡이더라고요. 원인을 파고들었더니 버그가 3개 겹쳐 있었어요.
버그 1: NSCache 조회 순서가 반대
커스텀 CachedAsyncImage를 만들어서 NSCache에 이미지를 저장하고 있었는데요. 문제는 캐시 조회 순서였어요.
Before
// @State에 저장된 이미지를 먼저 확인
if let uiImage = uiImage ?? cache[url] {
Image(uiImage: uiImage)
} else {
placeholder
}
이 순서가 왜 문제냐면:
- URL이 바뀌면
@State uiImage는 아직 이전 곡의 이미지 - 이전 곡 이미지가 보여진 뒤에야 새 이미지 로딩 시작
NSCache가 메모리 부족으로 evict하면cache[url]도 nil → placeholder 노출
After
// 캐시를 먼저 확인
if let uiImage = cache[url] ?? uiImage {
Image(uiImage: uiImage)
} else {
placeholder
}
순서를 뒤집었어요. 캐시 조회를 먼저 하고, 캐시에 없을 때만 @State의 이전 이미지를 폴백으로 사용하는 거죠.
버그 2: blur 배경의 @State 리셋
풀 플레이어 배경에 앨범 아트워크를 blur 처리해서 깔고 있었는데요. 이것도 CachedAsyncImage를 쓰고 있었거든요.
문제는 SwiftUI가 View를 다시 그릴 때 @State가 리셋되는 타이밍과 NSCache가 evict하는 타이밍이 겹치면 — 배경이 순간적으로 빈 화면이 되는 거였어요.
해결
배경 전용으로는 CachedAsyncImage를 아예 안 쓰기로 했어요. 대신 static var로 마지막 유효한 이미지를 영구 보관하는 방식으로 바꿨어요.
struct CoverArtBackground: View {
let url: URL?
// 마지막으로 로드 성공한 이미지를 static으로 영구 보관
static var lastImage: UIImage?
var body: some View {
if let image = Self.lastImage {
Image(uiImage: image)
.resizable()
.scaledToFill()
.blur(radius: 40)
.overlay(Color.black.opacity(0.5))
}
}
}
static var라서 View가 다시 그려져도, NSCache가 evict해도, 배경 이미지는 절대 사라지지 않아요.

버그 3: 매 렌더링마다 바뀌는 URL
여기까지 고쳤는데 아직도 깜빡이더라고요. 이게 가장 찾기 어려웠어요.
API 인증에 랜덤 salt를 사용하고 있었는데요:
func authParams() -> String {
let salt = UUID().uuidString // 매번 다른 salt!
let token = md5(password + salt)
return "u=\(user)&t=\(token)&s=\(salt)"
}
func getCoverArtURL(id: String, size: Int) -> URL {
// 매 호출마다 auth 파라미터가 다르므로 URL도 매번 다름
URL(string: "\(serverURL)/rest/getCoverArt?\(authParams())&id=\(id)&size=\(size)")!
}
SwiftUI의 .task(id: url)을 쓰고 있었는데, 렌더링할 때마다 URL이 달라지니까 task가 매번 재실행되는 거예요. 캐시 키도 URL 기반이라 캐시 히트도 안 되고요.
해결
캐시 키와 .task(id:)에는 auth 파라미터를 제외한 stable URL을 사용하도록 바꿨어요.
// auth 파라미터 제외한 안정적 URL (캐시 키 + task id용)
var stableURL: String {
"\(serverURL)/rest/getCoverArt?id=\(id)&size=\(size)"
}
// .task(id:)에 stable ID 사용
.task(id: stableURL) {
// 이제 같은 곡이면 재실행되지 않음
await loadImage(from: fullURL)
}
전체 그림
정리하면 이런 상황이었어요:
| 버그 | 원인 | 증상 |
|---|---|---|
| 캐시 조회 순서 | @State 우선 → stale 이미지 + placeholder 노출 | 곡 전환 시 이전 아트워크 깜빡 |
| @State 리셋 | View 재생성 + NSCache evict 동시 발생 | blur 배경 순간 소실 |
| URL 불안정 | 랜덤 salt → URL 매번 변경 → 캐시 미스 | 같은 곡인데도 이미지 재로딩 |
세 가지가 동시에 발생하니까 “전체적으로 깜빡인다"로만 보였고, 하나만 고쳐서는 “아직 깜빡이네?” 하게 되는 거였어요.
배운 점
이미지 깜빡임은 원인이 한 가지가 아닐 수 있다. 캐시 조회 순서, View 라이프사이클, URL 안정성을 모두 체크할 것.
특히 API 인증에 랜덤 값을 쓰는 경우, 캐시 키와 SwiftUI의 .task(id:)에 영향을 주지 않도록 분리하는 게 중요했어요.