
はじめに
最近、個人開発に挑戦しており 普段はweb開発をしているのですが サーバー代を節約したいということでネイティブアプリとして開発することを決めました。
(DBもsqliteを使えば、firrebaseのような外部サービスに頼る必要もない)
そのときの技術選定についてまとめたいと思います。
なぜReact Nativeを選んだのか
最大の理由は「iOS/Androidどちらのプラットフォームでも対応できること」です。
Swift/Kotlin で各プラットフォームで別々に開発するのは工数的にもしんどいため、同時開発はかなりメリットだと感じました。
また、普段Web開発をしているため、Reactのコンポーネント思考やStateなどの状態管理の知識をそのまま活かせる点も恩恵が大きいところでした。
UIの分割や再利用、責務の整理といった設計思想も自然に持ち込めます。
一方で、録音やファイル保存、権限管理などのネイティブ依存部分では調査コストが発生しました。
特に音声周りはWebにはない考慮点が多く、一筋縄ではいかない点もありました。
ただ総合的には、開発効率の高さがそれを上回ったと感じています。
アーキテクチャ方針:軽量DDD+Repositoryのみ依存逆転
設計面ではドメイン駆動設計(DDD)をベースにレイヤー分割を行いました。
また、テスト容易性の恩恵を受けるために一部クリーンアーキテクチャも取り入れております。
構成は以下の通りです。
- domain:Entity / ValueObject / ドメインルール
- application(usecase):画面単位のユースケース
- infrastructure:Repository実装(SQLiteやファイル保存)
- presentation:画面・ViewModel
意識したのは ビジネスルールを DomainのEntityやValueObjectで表現することです。
つまり、文字数などのルールをValueObject内で定義しておき、それに反したEntityをインスタンス化しようとするとエラーとするような厳格化を行いました。
クリーンアーキテクチャのように依存逆転を徹底したのはRepository境界のみです。
永続化の仕組みは変更可能性が高いため抽象化しました。
一方で、UseCaseやUIまで過度に抽象化するとコストが高いと考えたため、見送っております。
実装して感じたのは、「全部をクリーンアーキテクチャで踏襲するより、変わりやすい部分だけ組み換え可能にするのが合理的」ということでした。
抽象化は目的ではなく、変更に耐えるための手段のひとつという認識です。
UI設計:Atomic Designと責務の明確化
UI設計にはAtomic Designを採用しました。
- atoms:ButtonやTextなど最小単位
- molecules:入力+ラベルなどの組み合わせ
- organisms:一覧ブロックなどの機能単位
- templates:画面レイアウトの骨組み
この階層を明確にすることで、「どこまでがUIの責務か」が意識できるようになりました。
特に徹底したのが、UIコンポーネントはロジックを持たないという原則です。
ボタン押下時の処理や保存処理などの実行関数は、すべて上位レイヤーからPropsとして受け取る設計にしました。
- 実行ロジックはViewModelやUseCase側
- UIは見た目とイベント発火のみ
という責務分離を徹底しました。
これにより、UIは表示のみを意識し、ロジックの所在が明確になることで画面の肥大化も防ぎやすくなりました。
一方でデメリットもあります。 organisms → molecules → atoms と階層が深くなるにつれて、実行関数をPropsとして下へ渡していく「Propsの穴掘り作業」が発生しました。
上位で定義したonPress関数を複数階層を経由してatomsのButtonまで受け渡す必要があり、コード量が増える場合もあります。
コンポーネント分割を進めるほど、この傾向は顕著になります。
NativeWindを選んだ理由と課題
スタイリングにはNativeWindを採用しました。TailwindのようなユーティリティクラスをReact Nativeで使えるライブラリです。
選定理由はこれもWebの知見(Tailwind)が活きることと試行錯誤が速く回せることです。
余白やフォントサイズ、レイアウトをクラス指定で素早く調整できるため、デザインを調整しながら作る個人開発と相性が良いと感じました。クラス命名に悩む時間もなくなります。
しかしデメリットが2つほど。
- classNameが肥大化し、可読性が下がる場合があります。
import { View, Text, TouchableOpacity } from "react-native";
export const SubmitButton = ({ disabled }: { disabled?: boolean }) => {
return (
<TouchableOpacity
className={`
w-full
h-12
rounded-xl
items-center
justify-center
${disabled ? "bg-gray-400" : "bg-blue-500"}
shadow-md
active:opacity-70
`}
disabled={disabled}
>
<Text className="text-white text-base font-semibold tracking-wide">
送信する
</Text>
</TouchableOpacity>
);
};
- 一部のサードパーティ系のコンポーネントにはNativeWindが適用できないものがあります。この場合、React Nativeの素のスタイル定義とNativeWindでのスタイル定義が混ざってしまい、コードの一貫性がなくなってしまいました。
import { View, Text, StyleSheet } from "react-native";
import Animated from "react-native-reanimated";
export const Card = () => {
return (
<Animated.View
style={styles.animatedContainer}
>
<Text className="text-lg font-bold text-gray-800">
タイトル
</Text>
</Animated.View>
);
};
const styles = StyleSheet.create({
animatedContainer: {
transform: [{ scale: 1 }],
},
});
全体を通しての感想
React Nativeは、Webエンジニアが知見を活かしてネイティブアプリを作る際の良い選択肢の一つだと感じました。
軽量DDDをベースにしたレイヤーのルールを徹底することで、コードの意味づけが明確になり、機能追加時の迷いが減りました。
今回の構成は「速度を落とさずに、最低限の保守性を確保する」という観点ではバランスの取れた選択だったと感じています。
おわりに
はやり、開発というものは やってみてわかることが多く 工数や完了見込みを 予測するのは難しいと改めて感じました。
アーキテクチャや技術選定でベストな選択をしたと思っていても、それゆえの副作用や例外なども検討する必要があります。
とはいえ、自分で要件定義から実装までやるので裁量は本業と比較にならないほど大きく、自由度の高い開発というのは魅力的な体験だと感じました。
皆様がReact Nativeで開発するときの参考になれば幸いです。