Skip to content

Seungwoo-Seo/ShoppingApp

Repository files navigation

NETPING

네이버 쇼핑 API를 기반으로 만든 쇼핑 앱입니다.

검색 찜 최근 본 상품 소셜 로그인

📱 서비스

  • 최소 버전 : iOS 15.0
  • 개발 인원 : 1인
  • 개발 기간 : 2023.05.12 ~ 2023.06.21 (6주)

🚀 서비스 기능

  • 이메일 회원인증과 구글, 애플 소셜로그인 기능 제공
  • 네이버 쇼핑 API 기반 주제/스타일 추천/카테고리 상품 정보 제공
  • 검색/최근 검색어/찜/최근 본 상품 기능 제공

🛠 사용 기술

  • Swift
  • UIKit, WebKit, CryptoKit, Authentication Services
  • MVP, Singleton, Delegate Pattern
  • Alamofire, SnapKit, Kingfisher, Tabman, TTGTagCollectionView, Toast
  • CodeBase UI, AutoLayout, CompositionalLayout, DiffableDataSource, UserDefaults, Codable,
  • Firebase RealtimeDatabase, Firebase FireStore, Firebase Auth, Unit Test

💻 핵심 설명

  • Firebase Auth, Firebase FireStore를 활용해 OAuth 2.0 기반 이메일 회원인증/소셜 로그인(구글, 애플) 구현
  • 애플 로그인 구현 시, Firebase 무결성 검사를 위해 CryptoKit 기반 nonce 생성 로직 추가
  • Alamofire를 사용한 REST API 통신 구현
  • offset 기반 페이지 네이션을 통해 상품 정보 표현
  • UserDefaults를 활용해 최근 검색어 CRUD 구현, Firebase RealtimeDatabase를 활용해 찜/최근 본 상품 CRUD 구현
  • CompositionalLayout을 활용해 그리드, 리스트 레이아웃 구현
  • DiffableDataSource + Notification을 통한 Expandable Section 구현
  • BDD 기반의 Unit Test 구현

🚧 기술적 도전

1. CompositionalLayout

  • 도전 상황
    복잡한 화면을 구현할 때, UITableView + UICollectionView 구조로 구현하면 코드도 복잡해지고 핸들링하는데 많은 리소스가 들어가기 때문에 CompositionalLayout을 도입

  • 도전 결과
    단일 collectionView만으로 복잡한 레이아웃을 직관적으로 구현. 각각의 레이아웃 요소들을 모듈화 할 수 있었기에 레이아웃이 복잡해지더라도 가독성 및 유지보수 용이성 향상

/// HomeViewController에 collectionView의  layout
enum HomeCollectionViewLayout {
    case `default`

    /// layout 생성
    var createLayout: UICollectionViewLayout {
        let layout = UICollectionViewCompositionalLayout { sectionIndex, layoutEnvironment -> NSCollectionLayoutSection? in

            let section = HomeCollectionViewSectionKind(
                rawValue: sectionIndex
            )

            switch section {
            case .메인배너:
                return self.createMainBannerSection()
            case .카테고리:
                return self.createCategorySection()
            case .오늘의랭킹:
                return self.createRankSection()
            case .오늘구매해야할제품:
                return self.createTwoColumGridSection()
            case .이주의브랜드이슈:
                return self.createBrandOfTheWeekSection()
            case .지금눈에띄는후드티:
                return self.createTwoColumGridSection()
            case .서브배너:
                return self.createSubBannerSection()
            case .일초만에사로잡는나의취향:
                return self.createTwoColumGridSection()
            case .none:
                fatalError(
                    "레이아웃을 설정할 수 없는 섹션입니다."
                )
            }
        }

        return layout
    }
}
// Section
private extension HomeCollectionViewLayout {

