위와 같이 로그인, 회원가입, 영화 검색, 영화 상세내용, 예매 등의 화면을 구현해야 하고, 사진엔 없지만 마이페이지나 영화 목록 화면도 필요하다.
Model과 View, Controller를 나눠 MVC 아키텍처로 작성하기로 하였고, 각자 맡은 화면에 대한 View와 Controller를 구현하고, 몇몇 팀원이 Model에 필요한 데이터 구조를 짜기로 하였다.
내가 맡은 부분은 검색을 위한 화면으로, 텍스트필드와 컬렉션뷰로 영화 이미지와 이름을 띄우게 해야한다.
다른 팀원이 API로 서버에서 영화 데이터를 받아오도록 만들면,
를 하면 될 것이다.
그런데 컬렉션 뷰에 띄울 데이터들은 View보다는 Controller에서 관리하는 게 맞다고 생각했고,
View만 먼저 구현 중이니 어떻게 해야할지 고민하다가 일단 View에 더미 데이터를 만들고 CollectionView를 설정한 뒤, 나중에 Controller를 구현할 때 옮기기로 하였다.
import UIKit
import SnapKit
class SearchViewCell: UICollectionViewCell {
// 영화포스터 이미지 뷰
private let movieImageView: UIImageView = {
let imageView = UIImageView()
imageView.contentMode = .scaleAspectFit
imageView.clipsToBounds = true
return imageView
}()
// 영화 이름 레이블
private let movieNameLabel: UILabel = {
let label = UILabel()
label.text = "영화 이름"
label.font = .systemFont(ofSize: 16)
label.textColor = .label
label.backgroundColor = .systemBackground
return label
}()
// MARK: - 생성자
override init(frame: CGRect) {
super.init(frame: frame)
setupUI()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
// MARK: - UI 셋업
private func setupUI() {
[
movieImageView,
movieNameLabel
].forEach { contentView.addSubview($0) }
// 영화 이미지 레이아웃
movieImageView.snp.makeConstraints{
$0.top.equalTo(contentView.snp.top)
$0.leading.trailing.equalTo(contentView)
$0.width.equalTo(112)
$0.height.equalTo(163)
}
// 영화 이름 레이아웃
movieNameLabel.snp.makeConstraints{
$0.top.equalTo(movieImageView.snp.bottom).offset(12)
$0.leading.equalTo(contentView)
$0.trailing.equalTo(contentView).offset(-46)
$0.bottom.equalTo(contentView.snp.bottom).offset(-1)
}
}
// MARK: - 이미지뷰, 레이블 값 변경 메서드
func setCellData(movieImage: Data?, movieName: String) {
if let data = movieImage, let image = UIImage(data: data) {
movieImageView.image = image
} else {
movieImageView.backgroundColor = .systemGray4
}
movieNameLabel.text = movieName.isEmpty ? "영화 제목 없음" : movieName
}
}
import UIKit
import SnapKit
class SearchView: UIView {
// 앱 로고 레이블
private let logoLabel: UILabel = {
let label = UILabel()
label.text = "LuckVii"
label.textAlignment = .center
label.textColor = .label
label.backgroundColor = .systemBackground
label.font = .boldSystemFont(ofSize: 20)
return label
}()
// 검색을 위한 텍스트 필드
let searchTextField: UITextField = {
let textField = UITextField()
textField.backgroundColor = .systemBackground
textField.textColor = .label
textField.placeholder = "영화명을 입력해주세요."
textField.addTarget(self, action: #selector(searchTextChanged), for: .editingChanged)
textField.clearButtonMode = .whileEditing // 텍스트 입력 중에만 x 버튼
textField.layer.borderWidth = 1
textField.layer.borderColor = UIColor.systemGray.cgColor
textField.layer.cornerRadius = 22
let paddingView = UIView(frame: CGRect(x: 0, y: 0, width: 16, height: textField.frame.height))
textField.leftView = paddingView // 텍스트와 경계선 사이에 여백 추가
textField.leftViewMode = .always
return textField
}()
// 영화 목록을 띄우는 컬렉션 뷰
let movieCollectionView: UICollectionView = {
let layout = UICollectionViewFlowLayout()
layout.scrollDirection = .vertical // 세로 스크롤
let view = UICollectionView(frame: .zero, collectionViewLayout: layout)
view.showsVerticalScrollIndicator = false // 스크롤바 숨김
view.backgroundColor = .systemBackground
view.register(SearchViewCell.self, forCellWithReuseIdentifier: "SearchViewCell") // 셀 등록
return view
}()
// MARK: - 생성자
override init(frame: CGRect) {
super.init(frame: frame)
setupUI()
// 컬렉션 뷰 delegate, dataSource 설정
movieCollectionView.dataSource = self
movieCollectionView.delegate = self
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
// MARK: - UI 셋업
private func setupUI() {
[
logoLabel,
searchTextField,
movieCollectionView
].forEach { addSubview($0) }
logoLabel.snp.makeConstraints{
$0.top.equalToSuperview().offset(78)
$0.leading.trailing.equalToSuperview().inset(130)
}
searchTextField.snp.makeConstraints {
$0.top.equalTo(logoLabel.snp.bottom).offset(22)
$0.leading.trailing.equalToSuperview().inset(16)
$0.height.equalTo(44)
}
movieCollectionView.snp.makeConstraints{
$0.top.equalTo(searchTextField.snp.bottom).offset(16)
$0.leading.trailing.equalToSuperview().inset(16)
$0.bottom.equalToSuperview().offset(-36)
}
}
// MARK: - 임시 데이터와 메서드 -> 아래 내용들은 컨트롤러 만들면 수정 및 이동 할 예정입니다
// 텍스트필드 검색 입력 처리 메서드
@objc private func searchTextChanged(_ textField: UITextField) {
print("검색 텍스트: \(textField.text ?? "") ")
}
// 키보드 처리 메서드
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
endEditing(true) // 다른 곳 터치시 키보드 닫기
}
// 더미 데이터
private let dummyMovies: [(image: UIImage?, name: String)] = [
(UIImage(systemName: "film"), "영화 1"),
(UIImage(systemName: "film"), "영화 2"),
(UIImage(systemName: "film"), "영화 3"),
(UIImage(systemName: "film"), "영화 4"),
(UIImage(systemName: "film"), "영화 5"),
(UIImage(systemName: "film"), "영화 6"),
(UIImage(systemName: "film"), "영화 7"),
(UIImage(systemName: "film"), "영화 8"),
(UIImage(systemName: "film"), "영화 9"),
(UIImage(systemName: "film"), "영화 10")
]
}
extension SearchView: UICollectionViewDataSource, UICollectionViewDelegateFlowLayout {
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return 10
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "SearchViewCell", for: indexPath) as! SearchViewCell
cell.setCellData(movieImage: nil, movieName: "영화 \(indexPath.row + 1)")
cell.backgroundColor = .systemBackground
return cell
}
// 셀 크기 설정
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
let width = 112
let height = 198
return CGSize(width: width, height: height)
}
}
위와 같이 작성한 후 PR 하는데, 코드의 의도와 내용에 대한 설명을 코드 내에만 간략히 작성해서 팀원들이 약간 혼란을 겪었다.
컬렉션 뷰로 구현하려니 구현된 걸 확인하려면 컬렉션 뷰에 띄울 데이터가 필요하고, 컬렉션 뷰에 대한 delegate나 dataSource 설정 코드도 필요하다.
이것들은 View보다는 Controller에 띄우는 것이 맞지만 Controller가 아닌 View만 먼저 작성하고 있어서, 일단 View에 구현해 놓았었다.
이 부분이 팀원들이 보기엔 View와 Controller를 구분짓지 않고 그냥 작성한 코드처럼 보인 것이었다.
또, 개행에 대한 내용도 나중에 Controller 구현하면 옮길 코드들은 일부러 조금 더 띄워서 옮길 때 편하도록 작성 하였는데,
개행에 대한 코드 컨벤션이 일치하지 않게 되어 가독성이 떨어지고 약간의 불쾌감을 주게 되었다.
마지막에 나중에 옮기려고 주석에다 MARK: -
로 옮길 코드
라고 작성 해놓은 부분들이었다.
코드의 주석 부분에 설명을 써놓긴 했는데, 코드의 의도와 내용을 주석뿐만 아니라 PR을 작성하며 설명에 충분히 작성하고, 말로도 설명했어야 했다고 느꼈다.
이정도면 알아보겠지 했던 것이 다른 팀원에게 혼란을 주게 되었다.
다른 팀원들도 자신이 구현해야 하는 기능을 코딩하는 와중에 여러 팀원들의 코드 리뷰도 해야하니 적당히 알아보겠지 하는 식으로 해놓아선 안되는 것이었다.
이 점들에 대해 유의하며 Controller 구현을 시작하였다.
import UIKit
import SnapKit
class SearchView: UIView {
// 앱 로고 레이블
private let logoLabel: UILabel = {
let label = UILabel()
label.text = "LuckVii"
label.textAlignment = .center
label.textColor = .label
label.backgroundColor = .systemBackground
label.font = .boldSystemFont(ofSize: 20)
return label
}()
// 검색을 위한 텍스트 필드
let searchTextField: UITextField = {
let textField = UITextField()
textField.backgroundColor = .systemBackground
textField.textColor = .label
textField.placeholder = "영화명을 입력해주세요."
textField.clearButtonMode = .whileEditing // 텍스트 입력 중에만 x 버튼
textField.layer.borderWidth = 1
textField.layer.borderColor = UIColor.systemGray.cgColor
textField.layer.cornerRadius = 22
let paddingView = UIView(frame: CGRect(x: 0, y: 0, width: 16, height: textField.frame.height))
textField.leftView = paddingView // 텍스트와 경계선 사이에 여백 추가
textField.leftViewMode = .always
return textField
}()
// 영화 목록을 띄우는 컬렉션 뷰
let movieCollectionView: UICollectionView = {
let layout = UICollectionViewFlowLayout()
layout.scrollDirection = .vertical // 세로 스크롤
let view = UICollectionView(frame: .zero, collectionViewLayout: layout)
view.showsVerticalScrollIndicator = false // 스크롤바 숨김
view.backgroundColor = .systemBackground
view.register(SearchViewCell.self, forCellWithReuseIdentifier: "SearchViewCell") // 셀 등록
return view
}()
// MARK: - 생성자
override init(frame: CGRect) {
super.init(frame: frame)
setupUI()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
// MARK: - UI 셋업
private func setupUI() {
[
logoLabel,
searchTextField,
movieCollectionView
].forEach { addSubview($0) }
logoLabel.snp.makeConstraints{
$0.top.equalToSuperview().offset(78)
$0.leading.trailing.equalToSuperview().inset(130)
}
searchTextField.snp.makeConstraints {
$0.top.equalTo(logoLabel.snp.bottom).offset(22)
$0.leading.trailing.equalToSuperview().inset(16)
$0.height.equalTo(44)
}
movieCollectionView.snp.makeConstraints{
$0.top.equalTo(searchTextField.snp.bottom).offset(16)
$0.leading.trailing.equalToSuperview().inset(16)
$0.bottom.equalToSuperview().offset(-36)
}
}
// 키보드 처리 메서드
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
endEditing(true) // 다른 곳 터치시 키보드 닫기
}
}
위에서 View만 먼저 작성할 때 썼던 코드 중 컬렉션 뷰 설정에 대한 내용들과 더미데이터, 텍스트뷰 액션에 대한 내용을 Controller로 옮긴 것이다.
import UIKit
import SnapKit
class SearchViewController: UIViewController {
// 더미 데이터
private let dummyMovies: [(image: UIImage?, name: String)] = [
(UIImage(systemName: "nil"), "대가족"),
(UIImage(systemName: "nil"), "소방관"),
(UIImage(systemName: "nil"), "모아나 2"),
(UIImage(systemName: "nil"), "위키드"),
(UIImage(systemName: "nil"), "1승"),
(UIImage(systemName: "nil"), "히든페이스"),
(UIImage(systemName: "nil"), "서브스턴스"),
(UIImage(systemName: "nil"), "극장판 주술회전 0"),
(UIImage(systemName: "nil"), "더 크로우"),
(UIImage(systemName: "nil"), "크리스마스에 기적을 만날 확률")
]
// 더미 데이터에서 검색된 값 저장할 배열
private var searchMovies = [(image: UIImage?, name: String)]()
// 기본 뷰를 searchView로 설정
let searchView = SearchView()
override func loadView() {
self.view = searchView
}
override func viewDidLoad() {
super.viewDidLoad()
// 초기 상태에서는 전체 데이터를 표시
searchMovies = dummyMovies
// delegate, dataSource를 self로 설정
searchView.movieCollectionView.delegate = self
searchView.movieCollectionView.dataSource = self
// searchView의 텍스트필드 설정
searchView.searchTextField.delegate = self // textFieldShouldReturn 메서드 호출을 위한 delegate 설정
searchView.searchTextField.addTarget(self, action: #selector(searchTextChanged), for: .editingChanged)
}
}
// MARK: - 컬렉션 뷰 delegate, dataSource 설정
extension SearchViewController: UICollectionViewDataSource, UICollectionViewDelegateFlowLayout {
// 섹션당 셀 개수 설정
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return searchMovies.count
}
// 셀 속성 설정
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "SearchViewCell", for: indexPath) as! SearchViewCell
let movieData = searchMovies[indexPath.row]
cell.setCellData(movieImage: movieData.image?.pngData(), movieName: movieData.name)
cell.backgroundColor = .systemBackground
return cell
}
// 셀 크기 설정
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
let width = 112
let height = 198
return CGSize(width: width, height: height)
}
// 셀 선택 처리 메서드
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
searchView.endEditing(true) // 키보드 내리기
/* 영화 세부 페이지로 이동 처리 구현해야함 */
}
}
// MARK: - 텍스트 필드 메서드
extension SearchViewController: UITextFieldDelegate {
// 키보드 입력에 따른 컬렉션 뷰 출력 값 바꾸는 메서드
@objc func searchTextChanged(_ textField: UITextField) {
let input = textField.text ?? ""
print(input)
if input.isEmpty {
searchMovies = dummyMovies // 입력이 없으면 전체 데이터 출력
} else {
searchMovies = dummyMovies.filter { $0.name.contains(input) // 제목에 입력값이 포함된 영화 필터링
}
}
// 컬렉션 뷰 리로드
searchView.movieCollectionView.reloadData()
}
// 키보드의 return 버튼 클릭시 키보드 내리는 메서드
func textFieldShouldReturn(_ textField: UITextField) -> Bool {
textField.resignFirstResponder()
return true
}
}
컬렉션 뷰의 셀을 클릭했을때, 해당 영화에 대한 세부 페이지로 전환되어야 하지만, 영화의 세부 내용에 대한 페이지는 다른 팀원이 제작중이어서, 일단 비워두었다.
그리고 서버에서 데이터를 받아오는 것도 아직 다른 팀원이 구현중이어서, 더미데이터로 계속 테스트 하였다.
제목에 입력한 문자가 포함될 때 바로 해당 영화들이 나오도록 하였다.
그리고 입력을 다 했어도 키보드가 사라지지 않아 불편하기에 retrun 버튼을 누르거나 텍스트뷰를 제외한 화면 내 다른 영역을 터치하면 키보드가 내려가도록 하였다.
PR 내용도 조금 더 자세히 썼다.. 더 길게 쓰려다 코드만큼이나 길어질까봐 줄였는데, 다른 사람이 보기에 이해하기 편한지는 좀 더 고민해봐야 할 것 같다.
팀프로젝트 할 때마다 새로운 소통 문제를 겪는 것 같다..
물론 큰일은 아니었지만, 별 문제 없다고 생각했던 방식이 나 혼자 볼때나 이해할 법한 일처리 방식이었구나 싶어서 부끄러웠다.
민망한 것은 그게 그나마도 잘 설명하면서 작성한 코드와 PR 이라고 생각했던 것이다.
코드 컨벤션과 커밋 컨벤션 그리고 브랜치 룰 등등.. 각종 규칙들이 왜 그렇게나 많은 것인지 이해가 되는 하루였다..
카라멜 팝콘주문이요 !~ 🍿