GithubHelp home page GithubHelp logo

jinios / swift-storeapp Goto Github PK

View Code? Open in Web Editor NEW

This project forked from code-squad/swift-storeapp

0.0 1.0 0.0 12.81 MB

쇼핑몰 앱 - 코드스쿼드 미션 (2018.07 - 2018.08)

Swift 99.24% Ruby 0.76%

swift-storeapp's Introduction

스토어 앱

완성화면

주요 기능

  • 네트워크 환경에서 주문하기 기능을 구현한 애플리케이션
  • Slack Web Hook으로 주문하기 기능 수행
    • 상품 주문 요청시 Slack에서 제공하는 Web Hook 기능과 스위프트 URLSession클래스를 이용하여 Slack 채널에 주문 알림 메시지가 전송되도록 구현
  • 네트워크 연결성 확인: StatusBar 변경으로 연결성 구분
    • wifi: 파란색 / WWAN: 하늘색 / 네트워크 유실: 빨간색

Step1 ~ Step7 완성화면

링크 - 단계별 완성화면

사용한 기술

  • Network 병렬처리, Caching, UITableView, UIScrollView, AutoLayout, Web Hook(slack) 등

사용한 라이브러리

  • Toaster : 상품 클릭시 화면하단에 Toast기능 실행(안드로이드 Toast메시지 스타일)
  • Alamofire : Network Reachability status 확인을 위해 사용

공부한 부분 & Troubleshooting

데드락(Deadlock)

  • 문제 코드
// TableViewCell.swift

  private func setItemImage(imageURL: String) {
    // 1. ImageSetter의 download()호출
    ImageSetter.download(with: imageURL, handler: { imageData in
      // 3. download()함수가 종료되면서 핸들러가실행됨. 같은 main queue라서 문제발생  
      DispatchQueue.main.sync { [weak self] in
        guard let data = imageData else { return }
        self?.itemImage.image = UIImage(data: data)
      }
    })
  }
// TableViewCell에서 ImageSetter의 download가 호출

class func download(with url: String, handler: @escaping((Data) -> Void)) {
    let cacheURL = fileManager.urls(for: .cachesDirectory, in: .userDomainMask).first!
    let imageSavingPath = cacheURL.appendingPathComponent(URL(string: url)!.lastPathComponent)

    // 2. 아래 줄 코드의 existFile이 체크되고 handler가 실행됨
    if let imageData = existFile(at: imageSavingPath) {
        handler(imageData)
    } else {
        URLSession.shared.downloadTask(with: URL(string: url)!) { (tmpLocation, response, error) in
          // do something...
        }.resume()
    }
}
  • 컴플리션 핸들러를 처리하는 코드에서 데드락 상황 발견
    • main queue에서 또 main.sync로 돌아가는 코드블럭을 호출
    • setItemImage() 함수는 ImageSetter의 download()를 호출하며, 인자로 url과 함께 컴플리션 핸들러를 넘긴다. (main queue에서 실행)
    • 해당 컴플리션 핸들러의 코드 블럭을 편의상 A블럭이라고 칭한다.
    • 이때 �A블럭의 관련 worker는 다음과 같다.
      • Assignor : setItemImage(), Assignee : download()
    • download()는 함수를 빠져나오는 시점에 A블럭을 실행한다.
    • 이때의 A블럭의 worker는
      • Assignor : download(), Assignee : setItemImage()
    • 두 worker가 모두 main queue에서 sync로 동작하기때문에, 서로가 교차하여 서로를 가리키고있고 서로 일이 끝나기만을 기다리는 데드락 상황이 발생.
    • handler의 코드를 async로 변경하여 문제를 해결할 수 있다.

테이블뷰 업데이트 시 데이터 동기화

  • 테이블뷰에 표시될 데이터를 네트워크에서 다운로드할때 데이터를 섹션 단위로 받아올때, 전체 UI를 업데이트(tableView.reloadData())하는 것이아니라 데이터가 변경된 row들만 변경되도록(tableView.insertRows()) 처리하려했지만 UI업데이트 도중 데이터 변경(비동기적으로 다운로드 진행중)으로 인해 TableView에서 크래시 발생하는 문제.
private func resetTableView(indexPaths: [IndexPath]) {
    DispatchQueue.main.sync { [weak self] in // async는 크래시발생
        self?.tableView.beginUpdates()
        self?.tableView.insertRows(at: indexPaths, with: .automatic)
        self?.tableView.endUpdates()
    }
}
  • beginUpdates()endUpdates()사이의 시점에서는 모델이 변경되면 안됨
  • 테이블뷰 데이터소스는 reload나 insert를 할때 변경이 필요한 부분(테이블 뷰 내의 특정 섹션이나 셀)을 담당하는 모델이 같은 수인지 내부적으로 확인하는 과정을 거친다.
    • 이는 변경동작이 필요없는 곳에는 동작을 하지 않고 낭비를 막으려고 이렇게 동작함.
  • 위의 코드에서 async로 동작하도록 구현하면 크래시 발생
  • insertSection/insertRows, 혹은 reloadSection/rows로 인해서 변경된 테이블뷰의 데이터 수가 변경 전과 다르다는 에러.
 Terminating app due to uncaught exception 'NSInternalInconsistencyException',
 reason: 'Invalid update: invalid number of sections.  
 The number of sections contained in the table view after the update (0) must be equal to the number of sections contained in the table view before the update (2), plus or minus the number of sections inserted or deleted (0 inserted, 0 deleted).

