🟣 화면을 이루는 흔한 부품들
지금까지 뷰를 붙이고, 화면을 넘기고, 배치를 잡는 뼈대를 봤다. 이제 그 위에 올릴 실제 부품을 다룬다. iOS 화면 대부분은 몇 가지 표준 컴포넌트의 조합이다. 글자를 보여 주는 UILabel, 탭을 받는 UIButton, 입력을 받는 UITextField, 그림을 그리는 UIImageView, 그리고 목록을 그리는 UITableView다.
앞 장들에서 강조했듯 이들은 전부 UIView의 후손이라, 붙이고 제약을 거는 방식은 똑같다. 다른 점은 각자 고유한 설정 API를 갖는다는 것뿐이다.
let label = UILabel()
label.text = "제목"
label.numberOfLines = 0 // 0이면 줄 수 제한 없이 자동 줄바꿈
let field = UITextField()
field.placeholder = "이메일"
field.keyboardType = .emailAddress
field.borderStyle = .roundedRect
let imageView = UIImageView(image: UIImage(named: "cover"))
imageView.contentMode = .scaleAspectFill // 비율 유지하며 영역 채움
imageView.clipsToBounds = true // 넘치는 부분 잘라냄여기서 하나 짚을 게 UIImageView의 contentMode다. 이미지의 비율과 뷰의 비율이 다를 때 어떻게 맞출지를 정하는데, scaleToFill은 비율을 무시하고 늘려 찌그러지고, scaleAspectFit은 비율을 지키며 여백을 남기고, scaleAspectFill은 비율을 지키며 영역을 꽉 채우되 넘치는 부분을 잘라낸다. 이미지가 찌그러져 보이는 문제의 대부분은 이 설정 하나로 갈린다.
🟣 MVC: 뷰컨트롤러가 비대해지는 걸 막는 뼈대
컴포넌트가 늘어나면 뷰컨트롤러 코드가 순식간에 불어난다. 데이터를 불러오고, 화면을 그리고, 입력을 처리하는 코드가 한 파일에 뒤엉킨다. 이걸 나누는 가장 기본적인 틀이 MVC(Model, View, Controller)다.
모델은 데이터와 그 규칙이다. 사용자 정보, 상품 목록 같은 순수한 데이터 구조가 여기 속한다. 뷰는 화면에 보이는 것이고, 컨트롤러는 그 사이에서 데이터를 받아 뷰에 채우고 사용자 입력을 받아 모델을 고치는 중재자다. iOS에서는 이 컨트롤러 역할을 뷰컨트롤러가 맡는다.
MVC의 핵심 규율은 모델과 뷰가 서로를 직접 모른다는 것이다. 데이터가 화면을 직접 건드리지 않고, 화면이 데이터를 직접 고치지 않는다. 모든 왕래는 뷰컨트롤러를 거친다. 이 규율을 지키면 데이터 로직만 따로 테스트하거나, 같은 데이터를 다른 화면으로 재사용하기 쉬워진다.
실무에서는 뷰컨트롤러가 결국 비대해지는 문제 때문에 MVVM 같은 다른 패턴으로 넘어가지만, 역할을 데이터·화면·중재로 나눈다는 사고의 출발점은 MVC다. 이 구분이 몸에 붙어야 다른 패턴도 왜 그렇게 나누는지가 이해된다.
🟣 UITableView: 목록을 그리는 방식
목록은 거의 모든 앱에 나온다. UIKit에서 목록을 그리는 대표 컴포넌트가 UITableView다. 그런데 테이블뷰는 앞의 컴포넌트들과 다르게 동작한다. 레이블은 text만 넣으면 끝이지만, 테이블뷰는 "몇 줄인지", "각 줄에 뭘 그릴지"를 스스로 모른다. 그 답을 밖에서 받아 온다.
이 "밖에서 받아 오는" 방식이 dataSource와 delegate 패턴이다. 테이블뷰는 자기가 그려야 할 때 dataSource에게 "몇 줄이야?", "이 줄엔 뭘 그려?"라고 묻는다. 사용자가 줄을 탭하면 delegate에게 "이 줄이 눌렸어"라고 알린다. 보통 이 두 역할을 뷰컨트롤러가 맡는다.
final class ItemListViewController: UIViewController, UITableViewDataSource {
private let tableView = UITableView()
private var items: [String] = ["사과", "바나나", "포도"]
override func viewDidLoad() {
super.viewDidLoad()
tableView.dataSource = self
tableView.register(UITableViewCell.self, forCellReuseIdentifier: "cell")
// tableView를 view에 붙이고 제약 거는 코드는 생략
}
func tableView(_ tableView: UITableView,
numberOfRowsInSection section: Int) -> Int {
items.count // 몇 줄인지
}
func tableView(_ tableView: UITableView,
cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(
withIdentifier: "cell", for: indexPath)
cell.textLabel?.text = items[indexPath.row] // 이 줄에 뭘 그릴지
return cell
}
}🚀 셀 재사용: 화면 밖 셀을 다시 쓴다
테이블뷰에서 반드시 이해해야 할 것이 셀 재사용이다. 수천 줄짜리 목록이라고 셀 객체를 수천 개 만들면 메모리가 감당하지 못한다. 그래서 테이블뷰는 화면에 보이는 만큼만 셀을 만들고, 스크롤로 벗어난 셀을 재사용 큐에 넣어 두었다가 새로 들어올 줄에 다시 쓴다.
코드에서 dequeueReusableCell이 바로 이 큐에서 셀을 꺼내 오는 호출이다. 재사용이 일으키는 흔한 함정이 하나 있다. 재사용된 셀에는 이전에 쓰던 내용이 그대로 남아 있다. 그래서 cellForRowAt에서 셀의 모든 표시 내용을 매번 새로 채워 넣어야 한다. 이걸 빠뜨리면 스크롤할 때 엉뚱한 이미지가 다른 줄에 나타나는, 입문기의 대표적인 버그가 생긴다.
🟣 정리
- 화면은
UILabel,UIButton,UITextField,UIImageView,UITableView같은 표준 컴포넌트의 조합이다. 모두UIView의 후손이라 붙이고 배치하는 방식은 같고 설정 API만 다르다. - 이미지가 찌그러지거나 넘치는 문제는
contentMode와clipsToBounds로 조정한다. - MVC는 역할을 모델(데이터), 뷰(화면), 컨트롤러(중재)로 나눈다. 모델과 뷰는 서로를 모르고 모든 왕래는 뷰컨트롤러를 거친다.
- 테이블뷰는 몇 줄을 어떻게 그릴지를 dataSource에게, 탭 처리를 delegate에게 위임받는다. 보통 뷰컨트롤러가 두 역할을 맡는다.
- 셀은 재사용된다.
dequeueReusableCell로 꺼낸 셀에는 이전 내용이 남아 있으므로cellForRowAt에서 매번 전부 다시 채운다.
실무에서 테이블뷰의 셀 재사용은 목록 성능의 기본이자 "스크롤하면 데이터가 섞인다"는 버그의 원인이다. 면접에서 "셀 재사용이 왜 필요하고 무엇을 조심해야 하는가"는 UITableView를 실제로 써 봤는지 가르는 질문이다. dataSource와 delegate로 역할을 위임하는 구조는 이후 만나는 대부분의 UIKit 컴포넌트에 똑같이 나타난다.
이제 화면에 목록을 그릴 수 있다. 남은 것은 두 가지다. 버튼 탭 같은 사용자 입력을 어떻게 코드에 연결하는지, 그리고 그 목록을 하드코딩이 아니라 서버에서 받은 데이터로 채우는 방법이다. 마지막 장에서 타깃액션과 JSON 디코딩으로 이 둘을 잇는다.