Upgrade to Pro
— share decks privately, control downloads, hide ads and more …
Speaker Deck
Sign up for free
Menu
Search
Features
All features
Private URLs
Password Protection
Custom URLS
Scheduled publishing
Remove Branding
Restrict embedding
Deck Collections
Notes
Features
All features
Private URLs
Password Protection
Custom URLS
Scheduled publishing
Remove Branding
Restrict embedding
Deck Collections
Notes
Explore
Featured decks
Featured speakers
Programming
Technology
Storyboards
Explore
Featured decks
Featured speakers
Programming
Technology
Storyboards
Pricing
Search
Sign in
Sign up for free
プロダクトグロースと技術のベースアップを両立させるRettyのアプリ開発スタイル ...
Search
Sponsored
·
SiteGround - Reliable hosting with speed, security, and support you can count on.
→
Tomohiro Imaizumi
May 13, 2022
Technology
920
1
Share
Embed
Copy iframe code
Copy JS code
Copy link
Start on current slide
プロダクトグロースと技術のベースアップを両立させるRettyのアプリ開発スタイル / Achieve Product Growth and Tech Update
iOS Snack bar #1 での発表資料になります
https://ios-snack-bar.connpass.com/event/246443/
Tomohiro Imaizumi
May 13, 2022
More Decks by Tomohiro Imaizumi
See All by Tomohiro Imaizumi
VisionFrameworkで実現する - プライバシーに配慮した「顔ぼかし」機能 / Face blurring with Vision Framework
imaizume
0
400
Feature Flagを使った開発で高速かつストレスフリーなデリバリーを実現する / Fast and stress-free delivery with Feature Flag-based development
imaizume
3
4.9k
git branchを自由に操れるようになろう / Let's Play with Git branch!
imaizume
2
1.3k
スナップショットテスト実戦投入 / Practical Snapshot Testing
imaizume
14
8k
コーディング以外のエンジニアリング / About Engineering Without Coding
imaizume
1
1.9k
Firebase Remote Configの運用で知ったこと・知っておくと良いこと / Things I Learned from Operation of Firebase Remote Config
imaizume
6
4.4k
iOSアプリのテストを書きたいのに書けないあなたへ / How You Should Start to Write Your First Unit Test for iOS
imaizume
6
5.3k
循環的複雑度を上げないためのSwiftプログラミングTips / Tips of Swift Programming to Reduce Code Complexity
imaizume
11
8.7k
シングルトンではじめる状態管理と依存注入 / A way to control state using singleton pattern
imaizume
0
5.2k
Other Decks in Technology
See All in Technology
新機種発売前に見直そう!端末移行で再ログインが要るアプリ・要らないアプリは何が違うのか 〜シームレスに再開できる設計と実装〜
zozotech
PRO
0
190
あるけみー式LTスライド作成術
alchemy1115
1
210
データ_AIの事業の勝敗をわけるもの
nek0128
0
360
白金鉱業Meetup Vol.25 アウトカムが二値のデータに対するCausal Impact
brainpadpr
0
200
OpenTelemetryのメトリクスをCloudWatchに送ってPromQLで見てみた
ota1022
0
150
越境するなら専門用語を使うな高校校歌 / If you wanna cross border, you shouldn't use jargon
vtryo
0
140
俺の仕事は AIに奪われないし、たぶんその BIも要らない
hikaruri
0
470
おい、エージェントを使って終わらせろ
nwiizo
0
250
品質と信頼性を地続きにする
grimoh
0
390
AIを活用するために決めた "やらないこと" - 価値に注目する / Not betting on AI
soudai
PRO
1
520
10Xに技術的負債をもたらした「2つの境界の歪み」その構造と解消への営み
10xinc
0
1.9k
[2026-09-11]SREは誰のもの?運用エンジニアが始める 「SRE領域への越境」とチームの進化の軌跡 〜Road to NEXT CRE
tosite
0
250
Featured
See All Featured
ピンチをチャンスに:未来をつくるプロダクトロードマップ #pmconf2020
aki_iinuma
128
56k
The Anti-SEO Checklist Checklist. Pubcon Cyber Week
ryanjones
0
240
Leadership Guide Workshop - DevTernity 2021
reverentgeek
1
370
Ten Tips & Tricks for a 🌱 transition
stuffmc
1
230
Principles of Awesome APIs and How to Build Them.
keavy
128
18k
Everyday Curiosity
cassininazir
0
320
Avoiding the “Bad Training, Faster” Trap in the Age of AI
tmiket
0
240
Learning to Love Humans: Emotional Interface Design
aarron
275
41k
Building Flexible Design Systems
yeseniaperezcruz
330
41k
Public Speaking Without Barfing On Your Shoes - THAT 2023
reverentgeek
1
560
Creating an realtime collaboration tool: Agile Flush - .NET Oxford
marcduiker
35
2.6k
Getting science done with accelerated Python computing platforms
jacobtomlinson
2
480
Transcript
プロダクトグロースと 技術のベースアップを両立させる Rettyのアプリ開発スタイル Tomohiro Imaizumi @imaizume iOS Snack bar #1
2022/05/13
自己紹介 Tomohiro Imaizumi (@imaizume) • 2019年11月入社 • 担当業務 ◦ iOS
/ Android / アプリ向けサーバー開発 ◦ Scrum Master / 採用育成 / 業務改善 • 今日話せなかったこと・個人的な話はぜひMeetyで!
🔗 全国20ヶ所以上でワーケーションした私と 外で働く楽しさを語りませんか ? https://meety.net/matches/cmGHdDXSJLHC 🔗 User Happyを目指す iOS開発について語りませんか
https://meety.net/matches/wxbcVgDSiJZF
目次 1. Rettyでのアプリ開発での前提 2. 技術的成果 3. プロダクトグロースと技術のベースアップを両立するための具体的な 取り組み プロダクトと技術の成長を 限られたリソースで両立するときの
参考事例にしてください🙏
Rettyのアプリ開発を取り巻く環境
• チーム構成: 技術横断型 (LeSS) ◦ iOS / Android / APIサーバーを5名前後でメンテ
◦ 得意分野をベースに他領域もカバー (非分業) ◦ App単体だけでなくWeb・toB向けも共同で開発 • プロダクトドリブンな開発 ◦ 施策開発が中心 ◦ 「技術は手段」の位置付け ◦ 2年程前から長期戦略に基づく開発 へシフト Rettyのアプリ開発を取り巻く環境 限られたリソースで施策開発と技術向上を両立する必要 短期的施策から長期戦略に基づく施策へ 1つのバックログ・チームの役割
結果 • 過去の「点ベース」施策による技術負債が溜まりがち ◦ メンテしにくいReactNativeを使った画面 ◦ 特定UIや機能を実現するために導入した、メンテが止まったライブラリ ◦ WebViewによる実装 •
技術的改善やベースアップ専用の時間は取りづらい ◦ ライブラリのバージョンアップが追いつかない ◦ warningやdeprecated API使用箇所の増加 • 今後の長期戦略に耐えられる基礎技術の更新がも必要に ◦ UIKitからの脱却 ◦ 自前CIのメンテコストの削減
最近の成果
直近2~3年のプロダクト的成果 (新機能) • Go To Eat / PayPay キャンペーン ◦
プロダクトへの集客や回遊を上げる • 新人気店ラベル ◦ 「似た好みのユーザーさんたちがオススメするお店」を人気店として再定義 • 好きラベル ◦ その人が好き・詳しいジャンルを可視化し、好みの近い「人からお店が探せる」を目指す • マイベストリニューアル ◦ ユーザーさんのベストなお店のシェア体験を改善 • オススメラベル ◦ おすすめしている人の見える化で、人気の根拠・人から価値の信頼性を上がる • プロフィール編集画面ネイティブ化 ◦ プロフィールの表示設定がスムーズに
新機能 Go To Eatキャンペーン 好きジャンル/マイベスト 新人気店・おすすめラベル プロフィール編集
直近2~3年の技術的成果 • ReactNative/UIKitからの脱却 • SwiftUI/Combineを使った宣言的UI化 • Renovate導入によるライブラリ更新の定常化 • swiftlint/dangerでの自動スタイルチェック •
uber/mockoloで自動Mock生成 • ReSwift-Thunkへの移行 • Bitrise / GitHub ActionsでTest/ベータ配信 • Feature Flagsを使った開発の推進
タイムライン 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
プロダクトグロースと 技術のベースアップを 両立するための具体的な取り組み
両立するためのポイント 1. タイミング: 新規実装や事故をきっかけに 2. 複利的改善: 運用負荷軽減/開発効率向上を重視 3. スリム化: 標準APIで実現可能な仕様にする
1. タイミング (新規実装や事故をきっかけに)
新規実装でのSwiftUI/Combineの導入 • UIKitでの開発・レビューに限界が • 不安はありつつもGlobal検索画面で導入 (2021/03) ◦ 関心高いメンバーが試験的に実装 & チームに普及
• 新規の画面 / Viewに本格導入を開始 (2021/07) ◦ 新規画面 : SwiftUI + Combine製をデフォルトに ◦ 既存実装: リプレースは基本的に無価値のためやらない • iOS 13サポート切りが必要 ◦ 導入当初はiOS13サポートが一番キツかった 😢 ◦ Rettyでは2021年末でサポートを終了 改善を単体で行わず 日々の開発に取り入れる SwiftUIの導入箇所 完全SwiftUI製 部分的SwiftUI製
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 } }
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
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 インシデントの再発防止は 優先度を上げて取り組みやすい
2. 複利的改善 (開発効率向上 / 運用負荷軽減を重視)
SwiftUI統一で技術可用性と採用力強化 • UIKIt + ReactNative ▶ SwiftUIへ一本化 • 新規参入のハードルを下げる ◦
2022新卒もSwiftUI未経験から開始し即戦力に ◦ ペアプロ・レビューも容易でデリバリが高速に ◦ AppCode + Code With Me + johnno1962/InjectionIII • AndroidでもJetpack Composeを導入 ◦ 宣言的UIフレームワークでコードの類似性が高い ◦ Android ⇔ iOS でタスクをシェアしやすくなる ▶ 技術可用性が向上 ◦ 設計・ドメイン用語に一貫性をもたせやすく • 一定基準を満たせばFlutter経験者も採用候補に チーム全体でのアウトプット量を増やす → 新しい事・技術改善がしやすくなる (1.01の法則) ≒
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) 人気店ラベル
運用負荷軽減のための自動化・SaaS利用 • マニュアル作業・自前メンテナンスを極力減らす • β版配信 ◦ 自前Macmini + Firebase App
Distribution(FAD) ▶ GitHub Actions + FAD ◦ 配信用スクリプトのメンテナンスを廃止 • バナーの表出制御 ◦ 自前APIサーバー ▶ Firebase Remote Config ◦ エンジニア不要でキャンペーンバナーの表出が可能に • Slack WF ◦ QAからリリースまで関係者とのコミュニケーションを半自動化 継続的/安定的に本質的プロダクト開発ができる体制を構築
オフショアの活用 継続的/安定的に本質的プロダクト開発ができる体制を構築 • 2021年末からはオフショアを活用 • 主な依頼内容 ◦ warningの解消 (294 ▶
38) ◦ ライブラリ更新 (ReSwift-Thunk移行など一定コストがかかる定形作業 ) ◦ 施策開発の一部 (仕様が明確かつ納期がないもの ) • 国内の開発コストを上げないため ◦ コードレビューコストの削減 (SwiftLint / danger導入) ◦ GitHubカンバン • 国内作業はQA項目作成のみ
オフショアや外部サービスの活用 国内エンジニア 外部サービス 国内エンジニア オフショア 外部サービス 施策開発 施策外開発 運用 運用
施策開発 & 技術改善 施策外開発 運用 運用 施策外開発 施策開発 BEFORE AFTER
3. スリム化 (標準APIで実現可能な仕様にする)
サードパーティーライブラリへの依存を増やさない • 導入から削除まで一定コストが発生 ◦ 技術の比較検討 / バージョン更新と追従 / 対応機能の置換 ◦
Rettyでは極力依存を減らすことがコスト減になると判断 • 削除したライブラリたち ◦ siteline/SwiftUI-Introspect ▪ SwiftUI v1で仕様実現のため導入 ▶ 仕様調整 & OS13終了で不要に ◦ andreamazz/AMPopTip ▪ オンボーディング用ツールチップ表示に利用 ▶ 体験上不要と判断し削除 ◦ CEWendel/SWTableViewCell ▪ 横スワイプ可能なセルの実装に使用 ▶ 標準APIで実現可能なため削除 ◦ Alamofire/AlamofireImage ▪ APIクライアントはAlamofire、画像の取得/表示はKingfisherへ ▪ Background実行は自前実装(BackgroundTaskManager) へ 標準APIで実現するのが長期的に高コスパ
• 前提 ◦ 少人数 & プロダクトドリブンのうえで小さく改善を進める ◦ 施策・技術の両面で少しずつ成果が出始めている段階 • 両立するための取り組みポイント
◦ タイミング: 新規実装や事故をきっかけに ◦ 複利的改善: 運用負荷軽減/開発効率向上を重視 ◦ スリム化: 標準APIで実現可能な仕様にする まとめ: プロダクトグロースと 技術のベースアップを両立させるには 限られたリソースでプロダクトと技術の成長を両立する 参考事例になれば幸いです 🙏