- TableView 스크롤이 끊길 때 무엇부터 확인하나요?
- 셀 재사용은 왜 필요한가요? 그것만으로 충분한가요?
- 실제로 어떻게 측정하고 무엇을 고쳤나요?
cellForRowAt과 layoutSubviews에서 예산을 쓰지 않는다. 미리 하거나, 옮기거나, 없앤다30초 답변 🔥 먼저 이 문장
그래서 최적화 원칙은 하나예요 —
cellForRowAt과 layoutSubviews에서 예산을 쓰지 않는다.
비싼 일은 ① 미리 하거나(prefetch·캐시), ② 다른 스레드로 보내거나(디코딩), ③ 아예 하지 않게(계층 축소) 만듭니다.다만 어느 단계가 병목인지 먼저 확인해야 합니다. Layout이 문제인데 이미지를 최적화하면 아무 소용이 없거든요.
L1개념 — 프레임 예산과 파이프라인
L2설계 — 항목별 처방과 코드
A. 재사용 제대로 쓰기
final class FeedCell: UITableViewCell {
private let thumbnail = UIImageView()
private var imageTask: Task<Void, Never>?
private(set) var representedID: UUID?
override func prepareForReuse() {
super.prepareForReuse()
imageTask?.cancel() // ✅ 진행 중 다운로드 취소
imageTask = nil
thumbnail.image = nil // ✅ 이전 이미지 제거 (안 하면 깜빡임)
representedID = nil
accessoryType = .none // ✅ 이전 셀의 상태가 남지 않게
contentView.alpha = 1 // ✅ 애니메이션 중이었을 수 있다
}
func configure(_ item: Item, loader: ImageLoader) {
representedID = item.id
titleLabel.text = item.title
imageTask = Task { [weak self] in
guard let image = try? await loader.image(for: item.imageURL) else { return }
// ✅ 재사용 검증 — 응답이 늦게 오면 다른 셀이 되어 있을 수 있다
guard self?.representedID == item.id else { return }
self?.thumbnail.image = image
}
}
}셀 A가 이미지를 요청하고 스크롤로 재사용되어 셀 B가 됐는데, A의 응답이 뒤늦게 도착하면 B에 A의 이미지가 박힙니다. 이건 네트워크 버그가 아니라 race condition입니다 — 요청 시점과 응답 시점의 대상이 달라진 것이죠. 그래서 ①취소 + ②재사용 검증 둘 다 필요합니다. 취소만으로는 이미 날아간 응답을 막지 못합니다.
B. 높이 계산 비용 없애기
// ── 최선: 고정 높이 ──
tableView.rowHeight = 88 // 계산 자체가 없다
// ── 차선: 계산해서 캐시 ──
@MainActor
final class HeightCache {
private var cache: [UUID: CGFloat] = [:]
private var cachedWidth: CGFloat = 0
func height(for item: Item, width: CGFloat, font: UIFont) -> CGFloat {
if width != cachedWidth { // 회전 등으로 너비가 바뀌면 전부 무효
cache.removeAll()
cachedWidth = width
}
if let hit = cache[item.id] { return hit }
let bounding = (item.title as NSString).boundingRect(
with: CGSize(width: width - 32, height: .greatestFiniteMagnitude),
options: [.usesLineFragmentOrigin, .usesFontLeading],
attributes: [.font: font], context: nil)
let h = ceil(bounding.height) + 24
cache[item.id] = h
return h
}
}
func tableView(_ tv: UITableView, heightForRowAt ip: IndexPath) -> CGFloat {
heightCache.height(for: items[ip.row], width: tv.bounds.width, font: .preferredFont(forTextStyle: .body))
}
// ── 셀프사이징을 쓸 때는 추정값을 실제와 비슷하게 ──
tableView.rowHeight = UITableView.automaticDimension
tableView.estimatedRowHeight = 88 // 실제와 멀면 스크롤바가 튄다
// 정확한 높이를 직접 준다면 추정을 끄는 게 낫다
tableView.estimatedRowHeight = 0C. cellForRowAt에서 하면 안 되는 것
// ❌ ① DateFormatter를 매번 생성 — 매우 비싸다 (로케일·캘린더 초기화)
func tableView(_ tv: UITableView, cellForRowAt ip: IndexPath) -> UITableViewCell {
let f = DateFormatter()
f.dateFormat = "yyyy.MM.dd"
cell.dateLabel.text = f.string(from: item.date)
return cell
}
// ✅ 재사용 (또는 iOS 15+ 의 formatted API)
enum Formatters {
static let date: DateFormatter = {
let f = DateFormatter()
f.dateFormat = "yyyy.MM.dd"
f.locale = Locale(identifier: "ko_KR")
return f
}()
}
cell.dateLabel.text = Formatters.date.string(from: item.date)
// ❌ ② NSAttributedString을 매번 생성
cell.body.attributedText = NSAttributedString(string: item.text, attributes: attrs)
// ✅ 캐시
final class TextCache {
private var cache: [UUID: NSAttributedString] = [:]
func attributed(_ item: Item) -> NSAttributedString {
if let hit = cache[item.id] { return hit }
let s = NSAttributedString(string: item.text, attributes: attrs)
cache[item.id] = s
return s
}
}
// ❌ ③ 디스크 I/O · JSON 파싱 · 정규식 컴파일
let json = try! Data(contentsOf: fileURL) // 💥 메인 스레드 블로킹
// ❌ ④ 셀 안에서 매번 뷰 생성
cell.contentView.addSubview(UILabel()) // 💥 재사용될수록 쌓인다
// ✅ 셀 init에서 한 번만 만들고 configure에서 값만 바꾼다D. 이미지 — 가장 흔한 병목
import ImageIO
import UIKit
// ✅ 필요한 크기만 디코딩한다 — 원본을 통째로 메모리에 올리지 않는다
func downsample(imageAt url: URL, to pointSize: CGSize, scale: CGFloat) -> UIImage? {
let srcOptions = [kCGImageSourceShouldCache: false] as CFDictionary
guard let src = CGImageSourceCreateWithURL(url as CFURL, srcOptions) else { return nil }
let maxPixel = max(pointSize.width, pointSize.height) * scale
let options = [
kCGImageSourceCreateThumbnailFromImageAlways: true,
kCGImageSourceShouldCacheImmediately: true, // 지금 디코딩해 둔다
kCGImageSourceCreateThumbnailWithTransform: true,
kCGImageSourceThumbnailMaxPixelSize: maxPixel
] as CFDictionary
guard let cg = CGImageSourceCreateThumbnailAtIndex(src, 0, options) else { return nil }
return UIImage(cgImage: cg)
}
// 왜 필요한가 — 메모리 계산
// 4000×3000 원본 = 4000 × 3000 × 4바이트 ≈ 48MB (디코딩된 비트맵)
// 100pt 셀 @3x = 300×300 × 4 ≈ 0.36MB
// → 셀 20개면 960MB vs 7MB. Jetsam이 도는 이유가 이거다. (09번)
// ✅ 백그라운드에서 미리 디코딩해 두면 메인 스레드가 안 멈춘다
actor ImagePipeline {
private let cache = NSCache<NSURL, UIImage>()
func image(for url: URL, size: CGSize, scale: CGFloat) async -> UIImage? {
if let hit = cache.object(forKey: url as NSURL) { return hit }
let img = await Task.detached(priority: .utility) {
downsample(imageAt: url, to: size, scale: scale)
}.value
if let img { cache.setObject(img, forKey: url as NSURL) }
return img
}
}UIImage(named:)는 왜 첫 표시가 느린가
UIImage는 지연 디코딩합니다. 생성 시점에는 파일 참조만 들고 있다가, 실제로 그려질 때 압축을 풀어 비트맵을 만듭니다.
그리고 그 시점이 메인 스레드의 Display 단계입니다 — 정확히 프레임 예산 안이죠.
그래서 스크롤 중에 새 이미지가 나타날 때 끊깁니다. 해법은 디코딩을 미리, 백그라운드에서 끝내 두는 것입니다.
E. 렌더링 비용 줄이기
// ✅ ① 불투명하게 — 블렌딩 계산이 사라진다
cell.contentView.isOpaque = true
cell.contentView.backgroundColor = .systemBackground // 투명이면 의미 없다
// ✅ ② 그림자에는 반드시 shadowPath — 없으면 매번 알파 채널을 스캔한다
layer.shadowPath = UIBezierPath(roundedRect: bounds, cornerRadius: 12).cgPath
layer.shadowOpacity = 0.15
layer.shouldRasterize = true // 정적이라면
layer.rasterizationScale = UIScreen.main.scale // ⚠️ 안 주면 흐려진다
// ✅ ③ 계층을 얕게 — 중첩 스택뷰는 제약 수가 빠르게 늘어난다
// ❌ VStack(HStack(VStack(...))) 같은 4중 중첩
// ✅ 정적인 부분은 한 장의 이미지로 합성하거나, 프레임 기반 배치 고려
// ✅ ④ draw(_:)를 구현하지 않는다 — 구현하면 백킹스토어가 생긴다
// ❌ override func draw(_ rect: CGRect) { /* 원 그리기 */ }
// ✅ layer 속성으로 대체
view.layer.cornerRadius = 8
view.backgroundColor = .systemRed
// ⚠️ 정확히 말하기: "cornerRadius는 무조건 느리다"는 옛말이다.
// 단순한 cornerRadius는 최적화되어 있고,
// masksToBounds·mask·그룹 불투명과 겹칠 때 오프스크린 렌더가 발생한다.
// 확인: Instruments → Core Animation → Color Offscreen-Rendered YellowF. 미리 하기
// ✅ 화면에 나타나기 전에 미리 받아 둔다
extension FeedViewController: UITableViewDataSourcePrefetching {
func tableView(_ tv: UITableView, prefetchRowsAt indexPaths: [IndexPath]) {
for ip in indexPaths {
pipeline.prefetch(items[ip.row].imageURL)
}
}
func tableView(_ tv: UITableView, cancelPrefetchingForRowsAt indexPaths: [IndexPath]) {
for ip in indexPaths {
pipeline.cancelPrefetch(items[ip.row].imageURL) // ✅ 취소도 반드시
}
}
}
tableView.prefetchDataSource = self
// ✅ Diffable — 인덱스 불일치 크래시가 구조적으로 사라진다
private lazy var dataSource = UITableViewDiffableDataSource<Section, Item.ID>(
tableView: tableView
) { [weak self] tv, ip, id in
let cell = tv.dequeueReusableCell(withIdentifier: "cell", for: ip) as! FeedCell
if let item = self?.itemsByID[id] { cell.configure(item, loader: self!.pipeline) }
return cell
}
func apply(_ items: [Item], animating: Bool) {
var snapshot = NSDiffableDataSourceSnapshot<Section, Item.ID>()
snapshot.appendSections([.main])
snapshot.appendItems(items.map(\.id))
dataSource.apply(snapshot, animatingDifferences: animating)
}
// ⚠️ 항목 ID를 쓰면 "내용만 바뀐 경우" 갱신이 안 될 수 있다
// → reconfigureItems(_:) (iOS 15+) 로 셀 재구성만 요청한다L3구현 — 측정 없이 최적화하지 않는다
# ── ① Instruments → Time Profiler ──
# · 메인 스레드만 필터 (Main Thread)
# · "Invert Call Tree" 로 실제 시간을 쓰는 리프 함수를 본다
# · 찾을 이름:
# layoutSubviews / NSISEngine::... → Layout 병목
# -[CALayer display] / CGContext... → Display 병목
# CA::Transaction::commit → Commit 병목
# objc_msgSend 가 상위 → 호출 횟수 자체가 과다
# swift_retain / swift_release 가 상위 → ARC 과다 (04번)
# ── ② Instruments → Animation Hitches ──
# hitch 시간과 원인 단계를 직접 보여준다. 가장 빠른 진단.
# ── ③ Core Animation 디버그 옵션 ──
# Color Blended Layers → 빨간 영역 = 블렌딩 (isOpaque 확인)
# Color Offscreen-Rendered → 노란 영역 = 오프스크린 렌더
# Color Misaligned Images → 보라 = 픽셀 정렬 안 됨 (소수점 frame)
# ── ④ os_signpost 로 내 코드 구간 직접 측정 ──import OSLog
let signposter = OSSignposter(subsystem: "com.app.feed", category: "cell")
func tableView(_ tv: UITableView, cellForRowAt ip: IndexPath) -> UITableViewCell {
let state = signposter.beginInterval("configureCell", id: signposter.makeSignpostID())
defer { signposter.endInterval("configureCell", state) }
let cell = tv.dequeueReusableCell(withIdentifier: "cell", for: ip) as! FeedCell
cell.configure(items[ip.row], loader: pipeline)
return cell
}
// Instruments의 os_signpost 트랙에 구간별 시간이 그려진다.
// "cellForRowAt이 평균 0.8ms → 0.2ms로 줄었습니다" 같은 말을 할 수 있게 된다.import MetricKit
final class MetricsSubscriber: NSObject, MXMetricManagerSubscriber {
override init() {
super.init()
MXMetricManager.shared.add(self)
}
func didReceive(_ payloads: [MXMetricPayload]) {
for p in payloads {
// 스크롤 끊김의 사용자 지표
if let hitch = p.animationMetrics?.scrollHitchTimeRatio {
print("scroll hitch ratio: \(hitch)")
}
}
}
}
// 시뮬레이터·개발 기기 성능은 실사용자와 다르다.
// "구형 기기에서만 끊긴다"를 잡으려면 필드 데이터가 필요하다.L4CS 근본 — 풀링과 데드라인
① 셀 재사용은 "객체 풀링" 패턴이다
// 항목이 10,000개인 리스트를 생각해 보자
// ❌ 순진한 구현: 항목마다 뷰 하나
// 메모리: 10,000 × (뷰 객체 + 백킹스토어) → 수백 MB → Jetsam (09번)
// Commit: 레이어 10,000개를 매 프레임 인코딩 → 예산 초과
// 할당: 스크롤 중 계속 malloc/free → 단편화 + ARC 부하
// ✅ 재사용 풀: 화면에 보이는 수 + α 만 유지
// 메모리: ~15개 × 뷰 → 수 MB
// Commit: 레이어 ~15개
// 할당: 초기 15회 후 0회 — 스크롤 중 할당이 아예 없다
//
// ★ 자원 소비가 "데이터 개수 N"이 아니라 "화면 크기"에 비례한다.
// O(N) → O(1). 이게 재사용의 본질이다.이건 객체 풀링(object pooling)이라는 고전 패턴입니다. 게임 엔진의 총알·파티클, 데이터베이스 커넥션 풀, 스레드 풀이 전부 같은 아이디어예요 — "생성·파괴가 비싸면, 만들어 두고 상태만 초기화해서 다시 쓴다."
prepareForReuse가 "객체를 파괴하는 대신 상태를 초기화하는" 지점인 것도 이 패턴의 표준 구조입니다.
그리고 풀링의 고전적 위험도 그대로 따라옵니다 — 초기화를 빠뜨리면 이전 사용자의 상태가 새 사용자에게 새어 나갑니다. "이전 셀의 이미지가 잠깐 보이는" 현상이 정확히 그것입니다.
② 프레임 예산은 소프트 실시간 데드라인이다
경성 실시간 (hard real-time)
데드라인을 놓치면 → 시스템 실패 (에어백, 항공 제어)
연성 실시간 (soft real-time) ← UI 렌더링이 여기
데드라인을 놓치면 → 결과의 "가치"가 떨어진다 (끊김, 나쁜 경험)
일반 배치 처리
데드라인 없음 → 늦어도 결과는 유효하다
// ★ 핵심 차이:
// 배치 작업은 17ms 걸리면 "17ms 늦은 정답"이다.
// 프레임은 17ms 걸리면 "그 프레임을 놓친 것"이다 —
// 늦은 정답이 아니라 아예 표시되지 않는다.
//
// 그리고 다음 VSync까지 기다려야 하므로
// 1ms 초과가 16.7ms 지연으로 증폭된다. 벼랑 효과(cliff)다.③ "미리 하기"는 지연 시간을 처리량과 맞바꾸는 것이다
셀 재사용 = 객체 풀링 — 할당 비용을 O(N)→O(1)
높이 캐싱 = 메모이제이션 — 계산을 재사용
prefetching = 예측적 선반입 — 유휴 시간을 미리 소비
백그라운드 디코딩 = 작업 오프로딩 — 임계 경로에서 빼내기
다운샘플링 = 해상도 축소 — 필요한 만큼만 계산
NSCache = 캐시 + 방출 정책 — 메모리 압박 시 자동 해제
Diffable snapshot = 증분 갱신 — 전체 재계산 회피 (12번과 같은 아이디어)
지연 디코딩 = 지연 평가 — 필요할 때까지 미루기 (04번 CoW와 같은 계열)
// ★ 관통하는 원리는 둘뿐이다:
// ① 임계 경로(critical path)에서 일을 빼낸다 — 미리 / 나중에 / 다른 스레드로
// ② 같은 일을 두 번 하지 않는다 — 캐시 · 재사용 · 증분
미리 받아 두는 것은 공짜가 아닙니다. 쓰지 않을 데이터를 받으면 대역폭·배터리·메모리를 낭비합니다.
그래서 cancelPrefetchingForRowsAt이 API에 있는 겁니다 — 예측이 빗나갔을 때 되돌리라고요.
이건 CPU의 투기적 실행(speculative execution)과 같은 구조입니다 — 예측하고, 빗나가면 버린다.
05번의 분기 예측과 정확히 같은 아이디어가 애플리케이션 층에서 반복되는 셈입니다.
CS 정본으로 더 내려가기
이 페이지의 L4는 면접 답변에 필요한 깊이까지만 팝니다. 같은 주제를 CS 원리 → iOS 매핑 → 재현·측정 순서로 끝까지 파는 것은 허브의 iOS 개발자 CS 로드맵이고, 대응 챕터는 아래입니다.
경험으로 말하기
❌ "셀을 재사용하고 이미지를 캐싱해서 스크롤을 개선했습니다."
✅ "피드 스크롤이 구형 기기에서 끊겼습니다. Animation Hitches로 보니 hitch가 대부분 Display 단계에 몰려 있었고,
Time Profiler에서 -[CALayer display] 아래 이미지 디코딩이 상위였습니다.
UIImage(data:)가 지연 디코딩이라 첫 표시 때 메인 스레드에서 압축을 푸는 게 원인이었습니다.
CGImageSourceCreateThumbnailAtIndex로 셀 크기만큼만 백그라운드에서 디코딩하도록 바꿨고, os_signpost로 재보니
cellForRowAt 평균이 0.8ms에서 0.2ms로 떨어졌습니다. 부수 효과로 메모리 피크도 크게 줄었습니다 — 원본 비트맵을 안 들고 있게 됐으니까요.
반면 셀 계층 최적화는 안 했습니다. Layout 단계는 애초에 예산의 10% 정도라 병목이 아니었고, 계층을 건드리면 회귀 위험만 컸습니다."
이 답의 강점: 도구 이름, 실제 병목 단계, 원인의 메커니즘, 숫자, 그리고 안 한 것과 그 이유가 전부 있습니다.
꼬리 질문 대비
L2 셀 재사용만 하면 충분한가요?
재사용은 최적화가 아니라 기본값입니다. 그것만으로 부족한 이유가 명확합니다.
재사용이 해결하는 것은 "뷰 객체를 몇 개 만들 것인가"입니다. 즉 메모리와 Commit 단계의 레이어 수를 잡아 주죠.
하지만 해결하지 못하는 것이 더 많습니다.
- 재사용될 때마다
configure가 불립니다 — 그 안에서 DateFormatter를 만들면, 재사용을 해도 스크롤할 때마다 비싼 일을 반복합니다. - Layout 단계 — 셀프사이징은 셀마다 오토레이아웃을 풉니다. 재사용과 무관합니다.
- Display 단계 — 이미지 디코딩,
draw(_:), 오프스크린 렌더. 재사용과 무관합니다. - Commit 단계 — 셀 하나 안의 레이어가 20개면, 재사용해도 매 프레임 20개를 인코딩합니다.
그래서 답은 "재사용은 O(N)을 O(1)로 만드는 전제 조건이고, 그 O(1) 안에서 무엇을 하느냐가 실제 최적화"입니다.
L3 어느 단계가 병목인지 어떻게 아나요?
Time Profiler 스택에 이름이 그대로 나옵니다. 이걸 외워 두면 진단이 빨라집니다.
layoutSubviews / NSISEngine::optimize
→ Layout 병목
처방: 계층 얕게, 제약 수 줄이기, 높이 캐싱, 고정 높이
-[CALayer display] / CGContextDrawImage / CGBitmapContext
→ Display 병목
처방: 이미지 다운샘플링·백그라운드 디코딩, draw(_:) 제거,
오프스크린 렌더 회피
CA::Transaction::commit
→ Commit 병목
처방: 레이어 개수 줄이기, 백킹스토어 크기 줄이기
swift_retain / swift_release 가 상위
→ ARC 과다 (04번) — 값 타입 검토
objc_msgSend 가 상위
→ 호출 횟수 자체가 과다 — 알고리즘이나 호출 구조를 봐야 한다더 빠른 방법은 Animation Hitches 템플릿입니다. hitch가 발생한 프레임과 어느 단계에서 초과했는지를 바로 보여줍니다.
그리고 Core Animation 디버그 옵션은 눈으로 즉시 확인할 수 있어서 초기 스크리닝에 좋습니다 — Color Blended Layers(빨강 = 블렌딩), Color Offscreen-Rendered(노랑 = 오프스크린).
L4 왜 평균 프레임 시간이 아니라 최악값을 봐야 하나요?
프레임 예산이 데드라인이기 때문입니다. 그리고 데드라인 시스템에서는 평균이 거의 쓸모없는 지표입니다.
① 벼랑 효과. 16.7ms를 1ms 초과하면 1ms 늦는 게 아니라 한 프레임을 통째로 놓칩니다. 다음 VSync까지 기다려야 하니까요. 즉 비용이 연속이 아니라 계단식입니다.
② 평균은 초과를 숨깁니다. 평균 10ms인데 100프레임 중 5프레임이 25ms라면, 평균은 훌륭하지만 사용자는 1초에 세 번 끊김을 봅니다. 사람은 부드러움이 아니라 끊김을 인지합니다.
③ 그래서 지표가 따로 있습니다. Apple의 hitch time ratio는 "총 스크롤 시간 중 프레임이 늦어진 시간의 비율"입니다 — 평균이 아니라 초과의 누적을 봅니다. MetricKit의 scrollHitchTimeRatio가 그것입니다.
이건 성능 공학의 일반 원칙이기도 합니다. 서버에서도 평균 응답 시간 대신 p95·p99를 보죠 — 사용자 경험은 최악값이 지배하기 때문입니다. 프레임 렌더링은 그 원칙이 가장 극단적으로 나타나는 경우입니다. 초당 60번의 데드라인이 있으니, 1%만 놓쳐도 1초에 한 번 가까이 끊깁니다.
쉽게 이해하기
스크롤을 초당 60번 사진을 찍어 넘겨 보여주는 것이라고 생각해 봅시다. 사진 한 장을 만들 시간은 16.7밀리초뿐입니다.
여기서 가장 중요한 사실이 있습니다. 17밀리초가 걸리면 "0.3밀리초 늦는" 게 아닙니다. 그 사진은 아예 못 나가고, 앞 사진을 한 번 더 보여줍니다. 그리고 다음 기회는 16.7밀리초 뒤죠. 0.3 늦으면 16.7 늦는 겁니다. 이래서 "평균은 괜찮은데 가끔 끊긴다"가 생깁니다.
그럼 셀 재사용은 뭘까요? 뷔페에서 접시를 손님 수만큼 준비하지 않는 것과 같습니다. 테이블에 앉을 수 있는 만큼만 두고, 다 먹으면 씻어서 다시 씁니다. 손님이 만 명이든 십만 명이든 접시는 스무 개면 됩니다.
그런데 여기서 함정이 있어요. 접시를 씻을 때 앞사람 음식이 남아 있으면 다음 손님이 그걸 봅니다. 그래서 prepareForReuse에서 깨끗이 지우는 겁니다. "이전 셀 이미지가 잠깐 보이는" 현상이 바로 덜 씻긴 접시예요.
그리고 진짜 문제는 접시 개수가 아니라 요리 시간인 경우가 많습니다. 접시를 스무 개로 줄여도, 주문마다 재료를 처음부터 손질하면 줄이 밀립니다. 그래서 미리 손질해 두거나(prefetch·캐시), 주방 보조에게 시키거나(백그라운드 디코딩), 애초에 손질할 게 적게(다운샘플링) 만듭니다.
마지막으로 미리 준비하는 것도 공짜가 아닙니다. 손님이 안 오면 준비한 재료를 버려야 하죠. 그래서 cancelPrefetching이 있는 겁니다 — "예측했는데 빗나갔으니 취소해". 재미있게도 CPU도 똑같은 일을 합니다(분기 예측). 미리 해 놓고, 틀리면 버립니다. 같은 아이디어가 층만 바꿔서 반복되는 거예요.
셀 최적화는 기법 목록이 아니라 데드라인 관리다. 임계 경로에서 일을 빼내고, 같은 일을 두 번 하지 않는다. 그리고 어느 단계가 병목인지 먼저 재고, 평균이 아니라 최악값으로 판정한다.
설계 근거 · 1차 자료
이 페이지의 "왜 그렇게 설계했나" 주장은 아래에서 나왔습니다. 면접에서 근거를 물으면 이 이름들을 대면 됩니다.
- Apple — 재사용 큐를 API로 노출한 계약
- WWDC20 — hitch를 "평균"이 아니라 "초과"로 재야 하는 이유
- Apple — 실사용자 scroll hitch ratio 지표의 정의