Upgrade to Pro — share decks privately, control downloads, hide ads and more …

プロダクトグロースと技術のベースアップを両立させるRettyのアプリ開発スタイル ...

プロダクトグロースと技術のベースアップを両立させるRettyのアプリ開発スタイル / Achieve Product Growth and Tech Update

iOS Snack bar #1 での発表資料になります
https://ios-snack-bar.connpass.com/event/246443/

Tomohiro Imaizumi

May 13, 2022
Tweet

More Decks by Tomohiro Imaizumi

Other Decks in Technology

Transcript

  1. 自己紹介 Tomohiro Imaizumi (@imaizume)
 • 2019年11月入社
 • 担当業務
 ◦ iOS

    / Android / アプリ向けサーバー開発 
 ◦ Scrum Master / 採用育成 / 業務改善 
 • 今日話せなかったこと・個人的な話はぜひMeetyで! 

  2. • チーム構成: 技術横断型 (LeSS) ◦ iOS / Android / APIサーバーを5名前後でメンテ

    ◦ 得意分野をベースに他領域もカバー (非分業) ◦ App単体だけでなくWeb・toB向けも共同で開発 • プロダクトドリブンな開発 ◦ 施策開発が中心 ◦ 「技術は手段」の位置付け ◦ 2年程前から長期戦略に基づく開発 へシフト Rettyのアプリ開発を取り巻く環境 限られたリソースで施策開発と技術向上を両立する必要
 短期的施策から長期戦略に基づく施策へ 
 1つのバックログ・チームの役割 

  3. 結果 • 過去の「点ベース」施策による技術負債が溜まりがち ◦ メンテしにくいReactNativeを使った画面 ◦ 特定UIや機能を実現するために導入した、メンテが止まったライブラリ ◦ WebViewによる実装 •

    技術的改善やベースアップ専用の時間は取りづらい ◦ ライブラリのバージョンアップが追いつかない ◦ warningやdeprecated API使用箇所の増加 • 今後の長期戦略に耐えられる基礎技術の更新がも必要に ◦ UIKitからの脱却 ◦ 自前CIのメンテコストの削減
  4. 直近2~3年のプロダクト的成果 (新機能) • Go To Eat / PayPay キャンペーン ◦

    プロダクトへの集客や回遊を上げる • 新人気店ラベル ◦ 「似た好みのユーザーさんたちがオススメするお店」を人気店として再定義 • 好きラベル ◦ その人が好き・詳しいジャンルを可視化し、好みの近い「人からお店が探せる」を目指す • マイベストリニューアル ◦ ユーザーさんのベストなお店のシェア体験を改善 • オススメラベル ◦ おすすめしている人の見える化で、人気の根拠・人から価値の信頼性を上がる • プロフィール編集画面ネイティブ化 ◦ プロフィールの表示設定がスムーズに
  5. 直近2~3年の技術的成果 • ReactNative/UIKitからの脱却 • SwiftUI/Combineを使った宣言的UI化 • Renovate導入によるライブラリ更新の定常化 • swiftlint/dangerでの自動スタイルチェック •

    uber/mockoloで自動Mock生成 • ReSwift-Thunkへの移行 • Bitrise / GitHub ActionsでTest/ベータ配信 • Feature Flagsを使った開発の推進
  6. タイムライン ReactNative廃止
 2021/06
 SwiftUI製画面リリース 
 2021/03
 Bitrise導入
 2021/04
 Renovate導入
 2021/02


    Mockolo導入
 2020/11
 swiftlint/danger導入
 2021/11
 ReSwift-Thunk移行完了 
 2022/01
 Feature Flags
 2021/11
 ネイティブプロフィール編集 
 2021/12
 Go To Eat キャンペーン 
 2020/10
 おすすめラベル
 2022/04
 マイベストリニューアル 
 2021/12
 好きラベル
 2022/02
 新人気店リリース
 2021/11
 PayPayボーナスキャンペーン 
 2021/02

  7. 新規実装でのSwiftUI/Combineの導入 • UIKitでの開発・レビューに限界が • 不安はありつつもGlobal検索画面で導入 (2021/03) ◦ 関心高いメンバーが試験的に実装 & チームに普及

    • 新規の画面 / Viewに本格導入を開始 (2021/07) ◦ 新規画面 : SwiftUI + Combine製をデフォルトに ◦ 既存実装: リプレースは基本的に無価値のためやらない • iOS 13サポート切りが必要 ◦ 導入当初はiOS13サポートが一番キツかった 😢 ◦ Rettyでは2021年末でサポートを終了 改善を単体で行わず
 日々の開発に取り入れる
 SwiftUIの導入箇所
 完全SwiftUI製
 部分的SwiftUI製

  8. struct AttributedText: UIViewRepresentable { private let attributedText: NSAttributedString private let

    linkTextAttributes: [NSAttributedString.Key: Any] private let onTap: (URL) -> Void @Binding private var height: CGFloat init( _ attributedText: NSAttributedString, linkTextAttributes: [NSAttributedString.Key: Any] = [:], dynamicHeight: Binding<CGFloat>, onTap: @escaping (URL) -> Void = { _ in } ) { _height = dynamicHeight self.attributedText = attributedText self.linkTextAttributes = linkTextAttributes self.onTap = onTap } func makeUIView(context _: Context) -> UITextView { let view = TextView(onTap: onTap) // 独自のTextViewを実装 view.delegate = view view.attributedText = attributedText view.linkTextAttributes = linkTextAttributes } } Extensionの例 : AttributedTextをSwiftUI向けに実装 class TextView: UITextView, UITextViewDelegate { private var onTap: (URL) -> Void = { _ in } init(onTap: @escaping (URL) -> Void) { self.onTap = onTap super.init(frame: .zero, textContainer: nil) } required init?(coder _: NSCoder) { fatalError() } func textView( _: UITextView, shouldInteractWith url: URL, in _: NSRange, interaction _: UITextItemInteraction ) -> Bool { onTap(url) return false } }
  9. public final class UIHostingCell<Content>: UITableViewCell where Content: View { private

    let hostingController = FixedSafeAreaInsetsHostingViewController<Content?>(rootView: nil) override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { super.init(style: style, reuseIdentifier: reuseIdentifier) hostingController.view.backgroundColor = .clear } deinit { removeHostingController() } func configure(_ view: Content, parent: UIViewController) { hostingController.rootView = view hostingController.view.invalidateIntrinsicContentSize() hostingController.view.fillConstraint(to: contentView) … } } Extensionの例 : UIHostingCell https://medium.com/@hongseongho/43321a9e9e90
  10. Renovateの導入 • renovatebot/renovate • インシデントをきかっけに ◦ 未更新ライブラリが原因でインシデント発生 ◦ 更新コスト削減のために導入 •

    導入後 ◦ 定常的に更新PRが出ている状態に ◦ ライブラリ起因のインシデントは発生せず ◦ QA項目記載のみで更新可 • 今後 ◦ QAの作業負荷軽減 (UITestの充実など) ◦ SPMへの移行 { "labels": ["renovate"], "extends": ["config:base"], "commitMessagePrefix": "[ci skip]", "packageRules": [ { "groupName": "FBSDK", "managers": ["cocoapods"], "matchPackagePatterns": ["^FBSDK"], "prPriority": 5 }, … ] } renovate.json
 インシデントの再発防止は
 優先度を上げて取り組みやすい

  11. SwiftUI統一で技術可用性と採用力強化 • UIKIt + ReactNative ▶ SwiftUIへ一本化 • 新規参入のハードルを下げる ◦

    2022新卒もSwiftUI未経験から開始し即戦力に ◦ ペアプロ・レビューも容易でデリバリが高速に ◦ AppCode + Code With Me + johnno1962/InjectionIII • AndroidでもJetpack Composeを導入 ◦ 宣言的UIフレームワークでコードの類似性が高い ◦ Android ⇔ iOS でタスクをシェアしやすくなる ▶ 技術可用性が向上 ◦ 設計・ドメイン用語に一貫性をもたせやすく • 一定基準を満たせばFlutter経験者も採用候補に チーム全体でのアウトプット量を増やす
 → 新しい事・技術改善がしやすくなる (1.01の法則)
 ≒

  12. SwiftUIとJetpack Composeの比較 (人気店ラベル) Row( verticalAlignment = Alignment.CenterVertically, modifier = Modifier

    .clip(shape = RoundedCornerShape(2.dp)) .background(brush = level.labelBackground), ) { Text( modifier = Modifier .clip(shape = RoundedCornerShape(1.dp)) .background( brush = level.categoryNameBackground ) .padding( horizontal = 4.dp, vertical = size.categoryNameVerticalPaddingSize ), text = "${name}好き", fontSize = size.fontSize, color = level.categoryNameTextColor, fontWeight = FontWeight.Bold, ) Text( modifier = Modifier.padding( horizontal = size.popularityTextHorizontalPaddingSize ), text = "人気店", fontSize = size.fontSize, color = level.popularityTextColor, fontWeight = FontWeight.Bold, ) } HStack(alignment: .center, spacing: 2) { Text("\(name)好き") .foregroundColor(level.categoryNameTextColor) .fontWeight(.bold) .font(.system(size: size.fontSize)) .padding(.horizontal, 4) .padding(.vertical, size.verticalPadding) .background(level.categoryNameBackground) .cornerRadius(1) Text("人気店") .foregroundColor(level.suffixTextColor) .fontWeight(.bold) .font(.system(size: size.fontSize)) .padding(.horizontal, 4) } .padding(2) .background(level.badgeBackground) .cornerRadius(2) SwiftUI (iOS)
 Jetpack Compose (Android) 
 人気店ラベル

  13. 運用負荷軽減のための自動化・SaaS利用 • マニュアル作業・自前メンテナンスを極力減らす • β版配信 ◦ 自前Macmini + Firebase App

    Distribution(FAD) ▶ GitHub Actions + FAD ◦ 配信用スクリプトのメンテナンスを廃止 • バナーの表出制御 ◦ 自前APIサーバー ▶ Firebase Remote Config ◦ エンジニア不要でキャンペーンバナーの表出が可能に • Slack WF ◦ QAからリリースまで関係者とのコミュニケーションを半自動化 継続的/安定的に本質的プロダクト開発ができる体制を構築

  14. オフショアの活用 継続的/安定的に本質的プロダクト開発ができる体制を構築
 • 2021年末からはオフショアを活用 • 主な依頼内容 ◦ warningの解消 (294 ▶

    38) ◦ ライブラリ更新 (ReSwift-Thunk移行など一定コストがかかる定形作業 ) ◦ 施策開発の一部 (仕様が明確かつ納期がないもの ) • 国内の開発コストを上げないため ◦ コードレビューコストの削減 (SwiftLint / danger導入) ◦ GitHubカンバン • 国内作業はQA項目作成のみ
  15. サードパーティーライブラリへの依存を増やさない • 導入から削除まで一定コストが発生 ◦ 技術の比較検討 / バージョン更新と追従 / 対応機能の置換 ◦

    Rettyでは極力依存を減らすことがコスト減になると判断 • 削除したライブラリたち ◦ siteline/SwiftUI-Introspect ▪ SwiftUI v1で仕様実現のため導入 ▶ 仕様調整 & OS13終了で不要に ◦ andreamazz/AMPopTip ▪ オンボーディング用ツールチップ表示に利用 ▶ 体験上不要と判断し削除 ◦ CEWendel/SWTableViewCell ▪ 横スワイプ可能なセルの実装に使用 ▶ 標準APIで実現可能なため削除 ◦ Alamofire/AlamofireImage ▪ APIクライアントはAlamofire、画像の取得/表示はKingfisherへ ▪ Background実行は自前実装(BackgroundTaskManager) へ 標準APIで実現するのが長期的に高コスパ

  16. • 前提 ◦ 少人数 & プロダクトドリブンのうえで小さく改善を進める ◦ 施策・技術の両面で少しずつ成果が出始めている段階 • 両立するための取り組みポイント

    ◦ タイミング: 新規実装や事故をきっかけに ◦ 複利的改善: 運用負荷軽減/開発効率向上を重視 ◦ スリム化: 標準APIで実現可能な仕様にする まとめ: プロダクトグロースと 技術のベースアップを両立させるには 限られたリソースでプロダクトと技術の成長を両立する
 参考事例になれば幸いです 🙏