원인 분석 상세 & 해결

  • 확인해보니 insertRows에 전달되는 IndexPath는 모두 잘 만들어져 전달되었다.
  • 하지만 update를 시작하고 insert하고 update가 끝나는 동작이 async로 작동하여 모델 변경이 언제 되는지 모르는 문제가 있었다.
  • URLSession으로 모델 업데이트(async) - 0개의 데이터로 테이블뷰 그림 - 모델 업데이트(async) noti로 테이블뷰 업데이트 시작 - 다른 모델도 업데이트 되는 중
  • 해당 동작을 async하게 동작하면, URLSession에서 모델을 받아오는게 전부 완료되지 않고 다른 스레드에서 다른 섹션을 담당하는 모델이 업데이트되고있는 상태에서 다른 섹션의 업데이트를 비동기적으로 요청하고, 테이블뷰를 비동기적으로 업데이트하려고하니까 업데이트 전후의 데이터 수가 일치하지 않는 문제가 발생한다.
    • 쉽게말해, 1섹션의 테이블뷰를 업데이트하고있는데 2의 데이터가 만들어져서 또 2 섹션을 업데이트하라고 noti가 오는 상황
  • beginUpdates()endUpdates()사이에서 모델과 테이블뷰가 가진 데이터의 수가 같아야하는게 포인트! (업데이트 전의 데이터가 0개이면 테이블뷰의 rows도 0, 데이터가 변경되서 3개가 되면 rows도 3개여야하며, 에러메시지에서도 언급하고 있는 내용이다.)
  • 따라서 모델은 비동기로 업데이트된다고 하더라도, 테이블뷰 업데이트는 sync하게 동작하도록 하여 메인스레드에서 직렬적으로(차례대로) 업데이트되게하여 모델업데이트와 테이블뷰 업데이트의 타이밍을 맞춘다!

downloadTask() 동작방식

  • func downloadTask(with url: URL, completionHandler: @escaping (URL?, URLResponse?, Error?) -> Void) -> URLSessionDownloadTask
  • downloadTask가 일반 dataTask랑은 다르게 컴플리션 핸들러에서 data가 아닌 location(URL-첫번째파라미터)을 받는데, 어떻게 다운로드가 동작하는지?
  • 컴플리션핸들러를 거치면서 파일은 location에 저장된거고 location(URL)이 넘어옴. 해당 위치에는 이미 url로부터 다운받아진 이미지파일이 존재함.
  • 임시경로에는 /Users/jeonmijin/Library/Developer/CoreSimulator/Devices/DE1DE4FA-2208-4062-8C55-0673E3019F6C/data/Containers/Data/Application/34D0E5CC-B577-49E2-913A-82DDBA91CB59/tmp/CFNetworkDownload_nEtNzv.tmp 이런식으로 임시 파일이 저장됨
// 임시 파일 location으로 바로 이미지 가져와봄
func test() {
       let url = URL(string: "https://cdn.bmf.kr/_data/product/HCCFE/757878b14ee5a8d5af905c154fc38f01.jpg")!

       URLSession.shared.downloadTask(with: url) { (location, response, error) in
           if let error = error {
               print("\(error)")
           }
           if let location = location { // 파일이 저장된 url애 접근하여 UIImage생성
               let img = UIImage(contentsOfFile: location.path)
               DispatchQueue.main.sync {
                   self.view.addSubview(UIImageView(image: img))
               }
           }
       }.resume()
   }

캐시폴더로 파일 옮기기

  1. downloadTask의 컴플리션핸들러에서 받은 location은 tmpLocation
  2. tmp경로에서 cache path(imageSavingPath)로 move시
  3. imageSavingPath에 같은 이름이 있으면 moveError발생

해당 경로에 파일이 존재하는지 확인 후 Data생성

  do {
        try fileManager.moveItem(at: tmpLocation, to: imageSavingPath)
            let imageData = try? Data(contentsOf: imageSavingPath)
            handler(imageData)
    } catch {
        if FileManager().fileExists(atPath: imageSavingPath.path) {
            let imageData = try? Data(contentsOf: imageSavingPath)
            handler(imageData)
        } else { handler(nil) }

swift-storeapp's People

Contributors

godrm avatar jinios avatar

Watchers

 avatar

swift-storeapp's Issues

[Step6]Feedback from JK

네트워크 에러 처리

          URLSession.shared.downloadTask(with: URL(string: url)!) { (tmpLocation, response, error) in
                if let error = error {
                    print("Image download Error log: \(error)\n")
                }
                if let response = response as? HTTPURLResponse, response.statusCode == 200, let tmpLocation = tmpLocation {
                    do {
                        try fileManager.moveItem(at: tmpLocation, to: imageSavingPath)
                        if let imageData = try? Data(contentsOf: imageSavingPath) {
                            handler(imageData)
                        } else {
                            print("MOVE Error!")
                        }
                    } catch { print("MOVE Error!") }
                }
            }.resume()
        }
    }

