[TIL] 해리포터 시리즈 앱 트러블슈팅

박주하·2025년 6월 23일

1. 기존 UI가 계속 쌓이는 문제


🔍 문제

// MainContentView.swift
func configure(book: Book, seriesNumber: Int) {
	let stacks: [UIStackView] = [
		makeImageAndInfoStack(book: book, seriesNumber: seriesNumber),
		makeSummaryStack(title: "Dedication", value: book.dedication),
		makeSummaryStack(title: "Summary", value: book.summary, seriesNumber: seriesNumber),
		makeChapterStack(title: "Chapter", value: book.chapters)
	]
	stacks.forEach { totalStack.addArrangedSubview($0) }
}
  • configure() 호출 시 UIStackView에 기존 서브뷰가 남은 상태로 추가됨

💡 해결

// MainContentView.swift
func configure(book: Book, seriesNumber: Int) {
	totalStack.arrangedSubviews.forEach { $0.removeFromSuperview() }
	//...
}
  • 재구성 전 기존 arrangedSubviews를 제거하여 해결

2. Summary 450자 이하에도 "…" 붙는 문제


🔍 문제

// MainContentView.swift
    func configureSummary(value: String, selectedSeries: Int) {
        let key = "isExpanded_\(selectedSeries)"
        let isExpanded = UserDefaults.standard.bool(forKey: key)
        
        summaryValueLabel.text = isExpanded ? value : "\(value.prefix(450))..."
        
        //...
    }
  • 문자 수와 관계없이 항상 접힌 형식이 적용됨
  • 450자 이하에서도 summaryValueLabel.text...이 붙음

💡 해결

    func configureSummary(value: String, selectedSeries: Int) {
        let key = "isExpanded_\(selectedSeries)"
        let isExpanded = UserDefaults.standard.bool(forKey: key)
        let isTruncate = value.count > 450
        
        summaryValueLabel.text = isTruncate && !isExpanded
        ? "\(value.prefix(450))..."
        : value
        
		//...
    }
  • 450자 이상인 경우에만 접힌 텍스트(...)가 적용되도록 조건 추가

3. JSON 디코딩과 날짜 처리


🔍 문제

// DataService.swift
	func loadBooks(completion: @escaping (Result<[Book], Error>) -> Void) {
        //...
        
        do {
            let data = try Data(contentsOf: URL(fileURLWithPath: path))
            let bookResponse = try JSONDecoder().decode(BookResponse.self, from: data)
            //...
        }
    }
    
// MainContentView.swift
    func configure(book: Book, selectedSeries: Int) {
        //...
        releasedValueLabel.text = formattedDate(book.releaseDate)
        //...
    }
    
    func formattedDate(_ dateValue: String) -> String {
        let formatter = DateFormatter()
        formatter.dateFormat = "yyyy-MM-dd"

        guard let date = formatter.date(from: dateValue) else { return dateValue }

        let displayFormatter = DateFormatter()
        displayFormatter.dateStyle = .long
        displayFormatter.locale = Locale(identifier: "en_US")

        return displayFormatter.string(from: date)
    }
  • JSON 파싱 시 날짜(release_date)를 String 타입으로 디코딩
  • formattedDate() 함수를 따로 만들어 DateFormatter를 사용해 포맷팅하고 있었음

💡 해결

// DataService.swift
	func loadBooks(completion: @escaping (Result<[Book], Error>) -> Void) {
        //...
        
        let decoder = JSONDecoder()
        decoder.dateDecodingStrategy = .formatted({
            let dateFormatter = DateFormatter()
            dateFormatter.dateFormat = "yyyy-MM-dd"
            return dateFormatter
        }())
        
        do {
            let data = try Data(contentsOf: URL(fileURLWithPath: path))
            let bookResponse = try decoder.decode(BookResponse.self, from: data)
            //...
        }
    }
    
// MainContentView.swift
    func configure(book: Book, selectedSeries: Int) {
        //...
        releasedValueLabel.text = "\(book.releaseDate, format: .long, locale: Locale(identifier: "en_US"))"
        //...
    }
    
extension String.StringInterpolation {
    mutating func appendInterpolation(_ date: Date, format: DateFormatter.Style = .long, locale: Locale = .current) {
        let formatter = DateFormatter()
        formatter.dateStyle = format
        formatter.locale = locale
        appendLiteral(formatter.string(from: date))
    }
}
  • JSONDecoderdateDecodingStrategy를 사용해 날짜 문자열을 파싱 단계에서 바로 Date로 변환함
  • 이후 UI에서는 DateFormatter가 아닌 String.StringInterpolationappendInterpolation(_:format:locale:) 확장을 활용함
  • Date → 포맷된 String 변환을 인터폴레이션 방식으로 간결하게 처리함
  • 가독성과 재사용성이 높아짐

4. 커스텀 에러에 사용자 메시지 적용


🔍 문제

// DataService.swift
enum DataError: Error {
    case fileNotFound
    case parsingFailed
}

// MainViewController.swift
	private func handleError(_ error: Error) {
        let alert = UIAlertController(title: "데이터 로딩 실패", message: error.localizedDescription, preferredStyle: .alert)
        alert.addAction(.init(title: "확인", style: .default))
        present(alert, animated: true)
    }
  • DataError가 단순히 Error만 채택하고 있음
  • error.localizedDescription"The operation couldn’t be completed." 같은 일반적인 메시지만 반환함
    → 사용자에게 적절한 안내 문구가 제공되지 않음

💡 해결

// DataService.swift
enum DataError: Error, LocalizedError {
    case fileNotFound
    case parsingFailed
    
    var errorDescription: String? {
        switch self {
        case .fileNotFound:
            return "데이터 파일을 찾을 수 없습니다."
        case .parsingFailed:
            return "데이터를 불러오는 데 실패했습니다."
        }
    }
}
  • DataErrorLocalizedError 프로토콜 채택
  • errorDescription을 직접 정의하여 에러별 사용자 친화적인 메시지를 제공함

5. MainView -> MainViewController 통합


🔍 문제

  • MainView에서는 MainHeaderViewMainContentView를 포함하는 구조
  • MainViewController에서는 MainView 관리
  • MainView에서 컨트롤러의 일들을 처리하는 구조로, MainViewController에서 하는 일이 별로 없는 상황

💡 해결

  • MainView 코드를 MainViewController로 통합하여 해결
  • MainViewController에서 헤더/컨텐츠 구성, 네트워크 호출과 에러 핸들링 처리

0개의 댓글