-
Notifications
You must be signed in to change notification settings - Fork 0
Technology Stack
Makoto Horikawa edited this page Jul 9, 2025
·
1 revision
GitHub Power Scouterで使用している技術スタックの詳細解説です。初心者にも分かりやすく、なぜその技術を選択したのかも含めて説明します。
graph TB
subgraph "Frontend Layer"
A[React 19]
B[TypeScript]
C[CSS3 + Animations]
end
subgraph "Build Layer"
D[Vite]
E[ESLint]
F[Prettier]
end
subgraph "Data Layer"
G[GitHub REST API v3]
H[Local Storage]
end
subgraph "Deployment Layer"
I[Static Hosting]
J[GitHub Pages]
K[Cloudflare Pages]
end
A --> D
B --> D
C --> D
D --> I
I --> J
I --> K
A --> G
A --> H
| 理由 | 詳細 | メリット |
|---|---|---|
| コンポーネント指向 | UIを再利用可能なコンポーネントに分割 | 保守性・可読性の向上 |
| 宣言的UI | 状態に基づいてUIが自動更新 | バグの少ないコード |
| 豊富なエコシステム | 多数のライブラリ・ツール | 開発効率の向上 |
| React 19の新機能 | Server Components、新しいコンパイラ | パフォーマンス向上 |
// 関数コンポーネント + Hooks
import { useState, useEffect } from 'react';
const ScouterDisplay: React.FC<Props> = ({ username }) => {
const [powerLevel, setPowerLevel] = useState(0);
useEffect(() => {
// 副作用の処理
fetchUserData(username);
}, [username]);
return (
<div className="scouter-display">
{/* JSXでUIを記述 */}
</div>
);
};mindmap
root((React 19))
新機能
Server Components
React Compiler
Actions
use Hook
パフォーマンス
自動最適化
メモ化改善
バンドルサイズ削減
開発体験
型安全性向上
エラーメッセージ改善
DevTools強化
| 機能 | JavaScript | TypeScript | 影響 |
|---|---|---|---|
| 型安全性 | ❌ 実行時エラー | ✅ コンパイル時検出 | バグ削減 |
| IntelliSense | ✅ 完全サポート | 開発効率 | |
| リファクタリング | ✅ 安全 | 保守性 | |
| チーム開発 | ✅ ドキュメント化 | 品質向上 |
// GitHub API のレスポンス型
export interface GitHubUser {
login: string;
id: number;
avatar_url: string;
name: string | null;
public_repos: number;
followers: number;
created_at: string;
}
// コンポーネントのProps型
interface UserInputProps {
onScan: (username: string) => void;
isScanning: boolean;
hasDetailedData: boolean;
}
// 関数の型注釈
const calculatePowerLevel = (
userData: GitHubUser,
repoData: GitHubRepo[]
): PowerLevelResult => {
// 型安全な実装
};// tsconfig.json
{
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx",
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true
}
}comparison
title Vite vs Traditional Bundlers
section Build Speed
Vite: 95
Webpack: 60
Parcel: 70
section HMR Speed
Vite: 98
Webpack: 65
Parcel: 75
section Bundle Size
Vite: 85
Webpack: 90
Parcel: 80
| 機能 | 説明 | メリット |
|---|---|---|
| ES Modules | ネイティブESMサポート | 高速な開発サーバー |
| esbuild | Go製の超高速トランスパイラ | 10-100倍高速なビルド |
| Tree Shaking | 未使用コードの自動削除 | 小さなバンドルサイズ |
| HMR | 高速なホットリロード | 快適な開発体験 |
// vite.config.ts
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
export default defineConfig({
plugins: [react()],
base: './', // 静的ホスティング用
build: {
outDir: 'dist',
sourcemap: false,
rollupOptions: {
output: {
manualChunks: {
vendor: ['react', 'react-dom'], // ベンダーチャンク分離
},
},
},
},
server: {
port: 5173,
open: true, // 自動ブラウザ起動
},
})| CSS-in-JS vs CSS | CSS-in-JS | Pure CSS | 選択理由 |
|---|---|---|---|
| バンドルサイズ | 大きい | 小さい | ✅ パフォーマンス重視 |
| 実行時処理 | あり | なし | ✅ ランタイム負荷なし |
| キャッシュ | 困難 | 簡単 | ✅ CDN活用可能 |
| SSR対応 | 複雑 | シンプル | ✅ 静的サイト前提 |
/* 某世界的漫画風のグロー効果 */
.scouter-text {
font-family: 'Orbitron', monospace;
fill: #00ff00;
filter: url(#glow);
animation: pulse 2s ease-in-out infinite;
}
/* SVGフィルター定義 */
.glow-filter {
filter: drop-shadow(0 0 10px #00ff00);
}
/* パワーレベルカウンターアニメーション */
@keyframes power-count {
0% { transform: scale(1); }
50% { transform: scale(1.1); filter: blur(0.5px); }
100% { transform: scale(1); }
}/* Mobile First アプローチ */
.scouter-container {
width: 100%;
max-width: 500px;
}
/* タブレット */
@media (min-width: 768px) {
.scouter-container {
max-width: 600px;
}
}
/* デスクトップ */
@media (min-width: 1024px) {
.scouter-container {
max-width: 800px;
}
}flowchart LR
A[Code Write] --> B[ESLint Check]
B --> C{Errors?}
C -->|Yes| D[Fix Errors]
C -->|No| E[Prettier Format]
E --> F[Git Commit]
D --> B
// eslint.config.js
import js from '@eslint/js'
import globals from 'globals'
import reactHooks from 'eslint-plugin-react-hooks'
import reactRefresh from 'eslint-plugin-react-refresh'
import tseslint from 'typescript-eslint'
export default tseslint.config(
{ ignores: ['dist'] },
{
extends: [js.configs.recommended, ...tseslint.configs.recommended],
files: ['**/*.{ts,tsx}'],
languageOptions: {
ecmaVersion: 2020,
globals: globals.browser,
},
plugins: {
'react-hooks': reactHooks,
'react-refresh': reactRefresh,
},
rules: {
...reactHooks.configs.recommended.rules,
'react-refresh/only-export-components': [
'warn',
{ allowConstantExport: true },
],
},
},
)sequenceDiagram
participant U as User
participant C as Component
participant A as API Service
participant G as GitHub API
U->>C: Enter Username
C->>A: fetchUserData(username)
A->>G: GET /users/{username}
G-->>A: User Data
A->>G: GET /users/{username}/repos
G-->>A: Repository Data
A->>G: GET /repos/{repo}/languages
G-->>A: Language Stats
A-->>C: Processed Data
C-->>U: Display Results
// GitHub API クライアント
class GitHubApiClient {
private baseUrl = 'https://api.github.com';
constructor(private token?: string) {}
private getHeaders(): HeadersInit {
return this.token
? { 'Authorization': `token ${this.token}` }
: {};
}
async getUser(username: string): Promise<GitHubUser> {
const response = await fetch(
`${this.baseUrl}/users/${username}`,
{ headers: this.getHeaders() }
);
if (!response.ok) {
throw new Error(`User not found: ${username}`);
}
return response.json();
}
async getUserRepos(username: string): Promise<GitHubRepo[]> {
const response = await fetch(
`${this.baseUrl}/users/${username}/repos?per_page=100&sort=stars`,
{ headers: this.getHeaders() }
);
return response.json();
}
}// Token管理
class TokenManager {
private static readonly TOKEN_KEY = 'github_token';
static saveToken(token: string): void {
localStorage.setItem(this.TOKEN_KEY, token);
}
static getToken(): string | null {
return localStorage.getItem(this.TOKEN_KEY);
}
static clearToken(): void {
localStorage.removeItem(this.TOKEN_KEY);
}
static hasToken(): boolean {
return this.getToken() !== null;
}
}| 静的サイト | SPA | SSR/SSG | 選択理由 |
|---|---|---|---|
| コスト | 無料〜安価 | 高い | ✅ 予算効率 |
| パフォーマンス | 高速 | 中〜高速 | ✅ CDN配信 |
| セキュリティ | 高い | 中 | ✅ 攻撃面が少ない |
| 運用コスト | 低い | 高い | ✅ メンテナンス不要 |
graph LR
A[Built Assets] --> B{Hosting Choice}
B --> C[GitHub Pages]
B --> D[Cloudflare Pages]
B --> E[Netlify]
B --> F[Vercel]
C --> G[github.io Domain]
D --> H[Custom Domain + CDN]
E --> I[Branch Deploys]
F --> J[Serverless Functions]
# バンドルサイズ分析
npm run build
npx vite-bundle-analyzer dist| メトリクス | 目標値 | 現在値 | 状態 |
|---|---|---|---|
| Initial Bundle | < 500KB | ~205KB | ✅ |
| Vendor Chunk | < 200KB | ~12KB | ✅ |
| First Paint | < 1.5s | ~0.8s | ✅ |
| Interactive | < 3s | ~1.2s | ✅ |
// 動的インポート(コード分割)
const ResumeModal = lazy(() => import('./ResumeModal'));
// メモ化
const MemoizedComponent = memo(({ data }) => {
return <div>{data}</div>;
});
// useMemoとuseCallback
const ExpensiveComponent = ({ items }) => {
const expensiveValue = useMemo(() =>
items.reduce((sum, item) => sum + item.value, 0),
[items]
);
const handleClick = useCallback((id) => {
// クリック処理
}, []);
return <div>{expensiveValue}</div>;
};quadrant-chart
title Tech Debt vs Business Value
x-axis Low Impact --> High Impact
y-axis Low Effort --> High Effort
React Upgrade: [0.8, 0.3]
TypeScript 5.9: [0.6, 0.2]
CSS Modules: [0.4, 0.7]
Testing Setup: [0.9, 0.8]
| 期間 | 技術 | 理由 | 優先度 |
|---|---|---|---|
| 短期 | Jest + Testing Library | テストカバレッジ向上 | 高 |
| 中期 | Zustand | 状態管理の改善 | 中 |
| 長期 | React Server Components | パフォーマンス向上 | 低 |
flowchart TD
A[JavaScript基礎] --> B[TypeScript]
B --> C[React基礎]
C --> D[React Hooks]
D --> E[Vite]
E --> F[API連携]
F --> G[デプロイ]
B --> H[型設計]
C --> I[コンポーネント設計]
D --> J[パフォーマンス最適化]
| 技術 | リソース | 難易度 |
|---|---|---|
| React | React公式ドキュメント | 初級〜中級 |
| TypeScript | TypeScript Handbook | 中級 |
| Vite | Vite公式ガイド | 初級 |
| CSS Animation | MDN CSS Animations | 中級 |
次のステップ: 基本文法ガイドで実装の詳細を学びましょう。