피드백

  • JK : 여기 에러들은 로그 찍는거 말고 어떻게 할 수 있을까요? 네트워크는 항상 실패할 수 있기 때문에 대비를 해야합니다.
  • 나: 네트워크 에러인 경우에는 사용자에게 알려주고 refresh를 하거나 나중에 다시 시도해달라고 해야할 것 같습니다. 하지만 네트워크가 문제라면 refresh를 하더라도 이미지를 표시할 수는 없을것같습니다....!
    (모든 이미지를 앱에 저장해놓지 않는 이상 다른 방법이 있을까요? 🤔)
    하지만 적어도 사용자에게 문제가 있는걸 알려줘야한다고 생각합니다. 수정해보겠습니다! :)
  • JK : 맞아요 WiFi가 안되거나 여러 이미지가 실패하면 사용자에게 못 받았다고 알림창을 여러개 띄우는걸 말하는건 아니겠죠?
    네트워크에 접근하는 코드는 항상 실패를 확인해서 상위 모듈에 전달하는 구조가 있어야 해요.
    그게 return type 이든지 throw exception 이든지
    레벨2에서 exception을 메인으로 전달하던 기억을 떠올려보세요 :)

해결

  • 이미지데이터를 불러올 수 없는 상황(moveItem실패, 네트워크에러)시 refresh표시 이미지를 표시하도록 변경.

ScrollView에대한 커스텀 클래스 구현하기

setThumbnailScrollView() 나 setDetailScrollView() 에서 하는 작업은 결국 내부 뷰에 서브뷰를 추가하는 작업인데
이 부분을 ViewController에서 하지 않는다면 어떻게 분리할 수 있을까요?

[쇼핑앱만들기 Step6] 병렬처리 요구사항

3개의 JSON 데이터가 모두 받고 나면 JSON 데이터에 포함된 이미지 URL을 분리해서 Image 파일들을 다운로드 받는다.
이미지 파일들을 병렬처리해서 한꺼번에 여러개를 다운로드하도록 구성한다.
(선택1) GCD Queue를 활용하거나
(선택2)Download Task 방식으로 구현한다.
다운로드가 완료되면 앱 디렉토리 중에 Cache 디렉토리에 URL에 있는 파일명으로 저장한다.
셀을 표기할 때 이미 다운로드된 이미지가 있으면 표시하고, 새로운 파일이 다운로드 완료되면 해당 이미지를 테이블뷰 셀에 뒤늦게(lazy) 표시한다.
화면에 표시할 때 다운로드를 담당하는 스레드와 화면을 처리하는 스레드를 위한 GCD Queue를 구분해서 처리한다.
이미지를 다 받을때 까지 화면이 하얗게 멈춰있지 않도록 만든다.

[Step3] Feedback from JK

// StoreItems.swift에서 전역변수로 사용
let CATEGORIES: [Category] = [.main, .soup, .side]
  • JK: 전역변수를 사용하기 보다는 StoreItems 같은 객체 네임스페이스를 활용해서 섹션에 대한 type 변수로 static 하게 설정해주는 게 어떨까요

  • 해결:

    • 원래는 StoreItems 구조체 내부에 static으로 구현했다가, 해당 변수에 접근하는 객체들이 많아서 전역변수로 변경함.
    • 하지만 이렇게 할 경우 해당 categories 변수가 어디 소속되어있는건지 알 수 없기때문에 객체 이름을 활용해서 구분 가능하도록 static 변수로 선언하는것이 좋다.
    • 또한 만약 다른 객체 내부에 동일한 용도의 변수가 생긴다면 (이번 애플리케이션 미션에서는 그럴리 없지만) 객체 이름으로 구별할 수 있게 된다.

상품목록 JSON파일 Decode하기

JSONDecoder를 활용해서 내부에 Array 타입으로 변환하는 DataSource에서 사용할 모델 객체를 만든다.

subscript로 배열에 index로 접근하면 StoreItem 구조체를 반환한다.
StoreItem은 Decodable 프로토콜을 채택하고, main.json에 있는 키와 값을 매핑해서 속성으로 갖도록 구현한다.
UITableViewDataSource 프로토콜 구현 부분에서 cell을 위에서 만든 DataSource 모델 객체에 접근해서 테이블뷰를 표시한다.

제공된 URL로 JSON데이터를 받아서 StoreItem객체로 활용(URLSession사용)

  • 제공된 url로 JSON 데이터를 받아오는 모델 객체를 만든다.
  • HTTP 프로토콜 GET 요청으로 JSON 데이터를 받는다.
  • HTTP 요청은 URLSession 관련 프레임워크를 활용한다.
  • 응답으로 받은 JSON 데이터를 마찬가지 방법으로 Decode해서 StoreItem 객체로 변환한다.
  • 모델 객체는 응답이 도착하면 Notification을 보내서 테이블뷰의 해당 섹션만 업데이트한다.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.