- 로딩·성공·실패·빈 상태를 어떻게 모델링해야 합니까?
- 검색 화면에서 이전 요청 취소를 왜, 어떻게 처리합니까?
- 같은 이미지를 동시에 10곳에서 요청하면 어떻게 됩니까?
- 화면 하나를 처음부터 설계한다면 어떤 순서로 말하겠습니까?
상태 모델링 — 여기서 절반이 갈린다 🔥 핵심
라이브코딩에서 가장 먼저 보는 건 화려한 알고리즘이 아니라 상태를 어떻게 표현하는가입니다.
Bool과 옵셔널을 늘어놓으면 실제로는 일어날 수 없는 조합이 타입상 표현 가능해집니다.
switch를 쓰면 새 케이스를 추가할 때 처리 누락을 컴파일러가 잡아줍니다.// ❌ 8가지 조합 중 4가지가 말이 안 됨
struct State {
var isLoading = false
var items: [Item] = []
var error: Error?
}
// ✅ 유효한 상태만 표현 가능
enum ViewState {
case idle
case loading
case loaded([Item])
case empty
case failed(Error)
}
이 원칙에는 이름이 있습니다 — "불가능한 상태를 표현 불가능하게 만든다 (make illegal states unrepresentable)". 라이브코딩 중에 이 말을 쓰면서 리팩토링하면, 단방향 아키텍처를 도구가 아니라 사상으로 이해했다는 신호가 됩니다.
단골 과제 ① — 검색 화면 🔥 핵심
"입력하면 검색되는 화면을 만들어보세요"는 가장 흔한 과제입니다. 쉬워 보이지만 채점 포인트가 네 개 숨어 있습니다.
// TCA — .cancellable(id:) 하나로 "이전 요청 취소"가 해결된다
case let .searchQueryChanged(query):
state.query = query
guard !query.isEmpty else {
state.results = []
return .cancel(id: CancelID.search)
}
return .run { send in
try await clock.sleep(for: .milliseconds(300)) // debounce
await send(.searchResponse(Result { try await client.search(query) }))
}
.cancellable(id: CancelID.search, cancelInFlight: true) // ← 핵심
| 채점 포인트 | 빠뜨리면 |
|---|---|
| debounce | 글자마다 요청 → 서버 부하·요금 |
| 이전 요청 취소 | 늦은 응답이 최신 결과를 덮어씀 |
| 빈 쿼리 처리 | 전체 목록을 불필요하게 요청 |
| 상태 4종 표현 | 로딩·빈 결과·에러가 구분 안 됨 |
cancelInFlight: true가 "같은 id의 진행 중인 작업을 취소하고 새로 시작"을 의미합니다.
Rx의 flatMapLatest, Combine의 switchToLatest와 같은 역할입니다.
세 프레임워크에서 같은 개념이 다른 이름으로 나온다는 걸 짚으면 좋은 인상을 줍니다.
단골 과제 ② — 이미지 캐시 🔥 핵심
"이미지 캐시를 만들어보세요"에서 진짜로 보는 건 메모리·디스크 2단 구조가 아니라 같은 URL이 동시에 여러 번 요청될 때의 처리입니다.
actor ImageCache {
private enum Entry {
case loading(Task<UIImage, Error>)
case ready(UIImage)
}
private var cache: [URL: Entry] = [:]
func image(for url: URL) async throws -> UIImage {
if let entry = cache[url] {
switch entry {
case .ready(let img): return img
case .loading(let task): return try await task.value // 남의 작업에 편승
}
}
let task = Task { try await download(url) }
cache[url] = .loading(task)
do {
let img = try await task.value
cache[url] = .ready(img)
return img
} catch {
cache[url] = nil // 실패는 캐시하지 않는다
throw error
}
}
}
실패를 캐시에 남기면 일시적인 네트워크 오류가 영구적인 실패로 굳습니다.
catch에서 항목을 지우는 이 한 줄을 빼먹는 경우가 아주 많습니다.
이미지 메모리 — 파일 크기가 아니다
캐시 용량을 계산할 때 파일 크기로 세면 크게 어긋납니다. 화면에 올라간 이미지가 쓰는 메모리는 디코딩된 비트맵 크기입니다.
let options: [CFString: Any] = [
kCGImageSourceCreateThumbnailFromImageAlways: true,
kCGImageSourceThumbnailMaxPixelSize: maxPixelSize, // 표시 크기 × scale
kCGImageSourceShouldCacheImmediately: true // 미리 디코딩 → 스크롤 hitch 방지
]
kCGImageSourceShouldCacheImmediately가 중요한 이유 — 기본적으로 디코딩은
화면에 그리는 순간 지연 수행됩니다. 그게 메인 스레드에서 일어나면 스크롤이 끊깁니다.
미리 백그라운드에서 디코딩해두면 부드러워집니다.
설계 토론 뼈대 🧩 롱테일
"이 화면을 처음부터 설계한다면?"에는 말하는 순서가 곧 점수입니다. 코드부터 시작하면 방향을 잃고, 아래 순서대로 가면 빠뜨리는 게 없습니다.
| 순서 | 말할 것 | 빠뜨리기 쉬운 것 |
|---|---|---|
| ① | 상태 모델링 (enum) | 빈 결과와 초기 상태 구분 |
| ② | 레이어 분리 | 각 층을 어떻게 테스트할지 |
| ③ | 모듈 경계 · Interface 의존 | 화면 이동을 Factory로 주입 |
| ④ | 비동기 정책 | 취소 · 중복 요청 합치기 |
| ⑤ | 캐싱 정책 | 데이터 종류마다 유효기간이 다름 |
| ⑥ | 관측 · 실패 처리 | 실패했을 때 사용자가 뭘 보나 |
⑤에서 "데이터 종류마다 캐시 정책이 다르다"를 짚으면 실무 감각이 드러납니다 — 목록은 짧게, 상세 정보는 길게, 실시간성이 필요한 값은 캐시하지 않습니다. 하나의 정책으로 전부 덮으려 하면 반드시 어딘가가 깨집니다.
최종 점검 — 라이브코딩 중 소리 내어 말할 것
| 타이밍 | 말할 문장 |
|---|---|
| 시작 직후 | "먼저 상태부터 정의하겠습니다. 불가능한 조합이 안 생기게 enum으로 가겠습니다." |
| 비동기 붙일 때 | "이 요청은 화면을 나가면 취소돼야 해서 취소 가능하게 만들겠습니다." |
| 막혔을 때 | "지금 A와 B 중 고민 중인데, A로 가겠습니다. 이유는 …입니다." |
| 시간이 부족할 때 | "여기는 실제로는 ○○ 처리가 더 필요한데, 지금은 뼈대부터 세우겠습니다." |
라이브코딩은 완성도보다 사고 과정을 봅니다. 침묵하며 코딩하면 평가할 근거가 없습니다. 막혔을 때 고민 중인 선택지를 소리 내어 말하는 것이 가장 안전한 전략입니다.