取り組みの背景 — この記事シリーズについて
本シリーズ「Rork 実践テクニック集」は、Rork Lab のプレミアム講座から厳選した実装パターンを、実装可能なコード例とともに解説する中級者向けガイドです。
前編(本記事)で学べること:
- アプリ設計のベストプラクティス
- UI 構築の効率的なパターン
- Supabase を使った基本的なバックエンド連携
- Jest による単体テストの書き方
後編(プレミアム記事)で扱うトピック:
- StoreKit 2 と RevenueCat による課金実装
- Detox E2E テストと本番品質の確保
- パフォーマンス最適化とオフラインファースト設計
- CI/CD パイプライン(EAS + GitHub Actions)
- AI 機能の統合(Gemini API + Cloudflare Workers)
このガイドに従えば、プロダクションレディなアプリを Rork で構築できるようになります。
アプリ設計のベストプラクティス
プロンプト設計のコツ(効果的な指示の出し方)
Rork で高品質なアプリを生成するためには、明確で構造化されたプロンプト が不可欠です。
Step 1:アプリの全体像を定義する
以下の仕様のアプリを Rork で構築します。
【アプリ名】
TaskFlow Pro
【概要】
チーム向けのタスク管理アプリ。複数ユーザーでリアルタイムにタスクを共有・更新でき、
プロジェクト管理が容易。
【対応プラットフォーム】
iOS / Android(React Native)
【ターゲットユーザー】
中小企業のプロジェクトマネージャー・チームメンバー
Step 2:主要な画面構成を列挙する
【画面一覧】
1. 認証画面
- ログイン(メール + パスワード)
- サインアップ(名前 + メール + パスワード + 企業名)
2. ホーム画面
- 本日のタスク一覧(優先度順)
- タスクの完了・未完了をトグル
- プロジェクトフィルタ
3. タスク詳細画面
- タイトル・説明・期限・優先度
- 担当者アサイン
- コメント機能
4. プロジェクト管理画面
- プロジェクト一覧
- メンバー管理
- 新規プロジェクト作成
Step 3:データモデルを指定する
【データベーススキーマ(Supabase PostgreSQL)】
users テーブル
- id (UUID, primary key)
- email (varchar, unique)
- full_name (varchar)
- company_name (varchar)
- created_at (timestamp)
projects テーブル
- id (UUID, primary key)
- user_id (UUID, foreign key)
- name (varchar)
- description (text)
- status (enum: 'active', 'archived')
- created_at (timestamp)
tasks テーブル
- id (UUID, primary key)
- project_id (UUID, foreign key)
- assigned_to (UUID, foreign key users)
- title (varchar)
- description (text)
- priority (enum: 'low', 'medium', 'high')
- status (enum: 'todo', 'in_progress', 'done')
- due_date (date)
- created_at (timestamp)
- updated_at (timestamp)
comments テーブル
- id (UUID, primary key)
- task_id (UUID, foreign key)
- user_id (UUID, foreign key)
- content (text)
- created_at (timestamp)
Step 4:認証とセキュリティを指定する
【認証・セキュリティ】
- 認証:Supabase Auth(メール + パスワード)
- RLS(Row Level Security):有効化
* users テーブル:自分のレコードのみ読み書き可能
* projects テーブル:プロジェクトメンバーのみアクセス可能
* tasks テーブル:割り当てられたユーザーと作成者がアクセス可能
- Session 管理:React Native AsyncStorage で JWT 管理
効果的なプロンプトを書くことで、Rork AI は自動的に上記のすべての要件を満たすコードを生成します。
コンポーネント分割のパターン
大規模アプリの場合、コンポーネント設計が保守性の鍵になります。
推奨フォルダ構成:
src/
├── components/
│ ├── common/
│ │ ├── Button.jsx
│ │ ├── Card.jsx
│ │ ├── Modal.jsx
│ │ └── LoadingSpinner.jsx
│ ├── auth/
│ │ ├── LoginScreen.jsx
│ │ └── SignupScreen.jsx
│ ├── tasks/
│ │ ├── TaskList.jsx
│ │ ├── TaskCard.jsx
│ │ └── TaskDetailsModal.jsx
│ └── projects/
│ ├── ProjectList.jsx
│ ├── ProjectCard.jsx
│ └── ProjectMembersModal.jsx
├── hooks/
│ ├── useAuth.js
│ ├── useSupabase.js
│ └── useTasks.js
├── context/
│ ├── AuthContext.jsx
│ └── TaskContext.jsx
├── utils/
│ ├── supabaseClient.js
│ ├── validation.js
│ └── dateFormatter.js
└── navigation/
└── RootNavigator.jsx
Custom Hook の例(useAuth):
import { useState, useEffect, useCallback } from 'react';
import { supabase } from '../utils/supabaseClient';
export const useAuth = () => {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
const session = supabase.auth.session();
setUser(session?.user ?? null);
const { subscription } = supabase.auth.onAuthStateChange(
(event, session) => {
setUser(session?.user ?? null);
setLoading(false);
}
);
return () => {
subscription?.unsubscribe();
};
}, []);
const login = useCallback(async (email, password) => {
setLoading(true);
setError(null);
try {
const { data, error } = await supabase.auth.signInWithPassword({
email,
password,
});
if (error) throw error;
setUser(data.user);
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
}, []);
const logout = useCallback(async () => {
setLoading(true);
try {
const { error } = await supabase.auth.signOut();
if (error) throw error;
setUser(null);
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
}, []);
return { user, loading, error, login, logout };
};ナビゲーション設計(Tab・Stack・Drawer)
React Navigation の構造化は、ユーザーフロー改善に直結します。
推奨パターン:Tab + Stack ハイブリッド
import { NavigationContainer } from '@react-navigation/native';
import { createNativeStackNavigator } from '@react-navigation/native-stack';
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
import { Ionicons } from '@expo/vector-icons';
import HomeScreen from '../screens/HomeScreen';
import ProjectsScreen from '../screens/ProjectsScreen';
import ProfileScreen from '../screens/ProfileScreen';
import TaskDetailsScreen from '../screens/TaskDetailsScreen';
const Stack = createNativeStackNavigator();
const Tab = createBottomTabNavigator();
const HomeStack = () => {
return (
<Stack.Navigator
screenOptions={{
headerShown: true,
headerStyle: { backgroundColor: '#0066cc' },
headerTintColor: '#fff',
headerTitleStyle: { fontWeight: '600' },
}}
>
<Stack.Screen
name="HomeList"
component={HomeScreen}
options={{ title: 'ホーム' }}
/>
<Stack.Screen
name="TaskDetails"
component={TaskDetailsScreen}
options={{ title: 'タスク詳細' }}
/>
</Stack.Navigator>
);
};
const ProjectsStack = () => {
return (
<Stack.Navigator
screenOptions={{
headerShown: true,
headerStyle: { backgroundColor: '#0066cc' },
headerTintColor: '#fff',
}}
>
<Stack.Screen
name="ProjectsList"
component={ProjectsScreen}
options={{ title: 'プロジェクト' }}
/>
<Stack.Screen
name="ProjectDetails"
component={ProjectDetailsScreen}
options={{ title: 'プロジェクト詳細' }}
/>
</Stack.Navigator>
);
};
const RootNavigator = () => {
return (
<NavigationContainer>
<Tab.Navigator
screenOptions={({ route }) => ({
headerShown: false,
tabBarIcon: ({ focused, color, size }) => {
let iconName;
if (route.name === 'Home') {
iconName = focused ? 'home' : 'home-outline';
} else if (route.name === 'Projects') {
iconName = focused ? 'folder' : 'folder-outline';
} else if (route.name === 'Profile') {
iconName = focused ? 'person' : 'person-outline';
}
return <Ionicons name={iconName} size={size} color={color} />;
},
tabBarActiveTintColor: '#0066cc',
tabBarInactiveTintColor: '#999',
})}
>
<Tab.Screen
name="Home"
component={HomeStack}
options={{ title: 'ホーム' }}
/>
<Tab.Screen
name="Projects"
component={ProjectsStack}
options={{ title: 'プロジェクト' }}
/>
<Tab.Screen
name="Profile"
component={ProfileScreen}
options={{ title: 'プロフィール' }}
/>
</Tab.Navigator>
</NavigationContainer>
);
};
export default RootNavigator;UI 構築の実践パターン
FlatList の効率的な使い方(React Native code)
大規模なリストを扱う場合、FlatList の最適化が必須です。
基本パターン(仮想化・メモ化):
import React, { useMemo, useCallback } from 'react';
import { FlatList, View, Text, StyleSheet, TouchableOpacity } from 'react-native';
const TaskListScreen = ({ tasks, onTaskPress, onCompleteTask }) => {
// タスクを優先度でソート(useMemo で再計算を防止)
const sortedTasks = useMemo(() => {
const priorityMap = { high: 1, medium: 2, low: 3 };
return [...tasks].sort(
(a, b) => priorityMap[a.priority] - priorityMap[b.priority]
);
}, [tasks]);
// キー生成関数
const keyExtractor = useCallback((item) => item.id, []);
// リスト項目コンポーネント(React.memo で再レンダー防止)
const TaskItem = React.memo(({ item }) => (
<TouchableOpacity
style={styles.taskItem}
onPress={() => onTaskPress(item.id)}
activeOpacity={0.7}
>
<View style={styles.taskContent}>
<Text style={styles.taskTitle} numberOfLines={1}>
{item.title}
</Text>
<Text style={styles.taskDescription} numberOfLines={2}>
{item.description}
</Text>
</View>
<View style={[styles.priorityBadge, styles[`priority${item.priority}`]]}>
<Text style={styles.priorityText}>
{item.priority === 'high' ? '高' : item.priority === 'medium' ? '中' : '低'}
</Text>
</View>
</TouchableOpacity>
));
return (
<FlatList
data={sortedTasks}
renderItem={({ item }) => <TaskItem item={item} />}
keyExtractor={keyExtractor}
initialNumToRender={10}
maxToRenderPerBatch={10}
updateCellsBatchingPeriod={50}
removeClippedSubviews={true}
contentContainerStyle={styles.container}
/>
);
};
const styles = StyleSheet.create({
container: {
paddingVertical: 8,
},
taskItem: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
paddingHorizontal: 16,
paddingVertical: 12,
borderBottomWidth: 1,
borderBottomColor: '#e0e0e0',
},
taskContent: {
flex: 1,
},
taskTitle: {
fontSize: 16,
fontWeight: '600',
color: '#333',
marginBottom: 4,
},
taskDescription: {
fontSize: 13,
color: '#999',
},
priorityBadge: {
paddingVertical: 4,
paddingHorizontal: 8,
borderRadius: 4,
marginLeft: 12,
},
priorityhigh: {
backgroundColor: '#ffcccc',
},
prioritymedium: {
backgroundColor: '#fff4cc',
},
prioritylow: {
backgroundColor: '#ccf0ff',
},
priorityText: {
fontSize: 12,
fontWeight: '600',
color: '#333',
},
});
export default TaskListScreen;アクセシビリティ対応の基本(VoiceOver・TalkBack)
アプリの使いやすさを障害者も含めた全員に提供することは、今や標準です。
アクセシビリティ対応のプラクティス:
import React from 'react';
import {
View,
Text,
TouchableOpacity,
StyleSheet,
AccessibilityInfo,
useAccessibilityInfo,
} from 'react-native';
const AccessibleButton = ({
title,
onPress,
accessibilityLabel,
accessibilityHint,
disabled = false,
}) => {
return (
<TouchableOpacity
onPress={onPress}
disabled={disabled}
accessible={true}
accessibilityLabel={accessibilityLabel || title}
accessibilityHint={accessibilityHint}
accessibilityRole="button"
accessibilityState={{ disabled }}
style={[styles.button, disabled && styles.buttonDisabled]}
>
<Text style={[styles.buttonText, disabled && styles.buttonTextDisabled]}>
{title}
</Text>
</TouchableOpacity>
);
};
const AccessibleTaskCard = ({ task, onPress }) => {
return (
<TouchableOpacity
onPress={onPress}
accessible={true}
accessibilityLabel={`タスク: ${task.title}`}
accessibilityHint={`優先度: ${task.priority}, 期限: ${task.dueDate}`}
accessibilityRole="button"
activeOpacity={0.7}
style={styles.card}
>
<View>
<Text
style={styles.cardTitle}
allowFontScaling={true}
maxFontSizeMultiplier={1.5}
>
{task.title}
</Text>
<Text
style={styles.cardDescription}
allowFontScaling={true}
maxFontSizeMultiplier={1.3}
>
{task.description}
</Text>
</View>
</TouchableOpacity>
);
};
const styles = StyleSheet.create({
button: {
paddingVertical: 12,
paddingHorizontal: 24,
backgroundColor: '#0066cc',
borderRadius: 8,
justifyContent: 'center',
alignItems: 'center',
minHeight: 44, // iOS アクセシビリティ最小タップ領域
},
buttonDisabled: {
opacity: 0.5,
},
buttonText: {
color: '#fff',
fontSize: 16,
fontWeight: '600',
},
buttonTextDisabled: {
color: '#ccc',
},
card: {
padding: 16,
backgroundColor: '#fff',
borderRadius: 8,
marginVertical: 8,
marginHorizontal: 16,
minHeight: 56, // タップ領域確保
},
cardTitle: {
fontSize: 18,
fontWeight: '700',
color: '#333',
marginBottom: 8,
},
cardDescription: {
fontSize: 14,
color: '#666',
lineHeight: 20,
},
});
export default { AccessibleButton, AccessibleTaskCard };デザイン品質を上げるTips(配色・タイポグラフィ)
カラーシステムの確立:
// colors.js - 一元管理
export const colors = {
// プライマリカラー(CTA・重要要素)
primary: '#0066cc',
primaryDark: '#004fa3',
primaryLight: '#e6f0ff',
// セカンダリカラー(情報・補助)
secondary: '#ff6b35',
secondaryDark: '#cc5429',
secondaryLight: '#ffe6d9',
// ニュートラル(テキスト・背景)
text: {
primary: '#1a1a1a',
secondary: '#666666',
tertiary: '#999999',
disabled: '#cccccc',
},
background: {
primary: '#ffffff',
secondary: '#f5f5f5',
tertiary: '#eeeeee',
},
border: '#e0e0e0',
// ステータスカラー
success: '#22c55e',
warning: '#f59e0b',
error: '#ef4444',
info: '#3b82f6',
};
// typography.js - フォント規格
export const typography = {
h1: {
fontSize: 32,
fontWeight: '700',
lineHeight: 40,
letterSpacing: -0.5,
},
h2: {
fontSize: 24,
fontWeight: '700',
lineHeight: 32,
letterSpacing: -0.3,
},
h3: {
fontSize: 18,
fontWeight: '600',
lineHeight: 24,
},
body: {
fontSize: 16,
fontWeight: '400',
lineHeight: 24,
},
bodySmall: {
fontSize: 14,
fontWeight: '400',
lineHeight: 20,
},
caption: {
fontSize: 12,
fontWeight: '400',
lineHeight: 16,
},
label: {
fontSize: 12,
fontWeight: '600',
lineHeight: 16,
letterSpacing: 0.5,
},
};バックエンド連携の基礎
Supabase 認証 + データベースの基本構成(TypeScript code)
Supabase クライアント初期化:
import { createClient } from '@supabase/supabase-js';
import AsyncStorage from '@react-native-async-storage/async-storage';
const supabaseUrl = process.env.SUPABASE_URL;
const supabaseAnonKey = process.env.SUPABASE_ANON_KEY;
export const supabase = createClient(supabaseUrl, supabaseAnonKey, {
auth: {
storage: AsyncStorage,
autoRefreshToken: true,
persistSession: true,
detectSessionInUrl: false,
},
});認証サービスの実装:
import { supabase } from './supabaseClient';
interface SignUpPayload {
email: string;
password: string;
fullName: string;
companyName: string;
}
interface TaskInsertPayload {
projectId: string;
title: string;
description: string;
priority: 'low' | 'medium' | 'high';
dueDate: string;
assignedTo?: string;
}
export const authService = {
async signUp(payload: SignUpPayload) {
// Supabase Auth でユーザー作成
const { data, error } = await supabase.auth.signUp({
email: payload.email,
password: payload.password,
});
if (error) throw error;
// profiles テーブルにプロフィール情報を挿入
const { error: profileError } = await supabase
.from('profiles')
.insert({
id: data.user.id,
full_name: payload.fullName,
company_name: payload.companyName,
});
if (profileError) throw profileError;
return data;
},
async signIn(email: string, password: string) {
const { data, error } = await supabase.auth.signInWithPassword({
email,
password,
});
if (error) throw error;
return data;
},
async signOut() {
const { error } = await supabase.auth.signOut();
if (error) throw error;
},
async getCurrentUser() {
const { data, error } = await supabase.auth.getUser();
if (error) throw error;
return data.user;
},
};
export const taskService = {
async getTasks(projectId: string) {
const { data, error } = await supabase
.from('tasks')
.select('*')
.eq('project_id', projectId)
.order('created_at', { ascending: false });
if (error) throw error;
return data;
},
async createTask(payload: TaskInsertPayload) {
const { data, error } = await supabase
.from('tasks')
.insert({
project_id: payload.projectId,
title: payload.title,
description: payload.description,
priority: payload.priority,
due_date: payload.dueDate,
assigned_to: payload.assignedTo,
status: 'todo',
})
.select()
.single();
if (error) throw error;
return data;
},
async updateTask(
taskId: string,
updates: Partial<{
title: string;
status: 'todo' | 'in_progress' | 'done';
priority: 'low' | 'medium' | 'high';
due_date: string;
}>
) {
const { data, error } = await supabase
.from('tasks')
.update(updates)
.eq('id', taskId)
.select()
.single();
if (error) throw error;
return data;
},
async deleteTask(taskId: string) {
const { error } = await supabase
.from('tasks')
.delete()
.eq('id', taskId);
if (error) throw error;
},
};プッシュ通知の実装基礎
Firebase Cloud Messaging(FCM)の設定:
import { initializeApp } from 'firebase/app';
import {
getMessaging,
getToken,
onMessage,
Messaging,
} from 'firebase/messaging';
const firebaseConfig = {
apiKey: process.env.FIREBASE_API_KEY,
projectId: process.env.FIREBASE_PROJECT_ID,
messagingSenderId: process.env.FIREBASE_MESSAGING_SENDER_ID,
};
const app = initializeApp(firebaseConfig);
const messaging = getMessaging(app);
export const notificationService = {
async initializeNotifications() {
try {
// FCM トークンを取得
const token = await getToken(messaging, {
vapidKey: process.env.FIREBASE_VAPID_KEY,
});
if (token) {
console.log('FCM Token:', token);
// トークンをサーバーに保存して、ユーザーにプッシュ通知を送信可能にする
await this.saveFcmToken(token);
}
} catch (error) {
console.error('FCM token error:', error);
}
},
onMessageListener() {
return new Promise((resolve) => {
onMessage(messaging, (payload) => {
console.log('Message received:', payload);
// フォアグラウンド時の通知処理
resolve(payload);
});
});
},
async saveFcmToken(token: string) {
const user = await authService.getCurrentUser();
if (!user) return;
await supabase
.from('user_fcm_tokens')
.insert({
user_id: user.id,
token,
device_type: 'mobile',
});
},
};
// 起動時に初期化
notificationService.initializeNotifications();
// フォアグラウンド時の通知受信
notificationService.onMessageListener().then((payload) => {
// UI 表示処理など
});ディープリンクの基本設定
React Navigation のディープリンク設定:
import { useLinkBuilder, useRoute } from '@react-navigation/native';
import * as Linking from 'expo-linking';
const prefix = Linking.createURL('/');
const linking = {
prefixes: [prefix, 'https://yourapp.com', 'yourapp://'],
config: {
screens: {
Home: '',
TaskDetails: 'task/:taskId',
ProjectDetails: 'project/:projectId',
UserProfile: 'profile/:userId',
},
},
};
export const RootNavigator = () => {
return (
<NavigationContainer linking={linking} fallback={<SplashScreen />}>
{/* ナビゲータ定義 */}
</NavigationContainer>
);
};
// ディープリンクを処理するカスタムフック
export const useDeepLinkHandler = () => {
const route = useRoute();
useEffect(() => {
const handleUrl = ({ url }: { url: string }) => {
const route = url.replace(/.*?:\/\//g, '');
const routeName = route.split('/')[0];
const params = route.split('/').slice(1);
if (routeName === 'task' && params[0]) {
// タスク詳細画面へ遷移
navigation.navigate('TaskDetails', { taskId: params[0] });
} else if (routeName === 'project' && params[0]) {
// プロジェクト詳細画面へ遷移
navigation.navigate('ProjectDetails', { projectId: params[0] });
}
};
const subscription = Linking.addEventListener('url', handleUrl);
return () => {
subscription.remove();
};
}, []);
};テストの基礎
Jest 単体テストの書き方(code)
テスト環境の設定:
// jest.config.js
module.exports = {
preset: 'jest-expo',
testEnvironment: 'node',
collectCoverageFrom: [
'src/**/*.{js,jsx,ts,tsx}',
'!src/**/*.d.ts',
'!src/**/index.ts',
],
coverageThreshold: {
global: {
branches: 70,
functions: 70,
lines: 70,
statements: 70,
},
},
};ユーティリティ関数のテスト:
// utils/validation.test.js
import { validateEmail, validatePassword } from './validation';
describe('validation utilities', () => {
describe('validateEmail', () => {
test('should return true for valid email', () => {
expect(validateEmail('user@example.com')).toBe(true);
});
test('should return false for invalid email', () => {
expect(validateEmail('invalid-email')).toBe(false);
expect(validateEmail('user@')).toBe(false);
expect(validateEmail('')).toBe(false);
});
});
describe('validatePassword', () => {
test('should return true for strong password', () => {
expect(validatePassword('Secure123!Pass')).toBe(true);
});
test('should return false for weak password', () => {
expect(validatePassword('weak')).toBe(false);
expect(validatePassword('12345678')).toBe(false);
expect(validatePassword('NoNumber!')).toBe(false);
});
});
});非同期テスト(API 呼び出し):
// services/taskService.test.js
import { taskService } from './taskService';
import { supabase } from '../utils/supabaseClient';
jest.mock('../utils/supabaseClient');
describe('taskService', () => {
describe('getTasks', () => {
test('should fetch tasks from Supabase', async () => {
const mockTasks = [
{ id: '1', title: 'Task 1', status: 'todo' },
{ id: '2', title: 'Task 2', status: 'in_progress' },
];
supabase.from.mockReturnValue({
select: jest.fn().mockReturnThis(),
eq: jest.fn().mockReturnThis(),
order: jest.fn().mockResolvedValue({ data: mockTasks, error: null }),
});
const tasks = await taskService.getTasks('project-1');
expect(tasks).toEqual(mockTasks);
expect(supabase.from).toHaveBeenCalledWith('tasks');
});
test('should handle Supabase errors', async () => {
const mockError = new Error('Network error');
supabase.from.mockReturnValue({
select: jest.fn().mockReturnThis(),
eq: jest.fn().mockReturnThis(),
order: jest
.fn()
.mockResolvedValue({ data: null, error: mockError }),
});
await expect(taskService.getTasks('project-1')).rejects.toThrow(
'Network error'
);
});
});
});React Native Testing Library の基本
コンポーネントテストの例:
// components/tasks/TaskCard.test.js
import React from 'react';
import { render, screen, fireEvent } from '@testing-library/react-native';
import TaskCard from './TaskCard';
describe('TaskCard', () => {
const mockTask = {
id: '1',
title: 'Test Task',
description: 'Task description',
priority: 'high',
dueDate: '2026-03-25',
};
test('should render task title and description', () => {
render(<TaskCard task={mockTask} onPress={jest.fn()} />);
expect(screen.getByText('Test Task')).toBeTruthy();
expect(screen.getByText('Task description')).toBeTruthy();
});
test('should call onPress when card is tapped', () => {
const mockOnPress = jest.fn();
render(<TaskCard task={mockTask} onPress={mockOnPress} />);
const taskButton = screen.getByRole('button');
fireEvent.press(taskButton);
expect(mockOnPress).toHaveBeenCalledWith(mockTask.id);
});
test('should display high priority badge', () => {
render(<TaskCard task={mockTask} onPress={jest.fn()} />);
expect(screen.getByText('高')).toBeTruthy();
});
});次のステップ — 後編(プレミアム)で学べること
このガイドで、Rork での基本的なアプリ設計と実装パターンを習得しました。
次のステップでは、アプリをプロダクションレディにするための高度なテクニックを学びます。
【Rork 実践テクニック集【後編】】で扱うトピック:
- アプリ収益化 — StoreKit 2、RevenueCat、AdMob の統合
- 本番品質の確保 — Detox E2E テスト、Sentry クラッシュ解析
- CI/CD パイプライン — EAS Build/Update + GitHub Actions
- パフォーマンス最適化 — メモリ管理、オフラインファースト設計
- AI 機能統合 — Gemini API ストリーミング、Cloudflare Workers
- App Store 成長戦略 — ASO、マーケティング
これらを習得すれば、Rork で数百万ダウンロードを目指すアプリが構築できるようになります。
記事情報
- 公開日:2026年3月20日
- 対応バージョン:Rork v3.2.0 以上、React Native 0.73.0 以上
- 関連記事
- 「Rork 完全ガイド 2026」(初級向け)
- 「Rork 実践テクニック集【後編】」(プレミアム・上級向け)
ご質問は Rork Lab Discord コミュニティまで。