    /// 메인배너 섹션 layout.
    /// 하나의 열에 여러 컬럼을 가진 형태
    func createMainBannerSection() -> NSCollectionLayoutSection {
        let itemSize = NSCollectionLayoutSize(
            widthDimension: .fractionalWidth(1.0),
            heightDimension: .fractionalHeight(1.0)
        )

        let item = NSCollectionLayoutItem(
            layoutSize: itemSize
        )

        let groupSize = NSCollectionLayoutSize(
            widthDimension: .fractionalWidth(1.0),
            heightDimension: .fractionalHeight(0.65)
        )

        let group = NSCollectionLayoutGroup.horizontal(
            layoutSize: groupSize,
            subitems: [item]
        )

        let section = NSCollectionLayoutSection(group: group)
        section.orthogonalScrollingBehavior = .groupPagingCentered

        return section
    }

    ...
}

2. DiffableDataSource를 활용해 Expandable Cell 구현

  • 도전 상황
    Expandable Cell을 구현하기

  • 도전 결과

    DiffableDataSourceNotification을 활용하여 구현

func didTapOutLineButton(_ sender: UIButton) {
    // 버튼의 tag값으로 현재 섹션을 찾고
    guard let section = dataSource.sectionIdentifier(for: sender.tag) else {return}

    // 전체 스냡삿이 아닌 섹션 스냅샷 생성
    var sectionSnapshot = NSDiffableDataSourceSectionSnapshot<CategoryItem>()

    // 버튼이 선택되어 있다면
    if sender.isSelected {
        // 1. 기존 스냅샷을 지운다.
        sectionSnapshot.deleteAll()

        var snapshot = NSDiffableDataSourceSnapshot<CategoryCollectionViewSectionKind, CategoryItem>()
        snapshot.appendSections(CategoryCollectionViewSectionKind.allCases)

        // 2. item을 제외하고 section들만 추가해서 스냅샷을 적용한다.
        dataSource.apply(sectionSnapshot, to: section, animatingDifferences: false)
    }

    // 버튼이 선택되어 있지 않다면
    else {
        // 1. 기존에 선택되어 있는 섹션의 스냅샷을 지운다.
        NotificationCenter.default.post(
            name: Notification.Name.likeRadioButton,
            object: sender.tag,
            userInfo: nil
        )

        // 2. section들을 추가하고
        var snapshot = NSDiffableDataSourceSnapshot<CategoryCollectionViewSectionKind, CategoryItem>()
        snapshot.appendSections(CategoryCollectionViewSectionKind.allCases)
        dataSource.apply(snapshot, animatingDifferences: false)

        let items = section.categorys

        // 3. 해당 섹션에 아이템을 추가한다.
        sectionSnapshot.append(items)

        // 4. 스냅샷 적용
        dataSource.apply(sectionSnapshot, to: section, animatingDifferences: true)
    }

    sender.toggle()
}

🚨 트러블 슈팅

1. ImageView에 cornerRadius와 shadow 동시 설정 불가 이슈

  • 문제 원인
    shadow는 뷰의 layer 외부에 그려지는데 clipsToBounds를 true로 설정하여 layer 외부의 모든 항목 제거한 상황이 원인

  • 해결 방법
    shadow만 적용한 빈 뷰를 두고 바로 위에 clipsToBounds를 true를 설정한 ImageView를 덮어씌우는 형태로 해결

private lazy var thumnailImageViewShadowView: UIView = {
    let view = UIView()
    shadowOffset = CGSize(width: 1, height: 1)
    shadowOpacity = 0.5
    shadowColor = UIColor.gray.cgColor
    return view
}()

private lazy var thumnailImageView: UIImageView = {
    let imageView = UIImageView()
    imageView.contentMode = .scaleToFill
    imageView.layer.cornerRadius = 16.0
    imageView.clipsToBounds = true

    return imageView
}()
thumnailImageViewShadowView.addSubview(thumnailImageView)

thumnailImageViewShadowView.snp.makeConstraints { make in
    make.top.equalToSuperview()
    make.leading.equalToSuperview()
    make.trailing.equalToSuperview()
}

thumnailImageView.snp.makeConstraints { make in
    make.edges.equalToSuperview()
}

About

NETPING Remote 레포입니다.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published