Slide 1

Slide 1 text

プロダクトグロースと
 技術のベースアップを両立させる
 Rettyのアプリ開発スタイル
 Tomohiro Imaizumi @imaizume
 iOS Snack bar #1
 2022/05/13 


Slide 2

Slide 2 text

自己紹介 Tomohiro Imaizumi (@imaizume)
 ● 2019年11月入社
 ● 担当業務
 ○ iOS / Android / アプリ向けサーバー開発 
 ○ Scrum Master / 採用育成 / 業務改善 
 ● 今日話せなかったこと・個人的な話はぜひMeetyで! 


Slide 3

Slide 3 text

🔗 全国20ヶ所以上でワーケーションした私と 外で働く楽しさを語りませんか ? https://meety.net/matches/cmGHdDXSJLHC 
 🔗 User Happyを目指す iOS開発について語りませんか 
 https://meety.net/matches/wxbcVgDSiJZF 


Slide 4

Slide 4 text

目次
 1. Rettyでのアプリ開発での前提
 2. 技術的成果
 3. プロダクトグロースと技術のベースアップを両立するための具体的な 取り組み
 プロダクトと技術の成長を
 限られたリソースで両立するときの
 参考事例にしてください🙏


Slide 5

Slide 5 text

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

Slide 6

Slide 6 text

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


Slide 7

Slide 7 text

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

Slide 8

Slide 8 text

最近の成果

Slide 9

Slide 9 text

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

Slide 10

Slide 10 text

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


Slide 11

Slide 11 text

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

Slide 12

Slide 12 text

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


Slide 13

Slide 13 text

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

Slide 14

Slide 14 text

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

Slide 15

Slide 15 text

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

Slide 16

Slide 16 text

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


Slide 17

Slide 17 text

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 } }

Slide 18

Slide 18 text

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

Slide 19

Slide 19 text

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
 インシデントの再発防止は
 優先度を上げて取り組みやすい


Slide 20

Slide 20 text

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

Slide 21

Slide 21 text

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


Slide 22

Slide 22 text

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) 
 人気店ラベル


Slide 23

Slide 23 text

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


Slide 24

Slide 24 text

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

Slide 25

Slide 25 text

オフショアや外部サービスの活用 国内エンジニア
 外部サービス
 国内エンジニア
 オフショア
 外部サービス
 施策開発 施策外開発 運用 運用 施策開発 & 技術改善 施策外開発 運用 運用 施策外開発 施策開発 BEFORE
 AFTER


Slide 26

Slide 26 text

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

Slide 27

Slide 27 text

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


Slide 28

Slide 28 text

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