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

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

プロダクトグロースと技術のベースアップを両立させる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. プロダクトグロースと

    技術のベースアップを両立させる

    Rettyのアプリ開発スタイル

    Tomohiro Imaizumi @imaizume

    iOS Snack bar #1

    2022/05/13 


    View Slide

  2. 自己紹介
    Tomohiro Imaizumi (@imaizume)

    ● 2019年11月入社

    ● 担当業務

    ○ iOS / Android / アプリ向けサーバー開発 

    ○ Scrum Master / 採用育成 / 業務改善 

    ● 今日話せなかったこと・個人的な話はぜひMeetyで! 


    View Slide

  3. 🔗 全国20ヶ所以上でワーケーションした私と
    外で働く楽しさを語りませんか ?
    https://meety.net/matches/cmGHdDXSJLHC 

    🔗 User Happyを目指す
    iOS開発について語りませんか 

    https://meety.net/matches/wxbcVgDSiJZF 


    View Slide

  4. 目次

    1. Rettyでのアプリ開発での前提

    2. 技術的成果

    3. プロダクトグロースと技術のベースアップを両立するための具体的な
    取り組み

    プロダクトと技術の成長を

    限られたリソースで両立するときの

    参考事例にしてください🙏


    View Slide

  5. Rettyのアプリ開発を取り巻く環境

    View Slide

  6. ● チーム構成: 技術横断型 (LeSS)
    ○ iOS / Android / APIサーバーを5名前後でメンテ
    ○ 得意分野をベースに他領域もカバー (非分業)
    ○ App単体だけでなくWeb・toB向けも共同で開発
    ● プロダクトドリブンな開発
    ○ 施策開発が中心
    ○ 「技術は手段」の位置付け
    ○ 2年程前から長期戦略に基づく開発 へシフト
    Rettyのアプリ開発を取り巻く環境
    限られたリソースで施策開発と技術向上を両立する必要

    短期的施策から長期戦略に基づく施策へ

    1つのバックログ・チームの役割

    View Slide

  7. 結果
    ● 過去の「点ベース」施策による技術負債が溜まりがち
    ○ メンテしにくいReactNativeを使った画面
    ○ 特定UIや機能を実現するために導入した、メンテが止まったライブラリ
    ○ WebViewによる実装
    ● 技術的改善やベースアップ専用の時間は取りづらい
    ○ ライブラリのバージョンアップが追いつかない
    ○ warningやdeprecated API使用箇所の増加
    ● 今後の長期戦略に耐えられる基礎技術の更新がも必要に
    ○ UIKitからの脱却
    ○ 自前CIのメンテコストの削減

    View Slide

  8. 最近の成果

    View Slide

  9. 直近2~3年のプロダクト的成果 (新機能)
    ● Go To Eat / PayPay キャンペーン
    ○ プロダクトへの集客や回遊を上げる
    ● 新人気店ラベル
    ○ 「似た好みのユーザーさんたちがオススメするお店」を人気店として再定義
    ● 好きラベル
    ○ その人が好き・詳しいジャンルを可視化し、好みの近い「人からお店が探せる」を目指す
    ● マイベストリニューアル
    ○ ユーザーさんのベストなお店のシェア体験を改善
    ● オススメラベル
    ○ おすすめしている人の見える化で、人気の根拠・人から価値の信頼性を上がる
    ● プロフィール編集画面ネイティブ化
    ○ プロフィールの表示設定がスムーズに

    View Slide

  10. 新機能
    Go To Eatキャンペーン
    
 好きジャンル/マイベスト
    
 新人気店・おすすめラベル
    
 プロフィール編集


    View Slide

  11. 直近2~3年の技術的成果
    ● ReactNative/UIKitからの脱却
    ● SwiftUI/Combineを使った宣言的UI化
    ● Renovate導入によるライブラリ更新の定常化
    ● swiftlint/dangerでの自動スタイルチェック
    ● uber/mockoloで自動Mock生成
    ● ReSwift-Thunkへの移行
    ● Bitrise / GitHub ActionsでTest/ベータ配信
    ● Feature Flagsを使った開発の推進

    View Slide

  12. タイムライン
    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


    View Slide

  13. プロダクトグロースと
    技術のベースアップを
    両立するための具体的な取り組み

    View Slide

  14. 両立するためのポイント
    1. タイミング: 新規実装や事故をきっかけに
    2. 複利的改善: 運用負荷軽減/開発効率向上を重視
    3. スリム化: 標準APIで実現可能な仕様にする

    View Slide

  15. 1. タイミング
    (新規実装や事故をきっかけに)

    View Slide

  16. 新規実装でのSwiftUI/Combineの導入
    ● UIKitでの開発・レビューに限界が
    ● 不安はありつつもGlobal検索画面で導入 (2021/03)
    ○ 関心高いメンバーが試験的に実装 & チームに普及
    ● 新規の画面 / Viewに本格導入を開始 (2021/07)
    ○ 新規画面 : SwiftUI + Combine製をデフォルトに
    ○ 既存実装: リプレースは基本的に無価値のためやらない
    ● iOS 13サポート切りが必要
    ○ 導入当初はiOS13サポートが一番キツかった 😢
    ○ Rettyでは2021年末でサポートを終了
    改善を単体で行わず

    日々の開発に取り入れる

    SwiftUIの導入箇所

    完全SwiftUI製
 部分的SwiftUI製


    View Slide

  17. 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,
    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
    }
    }

    View Slide

  18. public final class UIHostingCell: UITableViewCell where Content: View {
    private let hostingController = FixedSafeAreaInsetsHostingViewController(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

    View Slide

  19. 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

    インシデントの再発防止は

    優先度を上げて取り組みやすい


    View Slide

  20. 2. 複利的改善
    (開発効率向上 / 運用負荷軽減を重視)

    View Slide

  21. SwiftUI統一で技術可用性と採用力強化
    ● UIKIt + ReactNative ▶ SwiftUIへ一本化
    ● 新規参入のハードルを下げる
    ○ 2022新卒もSwiftUI未経験から開始し即戦力に
    ○ ペアプロ・レビューも容易でデリバリが高速に
    ○ AppCode + Code With Me + johnno1962/InjectionIII
    ● AndroidでもJetpack Composeを導入
    ○ 宣言的UIフレームワークでコードの類似性が高い
    ○ Android ⇔ iOS でタスクをシェアしやすくなる ▶ 技術可用性が向上
    ○ 設計・ドメイン用語に一貫性をもたせやすく
    ● 一定基準を満たせばFlutter経験者も採用候補に
    チーム全体でのアウトプット量を増やす

    → 新しい事・技術改善がしやすくなる (1.01の法則)

    ≒


    View Slide

  22. 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)

    人気店ラベル


    View Slide

  23. 運用負荷軽減のための自動化・SaaS利用
    ● マニュアル作業・自前メンテナンスを極力減らす
    ● β版配信
    ○ 自前Macmini + Firebase App Distribution(FAD) ▶ GitHub Actions + FAD
    ○ 配信用スクリプトのメンテナンスを廃止
    ● バナーの表出制御
    ○ 自前APIサーバー ▶ Firebase Remote Config
    ○ エンジニア不要でキャンペーンバナーの表出が可能に
    ● Slack WF
    ○ QAからリリースまで関係者とのコミュニケーションを半自動化
    継続的/安定的に本質的プロダクト開発ができる体制を構築


    View Slide

  24. オフショアの活用
    継続的/安定的に本質的プロダクト開発ができる体制を構築

    ● 2021年末からはオフショアを活用
    ● 主な依頼内容
    ○ warningの解消 (294 ▶ 38)
    ○ ライブラリ更新 (ReSwift-Thunk移行など一定コストがかかる定形作業 )
    ○ 施策開発の一部 (仕様が明確かつ納期がないもの )
    ● 国内の開発コストを上げないため
    ○ コードレビューコストの削減 (SwiftLint / danger導入)
    ○ GitHubカンバン
    ● 国内作業はQA項目作成のみ

    View Slide

  25. オフショアや外部サービスの活用
    国内エンジニア
 外部サービス
 国内エンジニア
 オフショア
 外部サービス

    施策開発
    施策外開発
    運用
    運用
    施策開発
    &
    技術改善
    施策外開発
    運用
    運用
    施策外開発
    施策開発
    BEFORE
 AFTER


    View Slide

  26. 3. スリム化
    (標準APIで実現可能な仕様にする)

    View Slide

  27. サードパーティーライブラリへの依存を増やさない
    ● 導入から削除まで一定コストが発生
    ○ 技術の比較検討 / バージョン更新と追従 / 対応機能の置換
    ○ Rettyでは極力依存を減らすことがコスト減になると判断
    ● 削除したライブラリたち
    ○ siteline/SwiftUI-Introspect
    ■ SwiftUI v1で仕様実現のため導入 ▶ 仕様調整 & OS13終了で不要に
    ○ andreamazz/AMPopTip
    ■ オンボーディング用ツールチップ表示に利用 ▶ 体験上不要と判断し削除
    ○ CEWendel/SWTableViewCell
    ■ 横スワイプ可能なセルの実装に使用 ▶ 標準APIで実現可能なため削除
    ○ Alamofire/AlamofireImage
    ■ APIクライアントはAlamofire、画像の取得/表示はKingfisherへ
    ■ Background実行は自前実装(BackgroundTaskManager) へ
    標準APIで実現するのが長期的に高コスパ


    View Slide

  28. ● 前提
    ○ 少人数 & プロダクトドリブンのうえで小さく改善を進める
    ○ 施策・技術の両面で少しずつ成果が出始めている段階
    ● 両立するための取り組みポイント
    ○ タイミング: 新規実装や事故をきっかけに
    ○ 複利的改善: 運用負荷軽減/開発効率向上を重視
    ○ スリム化: 標準APIで実現可能な仕様にする
    まとめ: プロダクトグロースと
    技術のベースアップを両立させるには
    限られたリソースでプロダクトと技術の成長を両立する

    参考事例になれば幸いです 🙏


    View Slide